From f7c59b3b18ccad008ead795c7efa32e5f70afc2f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 14 Mar 2016 19:13:45 -0400 Subject: [PATCH] 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 `` * 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 --- build.gradle | 3 +- ...hatMessageInboundChannelAdapterParser.java | 16 +- .../inbound/ChatMessageListeningEndpoint.java | 104 +++++++++--- .../config/spring-integration-xmpp-4.3.xsd | 35 +++- ...boundChannelAdapterParserTests-context.xml | 29 +++- ...ssageInboundChannelAdapterParserTests.java | 18 +- .../ChatMessageListeningEndpointTests.java | 134 ++++++++++++++- src/reference/asciidoc/whats-new.adoc | 5 + src/reference/asciidoc/xmpp.adoc | 154 +++++++++++++++++- 9 files changed, 443 insertions(+), 55 deletions(-) diff --git a/build.gradle b/build.gradle index ea1ed5062d..beecaba219 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } } diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParser.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParser.java index d0c2f14117..acb54ee523 100644 --- a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParser.java +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParser.java @@ -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"); } } diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpoint.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpoint.java index 0ac0a054e1..5fd1e06666 100644 --- a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpoint.java +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpoint.java @@ -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 to 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 true 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 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 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."); + } } } } diff --git a/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.3.xsd b/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.3.xsd index 5506b1e90b..cae1df791f 100644 --- a/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.3.xsd +++ b/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.3.xsd @@ -106,11 +106,40 @@ - + - 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'. + + + + + + + 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. + + + + + + + + + + + + Reference to an XMPP 'org.jivesoftware.smack.filter.StanzaFilter' bean. + See 'XMPPConnection.addAsyncStanzaListener(StanzaListener, StanzaFilter)' JavaDocs. diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests-context.xml b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests-context.xml index 5e92f61f8d..47cf66bdcb 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests-context.xml +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests-context.xml @@ -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"> - - + + + + + + + + + + + + + xmpp-connection="testConnection" payload-expression="#root" + auto-startup="false" error-channel="errorChannel" + mapped-request-headers="foo*, xmpp*" + stanza-filter="stanzaFilter"/> + xmpp-connection="testConnection" extract-payload="false" + auto-startup="false" error-channel="errorChannel" + mapped-request-headers="foo*, xmpp*"/> - + diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests.java index e925bd86e6..19d15a4960 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests.java +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageInboundChannelAdapterParserTests.java @@ -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 diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpointTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpointTests.java index 633d9507a8..3fe3d00511 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpointTests.java +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/inbound/ChatMessageListeningEndpointTests.java @@ -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() { + willAnswer(new Answer() { @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() { + willAnswer(new Answer() { @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 argumentCaptor = new ArgumentCaptor(); + + 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); + } + + } + + + } diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 683cb81894..5416f674c2 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -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 <> for more information. + +==== XMPP changes + +The XMPP Extensions (XEP) are now supported by the XMPP channel adapters. +See <> for more information. diff --git a/src/reference/asciidoc/xmpp.adoc b/src/reference/asciidoc/xmpp.adoc index 7b2550c89a..6bd34c8752 100644 --- a/src/reference/asciidoc/xmpp.adoc +++ b/src/reference/asciidoc/xmpp.adoc @@ -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 ---- @@ -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 ``) +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 <>. +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] +---- + + + + query + jabber:iq:time + org.jivesoftware.smack.packet.Time + + + + query + http://jabber.org/protocol/disco#items + org.jivesoftware.smackx.provider.DiscoverItemsProvider + + + + subscription + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider + + +---- + +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 + google:mobile:data + org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider + +---- + +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 <> and <> above for more information.