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

@@ -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"
}
}

View File

@@ -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");
}
}

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.
@@ -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.");
}
}
}
}

View File

@@ -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>

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);
}
}
}

View File

@@ -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.

View File

@@ -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.