INT-3541: The XMPP Extension Support on Outbound
JIRA: https://jira.spring.io/browse/INT-3541 Address PR comments
This commit is contained in:
committed by
Gary Russell
parent
c97ac195a5
commit
b363bdb15e
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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]*)[^>]*>[^<]*</\\1>");
|
||||
|
||||
private volatile XmppHeaderMapper headerMapper = new DefaultXmppHeaderMapper();
|
||||
|
||||
private ExtensionElementProvider<? extends ExtensionElement> 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<? extends ExtensionElement> 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 = "<root>" + data + "</root>";
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -160,7 +160,21 @@
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="xmppOutboundAdapterType"/>
|
||||
<xsd:extension base="xmppOutboundAdapterType">
|
||||
<xsd:attribute name="extension-provider" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type
|
||||
type="org.jivesoftware.smack.provider.ExtensionElementProvider"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
Reference to org.jivesoftware.smack.provider.ExtensionElementProvider bean.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
|
||||
<int:channel id="outboundEventChannel"/>
|
||||
|
||||
<bean id="testExtensionProvider" class="org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider"/>
|
||||
|
||||
<int-xmpp:outbound-channel-adapter id="outboundEventAdapter"
|
||||
channel="outboundEventChannel"
|
||||
xmpp-connection="testConnection"
|
||||
mapped-request-headers="foo*, bar*"/>
|
||||
mapped-request-headers="foo*, bar*"
|
||||
extension-provider="testExtensionProvider"/>
|
||||
|
||||
<int:channel id="outboundPollingChannel">
|
||||
<int:queue/>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<org.jivesoftware.smack.packet.Message> {
|
||||
|
||||
@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(" <f foo='foo'>" + json + "</f> ")
|
||||
.setHeader(XmppHeaders.TO, "kermit@frog.com")
|
||||
.build();
|
||||
|
||||
handler.handleMessage(message);
|
||||
|
||||
ArgumentCaptor<org.jivesoftware.smack.packet.Message> 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));
|
||||
|
||||
@@ -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.
|
||||
</int-xmpp:header-enricher>
|
||||
----
|
||||
|
||||
Starting with _version 4.3_ the packet extension support has been added to the `ChatMessageSendingMessageHandler`
|
||||
(`<int-xmpp:outbound-channel-adapter>`).
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user