Skip to content

Commit b363bdb

Browse files
artembilangaryrussell
authored andcommitted
INT-3541: The XMPP Extension Support on Outbound
JIRA: https://jira.spring.io/browse/INT-3541 Address PR comments
1 parent c97ac19 commit b363bdb

File tree

7 files changed

+192
-26
lines changed

7 files changed

+192
-26
lines changed

spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParser.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2010 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,17 +17,38 @@
1717
package org.springframework.integration.xmpp.config;
1818

1919

20+
import org.w3c.dom.Element;
21+
22+
import org.springframework.beans.factory.config.RuntimeBeanReference;
23+
import org.springframework.beans.factory.support.AbstractBeanDefinition;
24+
import org.springframework.beans.factory.xml.ParserContext;
25+
import org.springframework.integration.xmpp.outbound.ChatMessageSendingMessageHandler;
26+
import org.springframework.util.StringUtils;
27+
2028
/**
2129
* Parser for the XMPP 'outbound-channel-adapter' element
2230
*
2331
* @author Oleg Zhurakousky
32+
* @author Artem Bilan
33+
*
2434
* @since 2.0
2535
*/
2636
public class ChatMessageOutboundChannelAdapterParser extends AbstractXmppOutboundChannelAdapterParser {
2737

2838
@Override
2939
protected String getHandlerClassName() {
30-
return "org.springframework.integration.xmpp.outbound.ChatMessageSendingMessageHandler";
40+
return ChatMessageSendingMessageHandler.class.getName();
41+
}
42+
43+
@Override
44+
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
45+
AbstractBeanDefinition beanDefinition = super.parseConsumer(element, parserContext);
46+
String extensionProvider = element.getAttribute("extension-provider");
47+
if (StringUtils.hasText(extensionProvider)) {
48+
beanDefinition.getPropertyValues()
49+
.addPropertyValue("extensionProvider", new RuntimeBeanReference(extensionProvider));
50+
}
51+
return beanDefinition;
3152
}
3253

3354
}

spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandler.java

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616

1717
package org.springframework.integration.xmpp.outbound;
1818

19+
import java.io.StringReader;
20+
import java.util.regex.Pattern;
21+
1922
import org.jivesoftware.smack.AbstractXMPPConnection;
2023
import org.jivesoftware.smack.XMPPConnection;
24+
import org.jivesoftware.smack.packet.ExtensionElement;
25+
import org.jivesoftware.smack.provider.ExtensionElementProvider;
26+
import org.jivesoftware.smack.util.PacketParserUtils;
27+
import org.xmlpull.v1.XmlPullParser;
2128

