INT-3963: Add XMPP Extensions Support

JIRA: https://jira.spring.io/browse/INT-3963

* Update to Smack-4.1.6
* Introduce `stanza-filter` option for the `<int-xmpp:inbound-channel-adapter>`
* Introduce `payloadExpression` for the complex and specific `stanza` parsing, e.g. GCM packets
* Deprecate `extract-payload` in favor of `payload-expression`
* Add `ChatMessageListeningEndpointTests` test for GCM protocol
* Add `ChatMessageInboundChannelAdapterParser` test for new attributes
* Document changes

Polishing according PR comments

Extract `#extension` SpEL variable

Document the `#extension` SpEL variable
This commit is contained in:
Artem Bilan
2016-03-14 19:13:45 -04:00
committed by Gary Russell
parent 956cf275e1
commit f7c59b3b18
9 changed files with 443 additions and 55 deletions

View File

@@ -9,24 +9,35 @@
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<beans:bean id="testConnection" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.jivesoftware.smack.XMPPConnection"/>
<beans:bean id="testConnection" class="org.mockito.Mockito" factory-method="spy">
<beans:constructor-arg>
<beans:bean class="org.jivesoftware.smack.tcp.XMPPTCPConnection">
<beans:constructor-arg value="guest"/>
<beans:constructor-arg value="guest"/>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<channel id="xmppInbound">
<queue/>
</channel>
<beans:bean id="stanzaFilter" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.jivesoftware.smack.filter.StanzaFilter"/>
</beans:bean>
<xmpp:inbound-channel-adapter id="xmppInboundAdapter" channel="xmppInbound"
xmpp-connection="testConnection" extract-payload="false"
auto-startup="false" error-channel="errorChannel"
mapped-request-headers="foo*, xmpp*"/>
xmpp-connection="testConnection" payload-expression="#root"
auto-startup="false" error-channel="errorChannel"
mapped-request-headers="foo*, xmpp*"
stanza-filter="stanzaFilter"/>
<xmpp:inbound-channel-adapter id="autoChannel"
xmpp-connection="testConnection" extract-payload="false"
auto-startup="false" error-channel="errorChannel"
mapped-request-headers="foo*, xmpp*"/>
xmpp-connection="testConnection" extract-payload="false"
auto-startup="false" error-channel="errorChannel"
mapped-request-headers="foo*, xmpp*"/>
<bridge input-channel="autoChannel" output-channel="nullChannel" />
<bridge input-channel="autoChannel" output-channel="nullChannel"/>
</beans:beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import java.lang.reflect.Field;
import java.util.Map;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.StanzaListener;
@@ -34,10 +35,10 @@ import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.MessageChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.xmpp.inbound.ChatMessageListeningEndpoint;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
@@ -49,6 +50,7 @@ import org.springframework.util.ReflectionUtils;
* @author Mark Fisher
* @author Gunnar Hillert
* @author Florian Schmaus
* @author Artem Bilan
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@@ -69,6 +71,7 @@ public class ChatMessageInboundChannelAdapterParserTests {
private ChatMessageListeningEndpoint autoChannelAdapter;
@Test
@SuppressWarnings("rawtypes")
public void testInboundAdapter() {
ChatMessageListeningEndpoint adapter = context.getBean("xmppInboundAdapter", ChatMessageListeningEndpoint.class);
MessageChannel errorChannel = (MessageChannel) TestUtils.getPropertyValue(adapter, "errorChannel");
@@ -77,7 +80,16 @@ public class ChatMessageInboundChannelAdapterParserTests {
QueueChannel channel = (QueueChannel) TestUtils.getPropertyValue(adapter, "outputChannel");
assertEquals("xmppInbound", channel.getComponentName());
XMPPConnection connection = (XMPPConnection) TestUtils.getPropertyValue(adapter, "xmppConnection");
assertEquals(connection, context.getBean("testConnection"));
assertSame(connection, context.getBean("testConnection"));
Object stanzaFilter = context.getBean("stanzaFilter");
assertSame(stanzaFilter, TestUtils.getPropertyValue(adapter, "stanzaFilter"));
assertEquals("#root", TestUtils.getPropertyValue(adapter, "payloadExpression.expression"));
adapter.start();
Map asyncRecvListeners = TestUtils.getPropertyValue(connection, "asyncRecvListeners", Map.class);
assertEquals(1, asyncRecvListeners.size());
assertSame(stanzaFilter,
TestUtils.getPropertyValue(asyncRecvListeners.values().iterator().next(), "packetFilter"));
adapter.stop();
}
@Test

View File

@@ -16,26 +16,40 @@
package org.springframework.integration.xmpp.inbound;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.doAnswer;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.gcm.packet.GcmPacketExtension;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.xmlpull.v1.XmlPullParser;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.test.util.TestUtils;
@@ -63,7 +77,7 @@ public class ChatMessageListeningEndpointTests {
XMPPConnection connection = mock(XMPPConnection.class);
ChatMessageListeningEndpoint endpoint = new ChatMessageListeningEndpoint(connection);
doAnswer(new Answer<Object>() {
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
@@ -71,17 +85,19 @@ public class ChatMessageListeningEndpointTests {
return null;
}
}).when(connection).addAsyncStanzaListener(Mockito.any(StanzaListener.class), Mockito.any(StanzaFilter.class));
}).given(connection)
.addAsyncStanzaListener(Mockito.any(StanzaListener.class), Mockito.any(StanzaFilter.class));
doAnswer(new Answer<Object>() {
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
packetListSet.remove((StanzaListener) invocation.getArguments()[0]);
packetListSet.remove(invocation.getArguments()[0]);
return null;
}
}).when(connection).removeAsyncStanzaListener(Mockito.any(StanzaListener.class));
}).given(connection)
.removeAsyncStanzaListener(Mockito.any(StanzaListener.class));
assertEquals(0, packetListSet.size());
endpoint.setOutputChannel(new QueueChannel());
@@ -151,4 +167,110 @@ public class ChatMessageListeningEndpointTests {
assertEquals("hello", ((MessagingException) msg.getPayload()).getFailedMessage().getPayload());
}
@Test
@SuppressWarnings("deprecation")
public void testExpression() throws Exception {
TestXMPPConnection testXMPPConnection = new TestXMPPConnection();
QueueChannel inputChannel = new QueueChannel();
ChatMessageListeningEndpoint endpoint = new ChatMessageListeningEndpoint(testXMPPConnection);
endpoint.setExtractPayload(false);
endpoint.setOutputChannel(inputChannel);
endpoint.setBeanFactory(mock(BeanFactory.class));
endpoint.afterPropertiesSet();
endpoint.start();
Message smackMessage = new Message();
smackMessage.setBody("foo");
XmlPullParser xmlPullParser = PacketParserUtils.newXmppParser(new StringReader(smackMessage.toString()));
xmlPullParser.next();
testXMPPConnection.parseAndProcessStanza(xmlPullParser);
org.springframework.messaging.Message<?> receive = inputChannel.receive(10000);
assertNotNull(receive);
Object payload = receive.getPayload();
assertThat(payload, instanceOf(Message.class));
assertEquals(smackMessage.getStanzaId(), ((Message) payload).getStanzaId());
assertEquals(smackMessage.getBody(), ((Message) payload).getBody());
Log logger = Mockito.spy(TestUtils.getPropertyValue(endpoint, "logger", Log.class));
given(logger.isInfoEnabled()).willReturn(true);
new DirectFieldAccessor(endpoint).setPropertyValue("logger", logger);
endpoint.setPayloadExpression(null);
smackMessage = new Message();
xmlPullParser = PacketParserUtils.newXmppParser(new StringReader(smackMessage.toString()));
xmlPullParser.next();
testXMPPConnection.parseAndProcessStanza(xmlPullParser);
ArgumentCaptor<String> argumentCaptor = new ArgumentCaptor<String>();
verify(logger).info(argumentCaptor.capture());
assertEquals("The XMPP Message [" + smackMessage + "] with empty body is ignored.",
argumentCaptor.getValue());
endpoint.stop();
}
@Test
public void testGcmExtension() throws Exception {
String data = "{\n" +
" \"to\":\"me\",\n" +
" \"notification\": {\n" +
" \"title\": \"Something interesting\",\n" +
" \"text\": \"Here we go\"\n" +
" },\n" +
" \"time_to_live\":\"600\"\n" +
" }\n" +
"}";
GcmPacketExtension packetExtension = new GcmPacketExtension(data);
Message smackMessage = new Message();
smackMessage.addExtension(packetExtension);
TestXMPPConnection testXMPPConnection = new TestXMPPConnection();
QueueChannel inputChannel = new QueueChannel();
ChatMessageListeningEndpoint endpoint = new ChatMessageListeningEndpoint(testXMPPConnection);
Expression payloadExpression = new SpelExpressionParser().parseExpression("#extension.json");
endpoint.setPayloadExpression(payloadExpression);
endpoint.setOutputChannel(inputChannel);
endpoint.setBeanFactory(mock(BeanFactory.class));
endpoint.afterPropertiesSet();
endpoint.start();
XmlPullParser xmlPullParser = PacketParserUtils.newXmppParser(new StringReader(smackMessage.toString()));
xmlPullParser.next();
testXMPPConnection.parseAndProcessStanza(xmlPullParser);
org.springframework.messaging.Message<?> receive = inputChannel.receive(10000);
assertNotNull(receive);
assertEquals(data, receive.getPayload());
endpoint.stop();
}
private static class TestXMPPConnection extends XMPPTCPConnection {
private TestXMPPConnection() {
super(null);
}
@Override
protected void parseAndProcessStanza(XmlPullParser parser) throws Exception {
super.parseAndProcessStanza(parser);
}
}
}