diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParser.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParser.java index 774e392b65..5a630413fa 100644 --- a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParser.java +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParser.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. @@ -17,17 +17,38 @@ package org.springframework.integration.xmpp.config; +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.xmpp.outbound.ChatMessageSendingMessageHandler; +import org.springframework.util.StringUtils; + /** * Parser for the XMPP 'outbound-channel-adapter' element * * @author Oleg Zhurakousky + * @author Artem Bilan + * * @since 2.0 */ public class ChatMessageOutboundChannelAdapterParser extends AbstractXmppOutboundChannelAdapterParser { @Override protected String getHandlerClassName() { - return "org.springframework.integration.xmpp.outbound.ChatMessageSendingMessageHandler"; + return ChatMessageSendingMessageHandler.class.getName(); + } + + @Override + protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) { + AbstractBeanDefinition beanDefinition = super.parseConsumer(element, parserContext); + String extensionProvider = element.getAttribute("extension-provider"); + if (StringUtils.hasText(extensionProvider)) { + beanDefinition.getPropertyValues() + .addPropertyValue("extensionProvider", new RuntimeBeanReference(extensionProvider)); + } + return beanDefinition; } } diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandler.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandler.java index 78d9674f1d..9d6246e5cc 100644 --- a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandler.java +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandler.java @@ -16,8 +16,15 @@ package org.springframework.integration.xmpp.outbound; +import java.io.StringReader; +import java.util.regex.Pattern; + import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.xmlpull.v1.XmlPullParser; import org.springframework.integration.xmpp.XmppHeaders; import org.springframework.integration.xmpp.core.AbstractXmppConnectionAwareMessageHandler; @@ -40,8 +47,11 @@ import org.springframework.util.StringUtils; */ public class ChatMessageSendingMessageHandler extends AbstractXmppConnectionAwareMessageHandler { + private static final Pattern xmlPattern = Pattern.compile("<(\\S[^>\\s]*)[^>]*>[^<]*"); + private volatile XmppHeaderMapper headerMapper = new DefaultXmppHeaderMapper(); + private ExtensionElementProvider extensionProvider; public ChatMessageSendingMessageHandler() { super(); @@ -56,6 +66,17 @@ public class ChatMessageSendingMessageHandler extends AbstractXmppConnectionAwar this.headerMapper = headerMapper; } + /** + * Specify an {@link ExtensionElementProvider} to build an {@link ExtensionElement} + * for the {@link org.jivesoftware.smack.packet.Message#addExtension(ExtensionElement)} + * instead of {@code body}. + * @param extensionProvider the {@link ExtensionElementProvider} to use. + * @since 4.3 + */ + public void setExtensionProvider(ExtensionElementProvider extensionProvider) { + this.extensionProvider = extensionProvider; + } + @Override public String getComponentType() { return "xmpp:outbound-channel-adapter"; @@ -64,26 +85,50 @@ public class ChatMessageSendingMessageHandler extends AbstractXmppConnectionAwar @Override protected void handleMessageInternal(Message message) throws Exception { Assert.isTrue(this.initialized, getComponentName() + "#" + this.getComponentType() + " must be initialized"); - Object messageBody = message.getPayload(); + Object payload = message.getPayload(); org.jivesoftware.smack.packet.Message xmppMessage = null; - if (messageBody instanceof org.jivesoftware.smack.packet.Message) { - xmppMessage = (org.jivesoftware.smack.packet.Message) messageBody; + if (payload instanceof org.jivesoftware.smack.packet.Message) { + xmppMessage = (org.jivesoftware.smack.packet.Message) payload; } - else if (messageBody instanceof String) { + else { String to = message.getHeaders().get(XmppHeaders.TO, String.class); Assert.state(StringUtils.hasText(to), "The '" + XmppHeaders.TO + "' header must not be null"); xmppMessage = new org.jivesoftware.smack.packet.Message(to); - if (this.headerMapper != null) { - this.headerMapper.fromHeadersToRequest(message.getHeaders(), xmppMessage); + + if (payload instanceof ExtensionElement) { + xmppMessage.addExtension((ExtensionElement) payload); + } + else if (payload instanceof String) { + if (this.extensionProvider != null) { + String data = (String) payload; + if (!xmlPattern.matcher(data.trim()).matches()) { + // Since XMPP Extension parsers deal only with XML content, + // add an arbitrary tag that is removed by the extension parser, + // if the target content isn't XML. + data = "" + data + ""; + } + XmlPullParser xmlPullParser = PacketParserUtils.newXmppParser(new StringReader(data)); + xmlPullParser.next(); + ExtensionElement extension = this.extensionProvider.parse(xmlPullParser); + xmppMessage.addExtension(extension); + } + else { + xmppMessage.setBody((String) payload); + } + } + else { + throw new MessageHandlingException(message, + "Only payloads of type java.lang.String, org.jivesoftware.smack.packet.Message " + + "or org.jivesoftware.smack.packet.ExtensionElement " + + "are supported. Received [" + payload.getClass().getName() + + "]. Consider adding a Transformer prior to this adapter."); } - xmppMessage.setBody((String) messageBody); } - else { - throw new MessageHandlingException(message, - "Only payloads of type java.lang.String or org.jivesoftware.smack.packet.Message " + - "are supported. Received [" + messageBody.getClass().getName() + - "]. Consider adding a Transformer prior to this adapter."); + + if (this.headerMapper != null) { + this.headerMapper.fromHeadersToRequest(message.getHeaders(), xmppMessage); } + if (!this.xmppConnection.isConnected() && this.xmppConnection instanceof AbstractXMPPConnection) { ((AbstractXMPPConnection) this.xmppConnection).connect(); } 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 2123abe4b5..e2d11c5022 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 @@ -160,7 +160,21 @@ - + + + + + + + + + + Reference to org.jivesoftware.smack.provider.ExtensionElementProvider bean. + + + + diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests-context.xml b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests-context.xml index 0f991cb123..5b35e3a84c 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests-context.xml +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests-context.xml @@ -13,10 +13,13 @@ + + + mapped-request-headers="foo*, bar*" + extension-provider="testExtensionProvider"/> diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java index f5eb727e84..7993882186 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.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. @@ -17,12 +17,14 @@ package org.springframework.integration.xmpp.config; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smackx.jiveproperties.JivePropertiesManager; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,21 +34,21 @@ import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.integration.mapping.AbstractHeaderMapper; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; import org.springframework.integration.channel.QueueChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.SubscribableChannel; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.endpoint.PollingConsumer; import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice; -import org.springframework.messaging.support.GenericMessage; +import org.springframework.integration.mapping.AbstractHeaderMapper; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.xmpp.XmppHeaders; import org.springframework.integration.xmpp.support.DefaultXmppHeaderMapper; import org.springframework.integration.xmpp.support.XmppHeaderMapper; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.messaging.support.GenericMessage; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -67,6 +69,9 @@ public class ChatMessageOutboundChannelAdapterParserTests { @Autowired private XmppHeaderMapper headerMapper; + @Autowired + private ExtensionElementProvider extensionElementProvider; + private static volatile int adviceCalled; @Test @@ -107,6 +112,11 @@ public class ChatMessageOutboundChannelAdapterParserTests { assertFalse(requestHeaderMatcher.matchHeader("biz")); assertFalse(requestHeaderMatcher.matchHeader("else")); assertTrue(eventConsumer instanceof EventDrivenConsumer); + + MessageHandler outboundEventAdapterHandle = + context.getBean("outboundEventAdapter.handler", MessageHandler.class); + assertSame(this.extensionElementProvider, + TestUtils.getPropertyValue(outboundEventAdapterHandle, "extensionProvider")); } @SuppressWarnings("rawtypes") diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandlerTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandlerTests.java index 3f9461e5d4..4a1c0c6b50 100644 --- a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandlerTests.java +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandlerTests.java @@ -16,14 +16,21 @@ package org.springframework.integration.xmpp.outbound; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smackx.gcm.packet.GcmPacketExtension; +import org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; @@ -46,7 +53,7 @@ public class ChatMessageSendingMessageHandlerTests { @Test - public void validateMessagePostAsString() throws Exception { + public void testSendMessages() throws Exception { XMPPConnection connection = mock(XMPPConnection.class); ChatMessageSendingMessageHandler handler = new ChatMessageSendingMessageHandler(connection); handler.setBeanFactory(mock(BeanFactory.class)); @@ -92,6 +99,28 @@ public class ChatMessageSendingMessageHandlerTests { // in threaded conversation we need to look for existing chat verify(connection, times(1)).sendStanza(Mockito.argThat(new EqualSmackMessageWithThreadId())); + + reset(connection); + final String json = "{\"foo\": \"bar\"}"; + message = MessageBuilder.withPayload(new GcmPacketExtension(json)) + .setHeader(XmppHeaders.TO, "kermit@frog.com") + .build(); + handler.handleMessage(message); + + class EqualExtension extends ArgumentMatcher { + + @Override + public boolean matches(Object msg) { + org.jivesoftware.smack.packet.Message smackMessage = (org.jivesoftware.smack.packet.Message) msg; + boolean bodyMatches = smackMessage.getBody() == null; + boolean toMatches = smackMessage.getTo().equals("kermit@frog.com"); + GcmPacketExtension gcmPacketExtension = GcmPacketExtension.from(smackMessage); + boolean jsonMatches = gcmPacketExtension != null && gcmPacketExtension.getJson().equals(json); + return bodyMatches & toMatches & jsonMatches; + } + } + + verify(connection, times(1)).sendStanza(Mockito.argThat(new EqualExtension())); } @Test @@ -124,6 +153,39 @@ public class ChatMessageSendingMessageHandlerTests { verify(connection, times(1)).sendStanza(smackMessage); } + @Test + public void testExtensionProvider() throws Exception { + XMPPConnection connection = mock(XMPPConnection.class); + ChatMessageSendingMessageHandler handler = new ChatMessageSendingMessageHandler(connection); + GcmExtensionProvider extensionElementProvider = spy(new GcmExtensionProvider()); + handler.setExtensionProvider(extensionElementProvider); + handler.setBeanFactory(mock(BeanFactory.class)); + handler.afterPropertiesSet(); + + final String json = "{\"foo\": \"bar\"}"; + Message message = MessageBuilder.withPayload(" " + json + " ") + .setHeader(XmppHeaders.TO, "kermit@frog.com") + .build(); + + handler.handleMessage(message); + + ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(org.jivesoftware.smack.packet.Message.class); + + verify(connection).sendStanza(argumentCaptor.capture()); + + org.jivesoftware.smack.packet.Message smackMessage = argumentCaptor.getValue(); + + assertNull(smackMessage.getBody()); + assertEquals("kermit@frog.com", smackMessage.getTo()); + GcmPacketExtension gcmPacketExtension = GcmPacketExtension.from(smackMessage); + assertNotNull(gcmPacketExtension); + assertEquals(json, gcmPacketExtension.getJson()); + + verify(extensionElementProvider).from(eq(json)); + } + + @Test(expected = MessageHandlingException.class) public void validateFailureNoChatToUser() throws Exception { ChatMessageSendingMessageHandler handler = new ChatMessageSendingMessageHandler(mock(XMPPConnection.class)); diff --git a/src/reference/asciidoc/xmpp.adoc b/src/reference/asciidoc/xmpp.adoc index a0abbfbb97..f23a1170bb 100644 --- a/src/reference/asciidoc/xmpp.adoc +++ b/src/reference/asciidoc/xmpp.adoc @@ -136,7 +136,8 @@ Configuration support for the XMPP _Outbound Message Channel Adapter_ is provide xmpp-connection="testConnection"/> ---- -The adapter expects as its input - at a minimum - a payload of type `java.lang.String`, and a header value for `XmppHeaders.CHAT_TO` that specifies to which user the Message should be sent. +The adapter expects as its input - at a minimum - a payload of type `java.lang.String`, and a header value for +`XmppHeaders.CHAT_TO` that specifies to which user the Message should be sent. To create a message you might use the following Java code: [source,java] ---- @@ -155,6 +156,16 @@ Here is an example. ---- +Starting with _version 4.3_ the packet extension support has been added to the `ChatMessageSendingMessageHandler` +(``). +Alongside with the regular `String` and `org.jivesoftware.smack.packet.Message` `payload`, now you can send a message +with a `payload` as a `org.jivesoftware.smack.packet.ExtensionElement` which is populated to the +`org.jivesoftware.smack.packet.Message.addExtension()` instead of `setBody()`. +For the convenience an `extension-provider` option has been added for the `ChatMessageSendingMessageHandler` +to allow to inject `org.jivesoftware.smack.provider.ExtensionElementProvider`, which builds an `ExtensionElement` +against the `payload` at runtime. +For this case the payload must be `String` in JSON or XML format depending of the XEP protocol. + [[xmpp-presence]] === XMPP Presence