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:
committed by
Gary Russell
parent
956cf275e1
commit
f7c59b3b18
@@ -133,7 +133,7 @@ subprojects { subproject ->
|
||||
slf4jVersion = "1.7.13"
|
||||
tomcatVersion = "8.0.30"
|
||||
smack3Version = '3.2.1'
|
||||
smackVersion = '4.1.5'
|
||||
smackVersion = '4.1.6'
|
||||
springAmqpVersion = project.hasProperty('springAmqpVersion') ? project.springAmqpVersion : '1.6.0.BUILD-SNAPSHOT'
|
||||
// springCloudClusterVersion = '1.0.0.BUILD-SNAPSHOT'
|
||||
springDataJpaVersion = '1.10.0.M1'
|
||||
@@ -705,6 +705,7 @@ project('spring-integration-xmpp') {
|
||||
compile "org.igniterealtime.smack:smack-extensions:$smackVersion"
|
||||
|
||||
testCompile project(":spring-integration-stream")
|
||||
testCompile "org.igniterealtime.smack:smack-experimental:$smackVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.integration.xmpp.config;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
*
|
||||
* @author Josh Long
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ChatMessageInboundChannelAdapterParser extends AbstractXmppInboundChannelAdapterParser {
|
||||
@@ -38,7 +40,17 @@ public class ChatMessageInboundChannelAdapterParser extends AbstractXmppInboundC
|
||||
|
||||
@Override
|
||||
protected void postProcess(Element element, ParserContext parserContext, BeanDefinitionBuilder builder){
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-payload");
|
||||
if (element.hasAttribute("extract-payload")) {
|
||||
parserContext.getReaderContext()
|
||||
.warning("The 'extract-payload' is deprecated. Use 'payload-expression' instead.", element);
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-payload");
|
||||
}
|
||||
BeanDefinition expression =
|
||||
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("payload-expression", element);
|
||||
if (expression != null) {
|
||||
builder.addPropertyValue("payloadExpression", expression);
|
||||
}
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "stanza-filter");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -16,18 +16,22 @@
|
||||
|
||||
package org.springframework.integration.xmpp.inbound;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.StanzaListener;
|
||||
import org.jivesoftware.smack.XMPPConnection;
|
||||
import org.jivesoftware.smack.filter.StanzaFilter;
|
||||
import org.jivesoftware.smack.packet.ExtensionElement;
|
||||
import org.jivesoftware.smack.packet.Stanza;
|
||||
|
||||
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.integration.expression.ExpressionUtils;
|
||||
import org.springframework.integration.xmpp.core.AbstractXmppConnectionAwareEndpoint;
|
||||
import org.springframework.integration.xmpp.support.DefaultXmppHeaderMapper;
|
||||
import org.springframework.integration.xmpp.support.XmppHeaderMapper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* This component logs in as a user and forwards any messages <em>to</em> that
|
||||
@@ -41,11 +45,15 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public class ChatMessageListeningEndpoint extends AbstractXmppConnectionAwareEndpoint {
|
||||
|
||||
private volatile boolean extractPayload = true;
|
||||
|
||||
private final StanzaListener stanzaListener = new ChatMessagePublishingStanzaListener();
|
||||
|
||||
private volatile XmppHeaderMapper headerMapper = new DefaultXmppHeaderMapper();
|
||||
private XmppHeaderMapper headerMapper = new DefaultXmppHeaderMapper();
|
||||
|
||||
private Expression payloadExpression;
|
||||
|
||||
private StanzaFilter stanzaFilter;
|
||||
|
||||
private EvaluationContext evaluationContext;
|
||||
|
||||
public ChatMessageListeningEndpoint() {
|
||||
super();
|
||||
@@ -64,11 +72,36 @@ public class ChatMessageListeningEndpoint extends AbstractXmppConnectionAwareEnd
|
||||
* Specify whether the text message body should be extracted when mapping to a
|
||||
* Spring Integration Message payload. Otherwise, the full XMPP Message will be
|
||||
* passed within the payload. This value is <em>true</em> by default.
|
||||
*
|
||||
* @param extractPayload true if the payload should be extracted.
|
||||
* @deprecated since version 4.3 in favor of {@link #setPayloadExpression(Expression)}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setExtractPayload(boolean extractPayload) {
|
||||
this.extractPayload = extractPayload;
|
||||
if (this.payloadExpression == null) {
|
||||
setPayloadExpression(extractPayload ? null : EXPRESSION_PARSER.parseExpression("#this"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a {@link StanzaFilter} to use for the incoming packets.
|
||||
* @param stanzaFilter the {@link StanzaFilter} to use
|
||||
* @since 4.3
|
||||
* @see XMPPConnection#addAsyncStanzaListener(StanzaListener, StanzaFilter)
|
||||
*/
|
||||
public void setStanzaFilter(StanzaFilter stanzaFilter) {
|
||||
this.stanzaFilter = stanzaFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a SpEL expression to evaluate a {@code payload} against an incoming
|
||||
* {@link org.jivesoftware.smack.packet.Message}.
|
||||
* @param payloadExpression the {@link Expression} for payload evaluation.
|
||||
* @since 4.3
|
||||
* @see StanzaListener
|
||||
* @see org.jivesoftware.smack.packet.Message
|
||||
*/
|
||||
public void setPayloadExpression(Expression payloadExpression) {
|
||||
this.payloadExpression = payloadExpression;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,10 +109,16 @@ public class ChatMessageListeningEndpoint extends AbstractXmppConnectionAwareEnd
|
||||
return "xmpp:inbound-channel-adapter";
|
||||
}
|
||||
|
||||
@Override protected void onInit() {
|
||||
super.onInit();
|
||||
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() {
|
||||
Assert.isTrue(this.initialized, this.getComponentName() + " [" + this.getComponentType() + "] must be initialized");
|
||||
this.xmppConnection.addAsyncStanzaListener(this.stanzaListener, null);
|
||||
Assert.isTrue(this.initialized, this.getComponentName() + " [" + this.getComponentType()
|
||||
+ "] must be initialized");
|
||||
this.xmppConnection.addAsyncStanzaListener(this.stanzaListener, this.stanzaFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,23 +137,36 @@ public class ChatMessageListeningEndpoint extends AbstractXmppConnectionAwareEnd
|
||||
org.jivesoftware.smack.packet.Message xmppMessage = (org.jivesoftware.smack.packet.Message) packet;
|
||||
Map<String, ?> mappedHeaders = headerMapper.toHeadersFromRequest(xmppMessage);
|
||||
|
||||
String messageBody = xmppMessage.getBody();
|
||||
/*
|
||||
* Since there are several types of chat messages with different ChatState (e.g., composing, paused etc)
|
||||
* we need to perform further validation since for now we only support messages that have
|
||||
* content (e.g., Use A says 'Hello' to User B). We don't yet support messages with no
|
||||
* content (e.g., User A is typing a message for User B etc.).
|
||||
* See https://jira.springsource.org/browse/INT-1728
|
||||
* Also see: packet.getExtensions()
|
||||
*/
|
||||
if (StringUtils.hasText(messageBody)){
|
||||
Object payload = (extractPayload ? messageBody : xmppMessage);
|
||||
Object messageBody = xmppMessage.getBody();
|
||||
|
||||
AbstractIntegrationMessageBuilder<?> messageBuilder =
|
||||
ChatMessageListeningEndpoint.this.getMessageBuilderFactory()
|
||||
.withPayload(payload)
|
||||
.copyHeaders(mappedHeaders);
|
||||
sendMessage(messageBuilder.build());
|
||||
if (ChatMessageListeningEndpoint.this.payloadExpression != null) {
|
||||
EvaluationContext evaluationContext = ChatMessageListeningEndpoint.this.evaluationContext;
|
||||
|
||||
List<ExtensionElement> extensions = xmppMessage.getExtensions();
|
||||
if (extensions.size() == 1) {
|
||||
ExtensionElement extension = extensions.get(0);
|
||||
evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
|
||||
evaluationContext.setVariable("extension", extension);
|
||||
}
|
||||
|
||||
messageBody = ChatMessageListeningEndpoint.this.payloadExpression
|
||||
.getValue(evaluationContext, xmppMessage);
|
||||
}
|
||||
|
||||
if (messageBody != null) {
|
||||
sendMessage(getMessageBuilderFactory()
|
||||
.withPayload(messageBody)
|
||||
.copyHeaders(mappedHeaders).build());
|
||||
}
|
||||
else if (logger.isInfoEnabled()) {
|
||||
if (ChatMessageListeningEndpoint.this.payloadExpression != null) {
|
||||
logger.info("The 'payloadExpression' ["
|
||||
+ ChatMessageListeningEndpoint.this.payloadExpression.getExpressionString()
|
||||
+ "] has been evaluated to 'null'. The XMPP Message [" + xmppMessage + "] is ignored.");
|
||||
}
|
||||
else {
|
||||
logger.info("The XMPP Message [" + xmppMessage + "] with empty body is ignored.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +106,40 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="xmppInboundAdapterType">
|
||||
<xsd:attribute name="extract-payload" type="xsd:string" default="true">
|
||||
<xsd:attribute name="extract-payload" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specifies if generated Message payload should consist of only
|
||||
the text of the XMPP message or the entire XMPP (Smack API specific) message. Default is true.
|
||||
[DEPRECATED]
|
||||
Specifies if generated Message payload should consist of only
|
||||
the text of the XMPP message or the entire XMPP (Smack API specific) message.
|
||||
Default is true.
|
||||
Deprecated since 4.3 in favor of 'payload-expression'.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="payload-expression">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
A SpEL expression to evaluate a 'payload' with the incoming
|
||||
'org.jivesoftware.smack.packet.Message' as root object.
|
||||
It useful in case of custom (XEP) XMPP interactions, e.g. GCM.
|
||||
By default a Message 'body' is used as 'payload'.
|
||||
The '#extension' SpEL variable is registered in the evaluation context
|
||||
if one and only one extension is present in the Message.
|
||||
Replaces 'extract-payload' attribute since 4.3.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="stanza-filter">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.jivesoftware.smack.filter.StanzaFilter"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
Reference to an XMPP 'org.jivesoftware.smack.filter.StanzaFilter' bean.
|
||||
See 'XMPPConnection.addAsyncStanzaListener(StanzaListener, StanzaFilter)' JavaDocs.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -162,3 +162,8 @@ The `@InboundChannelAdapter` has now an alias `channel` attribute for regular `v
|
||||
In addition the target `SourcePollingChannelAdapter` components can now resolve the target `outputChannel` bean
|
||||
from its provided name (`outputChannelName` options) in late-binding manner.
|
||||
See <<annotations>> for more information.
|
||||
|
||||
==== XMPP changes
|
||||
|
||||
The XMPP Extensions (XEP) are now supported by the XMPP channel adapters.
|
||||
See <<xmpp-extensions>> for more information.
|
||||
|
||||
@@ -61,7 +61,6 @@ We also register a `ConnectionListener` which will log connection events if the
|
||||
The Spring Integration adapters support receiving chat messages from other users in the system.
|
||||
To do this, the _Inbound Message Channel Adapter_ "logs in" as a user on your behalf and receives the messages sent to that user.
|
||||
Those messages are then forwarded to your Spring Integration client.
|
||||
The payload of the inbound Spring Integration message may be of the raw type `org.jivesoftware.smack.packet.Message`, or of the type `java.lang.String` if you set the `extract-payload` attribute's value to 'true' when configuring an adapter.
|
||||
Configuration support for the XMPP _Inbound Message Channel Adapter_ is provided via the `inbound-channel-adapter` element.
|
||||
|
||||
[source,xml]
|
||||
@@ -69,7 +68,8 @@ Configuration support for the XMPP _Inbound Message Channel Adapter_ is provided
|
||||
<int-xmpp:inbound-channel-adapter id="xmppInboundAdapter"
|
||||
channel="xmppInbound"
|
||||
xmpp-connection="testConnection"
|
||||
extract-payload="false"
|
||||
payload-expression="getExtension('google:mobile:data').json"
|
||||
stanza-filter="stanzaFilter"
|
||||
auto-startup="true"/>
|
||||
----
|
||||
|
||||
@@ -80,6 +80,42 @@ When started it will register a `PacketListener` that will listen for incoming X
|
||||
It forwards any received messages to the underlying adapter which will convert them to Spring Integration Messages and send them to the specified `channel`.
|
||||
It will unregister the `PacketListener` when it is stopped.
|
||||
|
||||
Starting with _version 4.3_ the `ChatMessageListeningEndpoint` (and its `<int-xmpp:inbound-channel-adapter>`)
|
||||
supports a `org.jivesoftware.smack.filter.StanzaFilter` injection to be registered on the provided `XMPPConnection`
|
||||
together with an internal `StanzaListener` implementation.
|
||||
See their https://www.igniterealtime.org/builds/smack/docs/latest/javadoc/org/jivesoftware/smack/XMPPConnection.html#addAsyncStanzaListener%28org.jivesoftware.smack.StanzaListener,%20org.jivesoftware.smack.filter.StanzaFilter%29[JavaDocs] for more information.
|
||||
|
||||
Also with the _version 4.3_ the `payload-expression` has been introduced for the `ChatMessageListeningEndpoint`.
|
||||
The incoming `org.jivesoftware.smack.packet.Message` represents a root object of evaluation context.
|
||||
This option is useful in case of <<xmpp-extensions>>.
|
||||
For example, for the GCM protocol we can extract the body using expression:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
payload-expression="getExtension('google:mobile:data').json"
|
||||
----
|
||||
|
||||
for the XHTML protocol:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
payload-expression="getExtension(T(org.jivesoftware.smackx.xhtmlim.packet.XHTMLExtension).NAMESPACE).bodies[0]"
|
||||
----
|
||||
|
||||
To simplify the access to the Extension in the XMPP Message, the `extension` variable is added into the
|
||||
`EvaluationContext`.
|
||||
Note, it is done only when one and only one Extension is present in the Message.
|
||||
The samples above with the `namespace` manipulations can be simplified to something like:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
payload-expression="#extension.json"
|
||||
payload-expression="#extension.bodies[0]"
|
||||
----
|
||||
|
||||
|
||||
NOTE: The `extract-payload` option has been deprecated in favor of the new `payload-expression` one.
|
||||
|
||||
[[xmpp-message-outbound-channel-adapter]]
|
||||
==== Outbound Message Channel Adapter
|
||||
|
||||
@@ -125,7 +161,7 @@ If you would like to receive notification, or notify others, of state changes, y
|
||||
|
||||
Spring Integration provides an _Inbound Presence Message Channel Adapter_ which supports receiving Presence events from other users in the system who happen to be on your Roster.
|
||||
To do this, the adapter "logs in" as a user on your behalf, registers a `RosterListener` and forwards received Presence update events as Messages to the channel identified by the `channel` attribute.
|
||||
The payload of the Message will be a `org.jivesoftware.smack.packet.Presence` object (see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/org/jivesoftware/smack/packet/Presence.html).
|
||||
The payload of the Message will be a `org.jivesoftware.smack.packet.Presence` object (see https://www.igniterealtime.org/builds/smack/docs/latest/javadoc/org/jivesoftware/smack/packet/Presence.html).
|
||||
|
||||
Configuration support for the XMPP _Inbound Presence Message Channel Adapter_ is provided via the `presence-inbound-channel-adapter` element.
|
||||
|
||||
@@ -143,7 +179,7 @@ It will register a `RosterListener` when started and will unregister that `Roste
|
||||
==== Outbound Presence Message Channel Adapter
|
||||
|
||||
Spring Integration also supports sending Presence events to be seen by other users in the network who happen to have you on their Roster.
|
||||
When you send a Message to the _Outbound Presence Message Channel Adapter_ it extracts the payload, which is expected to be of type `org.jivesoftware.smack.packet.Presence` (see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/org/jivesoftware/smack/packet/Presence.html) and sends it to the XMPP Connection, thus advertising your presence events to the rest of the network.
|
||||
When you send a Message to the _Outbound Presence Message Channel Adapter_ it extracts the payload, which is expected to be of type `org.jivesoftware.smack.packet.Presence` and sends it to the XMPP Connection, thus advertising your presence events to the rest of the network.
|
||||
|
||||
Configuration support for the XMPP _Outbound Presence Message Channel Adapter_ is provided via the `presence-outbound-channel-adapter` element.
|
||||
|
||||
@@ -218,7 +254,8 @@ public class CustomConnectionConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
For more information on the JavaConfig style of Application Context configuration, refer to the following section in the Spring Reference Manual: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/beans.html#beans-java
|
||||
For more information on the JavaConfig style of Application Context configuration, refer to the following section
|
||||
in the http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-java[Spring Reference Manual].
|
||||
|
||||
[[xmpp-message-headers]]
|
||||
=== XMPP Message Headers
|
||||
@@ -256,3 +293,110 @@ Negated patterns get priority, so a list such as
|
||||
|
||||
IMPORTANT: If you have a user defined header that begins with `!` that you *do* wish to map, you need to escape it with
|
||||
`\` thus: `STANDARD_REQUEST_HEADERS,\!myBangHeader` and it *WILL* be mapped.
|
||||
|
||||
[[xmpp-extensions]]
|
||||
=== XMPP Extensions
|
||||
|
||||
The XMPP protocol stands for **eXstensible Messaging and Presence Protocol**.
|
||||
The "extensible" part is important.
|
||||
XMPP is based around XML, a data format that supports a concept known as _namespacing_.
|
||||
|
||||
Through namespacing, you can add bits to XMPP that are not defined in the original specifications.
|
||||
This is important because the XMPP specification deliberately describes only a set of core things like:
|
||||
|
||||
- How a client connects to a server
|
||||
- Encryption (SSL/TLS)
|
||||
- Authentication
|
||||
- How servers can communicate with each other to relay messages
|
||||
- and a few other basic building blocks.
|
||||
|
||||
Once you have implemented this, you have an XMPP client and can send any kind of data you like.
|
||||
But that's not the end.
|
||||
|
||||
For example, perhaps you decide that you want to include formatting in a message (bold, italic, etc.) which is not
|
||||
defined in the core XMPP specification.
|
||||
Well, you can make up a way to do that, but unless everyone else does it the same way as you,
|
||||
no other software will be able interpret it (they will just ignore namespaces they don't understand).
|
||||
|
||||
So the XMPP Standards Foundation (XSF) publishes a series of extra documents, known as
|
||||
http://xmpp.org/extensions/xep-0001.html[XMPP Enhancement Proposals] (XEPs).
|
||||
In general each XEP describes a particular activity (from message formatting, to file transfers, multi-user
|
||||
chats and many more), and they provide a standard format for everyone to use for that activity.
|
||||
|
||||
The Smack API provides many XEP implementations with its `extensions` and `experimental`
|
||||
http://www.igniterealtime.org/builds/smack/docs/latest/documentation/extensions/index.html[projects].
|
||||
And starting with Spring Integration _version 4.3_ any XEP can be use with the existing XMPP channel adapters.
|
||||
|
||||
To be able to process XEPs or any other custom XMPP extensions, the Smack's `ProviderManager` pre-configuration
|
||||
must be provided.
|
||||
It can be done via direct usage from the `static` Java code:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
ProviderManager.addIQProvider("element", "namespace", new MyIQProvider());
|
||||
ProviderManager.addExtensionProvider("element", "namespace", new MyExtProvider());
|
||||
----
|
||||
|
||||
or via `.providers` configuration file in the specific instance and JVM argument:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
-Dsmack.provider.file=file:///c:/my/provider/mycustom.providers
|
||||
----
|
||||
|
||||
where `mycustom.providers` might be like this:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<?xml version="1.0"?>
|
||||
<smackProviders>
|
||||
<iqProvider>
|
||||
<elementName>query</elementName>
|
||||
<namespace>jabber:iq:time</namespace>
|
||||
<className>org.jivesoftware.smack.packet.Time</className>
|
||||
</iqProvider>
|
||||
|
||||
<iqProvider>
|
||||
<elementName>query</elementName>
|
||||
<namespace>http://jabber.org/protocol/disco#items</namespace>
|
||||
<className>org.jivesoftware.smackx.provider.DiscoverItemsProvider</className>
|
||||
</iqProvider>
|
||||
|
||||
<extensionProvider>
|
||||
<elementName>subscription</elementName>
|
||||
<namespace>http://jabber.org/protocol/pubsub</namespace>
|
||||
<className>org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider</className>
|
||||
</extensionProvider>
|
||||
</smackProviders>
|
||||
----
|
||||
|
||||
For example the most popular XMPP messaging extension is
|
||||
https://developers.google.com/cloud-messaging/[Google Cloud Messaging] (GCM).
|
||||
The Smack provides the particular `org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider` for that and
|
||||
registers that by default with the `smack-experimental` jar in the classpath using `experimental.providers` resource:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<!-- GCM JSON payload -->
|
||||
<extensionProvider>
|
||||
<elementName>gcm</elementName>
|
||||
<namespace>google:mobile:data</namespace>
|
||||
<className>org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider</className>
|
||||
</extensionProvider>
|
||||
----
|
||||
|
||||
Also the `GcmPacketExtension` is present for the target messaging protocol to parse incoming packets and build outgoing:
|
||||
[source,java]
|
||||
----
|
||||
GcmPacketExtension gcmExtension = (GcmPacketExtension) xmppMessage.getExtension(GcmPacketExtension.NAMESPACE);
|
||||
String message = gcmExtension.getJson());
|
||||
----
|
||||
|
||||
[source,java]
|
||||
----
|
||||
GcmPacketExtension packetExtension = new GcmPacketExtension(gcmJson);
|
||||
Message smackMessage = new Message();
|
||||
smackMessage.addExtension(packetExtension);
|
||||
----
|
||||
|
||||
See <<xmpp-message-inbound-channel-adapter>> and <<xmpp-message-outbound-channel-adapter>> above for more information.
|
||||
|
||||
Reference in New Issue
Block a user