2229
import org.springframework.integration.xmpp.XmppHeaders;
2330
import org.springframework.integration.xmpp.core.AbstractXmppConnectionAwareMessageHandler;
@@ -40,8 +47,11 @@
4047
*/
4148
public class ChatMessageSendingMessageHandler extends AbstractXmppConnectionAwareMessageHandler {
4249

50+
private static final Pattern xmlPattern = Pattern.compile("<(\\S[^>\\s]*)[^>]*>[^<]*</\\1>");
51+
4352
private volatile XmppHeaderMapper headerMapper = new DefaultXmppHeaderMapper();
4453

54+
private ExtensionElementProvider<? extends ExtensionElement> extensionProvider;
4555

4656
public ChatMessageSendingMessageHandler() {
4757
super();
@@ -56,6 +66,17 @@ public void setHeaderMapper(XmppHeaderMapper headerMapper) {
5666
this.headerMapper = headerMapper;
5767
}
5868

69+
/**
70+
* Specify an {@link ExtensionElementProvider} to build an {@link ExtensionElement}
71+
* for the {@link org.jivesoftware.smack.packet.Message#addExtension(ExtensionElement)}
72+
* instead of {@code body}.
73+
* @param extensionProvider the {@link ExtensionElementProvider} to use.
74+
* @since 4.3
75+
*/
76+
public void setExtensionProvider(ExtensionElementProvider<? extends ExtensionElement> extensionProvider) {
77+
this.extensionProvider = extensionProvider;
78+
}
79+
5980
@Override
6081
public String getComponentType() {
6182
return "xmpp:outbound-channel-adapter";
@@ -64,26 +85,50 @@ public String getComponentType() {
6485
@Override
6586
protected void handleMessageInternal(Message<?> message) throws Exception {
6687
Assert.isTrue(this.initialized, getComponentName() + "#" + this.getComponentType() + " must be initialized");
67-
Object messageBody = message.getPayload();
88+
Object payload = message.getPayload();
6889
org.jivesoftware.smack.packet.Message xmppMessage = null;
69-
if (messageBody instanceof org.jivesoftware.smack.packet.Message) {
70-
xmppMessage = (org.jivesoftware.smack.packet.Message) messageBody;
90+
if (payload instanceof org.jivesoftware.smack.packet.Message) {
91+
xmppMessage = (org.jivesoftware.smack.packet.Message) payload;
7192
}
72-
else if (messageBody instanceof String) {
93+
else {
7394
String to = message.getHeaders().get(XmppHeaders.TO, String.class);
7495
Assert.state(StringUtils.hasText(to), "The '" + XmppHeaders.TO + "' header must not be null");
7596
xmppMessage = new org.jivesoftware.smack.packet.Message(to);
76-
if (this.headerMapper != null) {
77-
this.headerMapper.fromHeadersToRequest(message.getHeaders(), xmppMessage);
97+
98+
if (payload instanceof ExtensionElement) {
99+
xmppMessage.addExtension((ExtensionElement) payload);
100+
}
101+
else if (payload instanceof String) {
102+
if (this.extensionProvider != null) {
103+
String data = (String) payload;
104+
if (!xmlPattern.matcher(data.trim()).matches()) {
105+
// Since XMPP Extension parsers deal only with XML content,
106+
// add an arbitrary tag that is removed by the extension parser,
107+
// if the target content isn't XML.
108+
data = "<root>" + data + "</root>";
109+
}
110+
XmlPullParser xmlPullParser = PacketParserUtils.newXmppParser(new StringReader(data));
111+
xmlPullParser.next();
112+
ExtensionElement extension = this.extensionProvider.parse(xmlPullParser);
113+
xmppMessage.addExtension(extension);
114+
}
115+
else {
116+
xmppMessage.setBody((String) payload);
117+
}
118+
}
119+
else {
120+
throw new MessageHandlingException(message,
121+
"Only payloads of type java.lang.String, org.jivesoftware.smack.packet.Message " +
122+
"or org.jivesoftware.smack.packet.ExtensionElement " +
123+
"are supported. Received [" + payload.getClass().getName() +
124+
"]. Consider adding a Transformer prior to this adapter.");
78125
}
79-
xmppMessage.setBody((String) messageBody);
80126
}
81-
else {
82-
throw new MessageHandlingException(message,
83-
"Only payloads of type java.lang.String or org.jivesoftware.smack.packet.Message " +
84-
"are supported. Received [" + messageBody.getClass().getName() +
85-
"]. Consider adding a Transformer prior to this adapter.");
127+
128+
if (this.headerMapper != null) {
129+
this.headerMapper.fromHeadersToRequest(message.getHeaders(), xmppMessage);
86130
}
131+
87132
if (!this.xmppConnection.isConnected() && this.xmppConnection instanceof AbstractXMPPConnection) {
88133
((AbstractXMPPConnection) this.xmppConnection).connect();
89134
}

spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-4.3.xsd

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,21 @@
160160
</xsd:annotation>
161161
<xsd:complexType>
162162
<xsd:complexContent>
163-
<xsd:extension base="xmppOutboundAdapterType"/>
163+
<xsd:extension base="xmppOutboundAdapterType">
164+
<xsd:attribute name="extension-provider" type="xsd:string">
165+
<xsd:annotation>
166+
<xsd:appinfo>
167+
<tool:annotation kind="ref">
168+
<tool:expected-type
169+
type="org.jivesoftware.smack.provider.ExtensionElementProvider"/>
170+
</tool:annotation>
171+
</xsd:appinfo>
172+
<xsd:documentation>
173+
Reference to org.jivesoftware.smack.provider.ExtensionElementProvider bean.
174+
</xsd:documentation>
175+
</xsd:annotation>
176+
</xsd:attribute>
177+
</xsd:extension>
164178
</xsd:complexContent>
165179
</xsd:complexType>
166180
</xsd:element>

spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests-context.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
<int:channel id="outboundEventChannel"/>
1515

16+
<bean id="testExtensionProvider" class="org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider"/>
17+
1618
<int-xmpp:outbound-channel-adapter id="outboundEventAdapter"
1719
channel="outboundEventChannel"
1820
xmpp-connection="testConnection"
19-
mapped-request-headers="foo*, bar*"/>
21+
mapped-request-headers="foo*, bar*"
22+
extension-provider="testExtensionProvider"/>
2023

2124
<int:channel id="outboundPollingChannel">
2225
<int:queue/>

spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/ChatMessageOutboundChannelAdapterParserTests.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,12 +17,14 @@
1717
package org.springframework.integration.xmpp.config;
1818

1919
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.assertTrue;
2120
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertSame;
22+
import static org.junit.Assert.assertTrue;
2223
import static org.mockito.Mockito.times;
2324
import static org.mockito.Mockito.verify;
2425

2526
import org.jivesoftware.smack.XMPPConnection;
27+
import org.jivesoftware.smack.provider.ExtensionElementProvider;
2628
import org.jivesoftware.smackx.jiveproperties.JivePropertiesManager;
2729
import org.junit.Test;
2830
import org.junit.runner.RunWith;
@@ -32,21 +34,21 @@
3234

3335
import org.springframework.beans.factory.annotation.Autowired;
3436
import org.springframework.context.ApplicationContext;
35-
import org.springframework.integration.mapping.AbstractHeaderMapper;
36-
import org.springframework.messaging.Message;
37-
import org.springframework.messaging.MessageChannel;
3837
import org.springframework.integration.channel.QueueChannel;
39-
import org.springframework.messaging.MessageHandler;
40-
import org.springframework.messaging.SubscribableChannel;
4138
import org.springframework.integration.endpoint.EventDrivenConsumer;
4239
import org.springframework.integration.endpoint.PollingConsumer;
4340
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
44-
import org.springframework.messaging.support.GenericMessage;
41+
import org.springframework.integration.mapping.AbstractHeaderMapper;
4542
import org.springframework.integration.support.MessageBuilder;
4643
import org.springframework.integration.test.util.TestUtils;
4744
import org.springframework.integration.xmpp.XmppHeaders;
4845
import org.springframework.integration.xmpp.support.DefaultXmppHeaderMapper;
4946
import org.springframework.integration.xmpp.support.XmppHeaderMapper;
47+
import org.springframework.messaging.Message;
48+
import org.springframework.messaging.MessageChannel;
49+
import org.springframework.messaging.MessageHandler;
50+
import org.springframework.messaging.SubscribableChannel;
51+
import org.springframework.messaging.support.GenericMessage;
5052
import org.springframework.test.context.ContextConfiguration;
5153
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
5254

@@ -67,6 +69,9 @@ public class ChatMessageOutboundChannelAdapterParserTests {
6769
@Autowired
6870
private XmppHeaderMapper headerMapper;
6971

72+
@Autowired
73+
private ExtensionElementProvider<?> extensionElementProvider;
74+
7075
private static volatile int adviceCalled;
7176

7277
@Test
@@ -107,6 +112,11 @@ public void testEventConsumer() {
107112
assertFalse(requestHeaderMatcher.matchHeader("biz"));
108113
assertFalse(requestHeaderMatcher.matchHeader("else"));
109114
assertTrue(eventConsumer instanceof EventDrivenConsumer);
115+
116+
MessageHandler outboundEventAdapterHandle =
117+
context.getBean("outboundEventAdapter.handler", MessageHandler.class);
118+
assertSame(this.extensionElementProvider,
119+
TestUtils.getPropertyValue(outboundEventAdapterHandle, "extensionProvider"));
110120
}
111121

112122
@SuppressWarnings("rawtypes")

spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/outbound/ChatMessageSendingMessageHandlerTests.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@
1616

1717
package org.springframework.integration.xmpp.outbound;
1818

19+
import static org.junit.Assert.assertEquals;
1920
import static org.junit.Assert.assertNotNull;
21+
import static org.junit.Assert.assertNull;
22+
import static org.mockito.Matchers.eq;
2023
import static org.mockito.Mockito.mock;
2124
import static org.mockito.Mockito.reset;
25+
import static org.mockito.Mockito.spy;
2226
import static org.mockito.Mockito.times;
2327
import static org.mockito.Mockito.verify;
2428

2529
import org.jivesoftware.smack.XMPPConnection;
30+
import org.jivesoftware.smackx.gcm.packet.GcmPacketExtension;
31+
import org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider;
2632
import org.junit.Test;
33+
import org.mockito.ArgumentCaptor;
2734
import org.mockito.ArgumentMatcher;
2835
import org.mockito.Mockito;
2936

@@ -46,7 +53,7 @@ public class ChatMessageSendingMessageHandlerTests {
4653

4754

4855
@Test
49-
public void validateMessagePostAsString() throws Exception {
56+
public void testSendMessages() throws Exception {
5057
XMPPConnection connection = mock(XMPPConnection.class);
5158
ChatMessageSendingMessageHandler handler = new ChatMessageSendingMessageHandler(connection);
5259
handler.setBeanFactory(mock(BeanFactory.class));
@@ -92,6 +99,28 @@ public boolean matches(Object msg) {
9299

93100
// in threaded conversation we need to look for existing chat
94101
verify(connection, times(1)).sendStanza(Mockito.argThat(new EqualSmackMessageWithThreadId()));
102+
103+
reset(connection);
104+
final String json = "{\"foo\": \"bar\"}";
105+
message = MessageBuilder.withPayload(new GcmPacketExtension(json))
106+
.setHeader(XmppHeaders.TO, "[email protected]")
107+
.build();
108+
handler.handleMessage(message);
109+
110+
class EqualExtension extends ArgumentMatcher<org.jivesoftware.smack.packet.Message> {
111+
112+
@Override
113+
public boolean matches(Object msg) {
114+
org.jivesoftware.smack.packet.Message smackMessage = (org.jivesoftware.smack.packet.Message) msg;
115+
boolean bodyMatches = smackMessage.getBody() == null;
116+
boolean toMatches = smackMessage.getTo().equals("[email protected]");
117+
GcmPacketExtension gcmPacketExtension = GcmPacketExtension.from(smackMessage);
118+
boolean jsonMatches = gcmPacketExtension != null && gcmPacketExtension.getJson().equals(json);
119+
return bodyMatches & toMatches & jsonMatches;
120+
}
121+
}
122+
123+
verify(connection, times(1)).sendStanza(Mockito.argThat(new EqualExtension()));
95124
}
96125

97126
@Test
@@ -124,6 +153,39 @@ public void validateMessagePostAsSmackMessage() throws Exception {
124153
verify(connection, times(1)).sendStanza(smackMessage);
125154
}
126155

156+
@Test
157+
public void testExtensionProvider() throws Exception {
158+
XMPPConnection connection = mock(XMPPConnection.class);
159+
ChatMessageSendingMessageHandler handler = new ChatMessageSendingMessageHandler(connection);
160+
GcmExtensionProvider extensionElementProvider = spy(new GcmExtensionProvider());
161+
handler.setExtensionProvider(extensionElementProvider);
162+
handler.setBeanFactory(mock(BeanFactory.class));
163+
handler.afterPropertiesSet();
164+
165+
final String json = "{\"foo\": \"bar\"}";
166+
Message<?> message = MessageBuilder.withPayload(" <f foo='foo'>" + json + "</f> ")
167+
.setHeader(XmppHeaders.TO, "[email protected]")
168+
.build();
169+
170+
handler.handleMessage(message);
171+
172+
ArgumentCaptor<org.jivesoftware.smack.packet.Message> argumentCaptor =
173+
ArgumentCaptor.forClass(org.jivesoftware.smack.packet.Message.class);
174+
175+
verify(connection).sendStanza(argumentCaptor.capture());
176+
177+
org.jivesoftware.smack.packet.Message smackMessage = argumentCaptor.getValue();
178+
179+
assertNull(smackMessage.getBody());
180+
assertEquals("[email protected]", smackMessage.getTo());
181+
GcmPacketExtension gcmPacketExtension = GcmPacketExtension.from(smackMessage);
182+
assertNotNull(gcmPacketExtension);
183+
assertEquals(json, gcmPacketExtension.getJson());
184+
185+
verify(extensionElementProvider).from(eq(json));
186+
}
187+
188+
127189
@Test(expected = MessageHandlingException.class)
128190
public void validateFailureNoChatToUser() throws Exception {
129191
ChatMessageSendingMessageHandler handler = new ChatMessageSendingMessageHandler(mock(XMPPConnection.class));

src/reference/asciidoc/xmpp.adoc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ Configuration support for the XMPP _Outbound Message Channel Adapter_ is provide
136136
xmpp-connection="testConnection"/>
137137
----
138138

139-
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.
139+
The adapter expects as its input - at a minimum - a payload of type `java.lang.String`, and a header value for
140+
`XmppHeaders.CHAT_TO` that specifies to which user the Message should be sent.
140141
To create a message you might use the following Java code:
141142
[source,java]
142143
----
@@ -155,6 +156,16 @@ Here is an example.
155156
</int-xmpp:header-enricher>
156157
----
157158

159+
Starting with _version 4.3_ the packet extension support has been added to the `ChatMessageSendingMessageHandler`
160+
(`<int-xmpp:outbound-channel-adapter>`).
161+
Alongside with the regular `String` and `org.jivesoftware.smack.packet.Message` `payload`, now you can send a message
162+
with a `payload` as a `org.jivesoftware.smack.packet.ExtensionElement` which is populated to the
163+
`org.jivesoftware.smack.packet.Message.addExtension()` instead of `setBody()`.
164+
For the convenience an `extension-provider` option has been added for the `ChatMessageSendingMessageHandler`
165+
to allow to inject `org.jivesoftware.smack.provider.ExtensionElementProvider`, which builds an `ExtensionElement`
166+
against the `payload` at runtime.
167+
For this case the payload must be `String` in JSON or XML format depending of the XEP protocol.
168+
158169
[[xmpp-presence]]
159170
=== XMPP Presence
160171

0 commit comments

Comments
 (0)