diff --git a/spring-integration-xmpp/.classpath b/spring-integration-xmpp/.classpath new file mode 100644 index 0000000000..2daddec399 --- /dev/null +++ b/spring-integration-xmpp/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-integration-xmpp/.project b/spring-integration-xmpp/.project new file mode 100644 index 0000000000..3b38ec9900 --- /dev/null +++ b/spring-integration-xmpp/.project @@ -0,0 +1,29 @@ + + + spring-integration-xmpp + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.maven.ide.eclipse.maven2Nature + org.springframework.ide.eclipse.core.springnature + org.eclipse.jdt.core.javanature + + diff --git a/spring-integration-xmpp/.settings/org.eclipse.jdt.core.prefs b/spring-integration-xmpp/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..2d50f6f694 --- /dev/null +++ b/spring-integration-xmpp/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,3 @@ +#Sun Jun 06 10:22:56 EDT 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning diff --git a/spring-integration-xmpp/.settings/org.maven.ide.eclipse.prefs b/spring-integration-xmpp/.settings/org.maven.ide.eclipse.prefs new file mode 100644 index 0000000000..bf157b8d10 --- /dev/null +++ b/spring-integration-xmpp/.settings/org.maven.ide.eclipse.prefs @@ -0,0 +1,9 @@ +#Sun Jun 06 10:20:24 EDT 2010 +activeProfiles= +eclipse.preferences.version=1 +fullBuildGoals=process-test-resources +includeModules=false +resolveWorkspaceProjects=true +resourceFilterGoals=process-resources resources\:testResources +skipCompilerPlugin=true +version=1 diff --git a/spring-integration-xmpp/pom.xml b/spring-integration-xmpp/pom.xml new file mode 100644 index 0000000000..6d6b03b372 --- /dev/null +++ b/spring-integration-xmpp/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + org.springframework.integration + spring-integration-parent + 2.0.0.BUILD-SNAPSHOT + + org.springframework.integration + spring-integration-xmpp + jar + Spring Integration XMPP Support + + + jivesoftware + smack + 3.1.0 + + + jivesoftware + smackx + 3.1.0 + + + javax.activation + activation + 1.1.1 + true + + + cglib + cglib-nodep + ${cglib.version} + test + + + org.easymock + easymock + ${org.easymock.version} + test + + + org.easymock + easymockclassextension + ${org.easymock.version} + test + + + junit + junit + ${junit.version} + test + + + org.springframework + spring-context-support + ${org.springframework.version} + compile + + + org.springframework + spring-test + ${org.springframework.version} + test + + + org.springframework.integration + spring-integration-stream + ${project.version} + compile + + + org.springframework.integration + spring-integration-core + ${project.version} + compile + + + commons-lang + commons-lang + 2.5 + + + commons-io + commons-io + 1.4 + + + + + + com.springsource.bundlor + com.springsource.bundlor.maven + + + + diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/XmppConnectionFactory.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/XmppConnectionFactory.java new file mode 100644 index 0000000000..e546f31543 --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/XmppConnectionFactory.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.xmpp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.SASLAuthentication; +import org.jivesoftware.smack.XMPPConnection; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.util.StringUtils; + +/** + * This class configures an {@link org.jivesoftware.smack.XMPPConnection} object. This object is used for all scenarios to talk to a Smack server. + * + * @author Josh Long + * @author Mark Fisher + * @see {@link org.jivesoftware.smack.XMPPConnection} + * @since 2.0 + */ +public class XmppConnectionFactory extends AbstractFactoryBean { + // TODO provide a default subscription mode for this class: Roster.setSubscriptionMode(Roster.SubscriptionMode) + private static final Log logger = LogFactory.getLog(XmppConnectionFactory.class); + + private volatile String user; + + private volatile String password; + + private volatile String host; + + private volatile String serviceName; + + private volatile String resource; + + private volatile String saslMechanismSupported; + + private volatile int saslMechanismSupportedIndex; + + private volatile int port; + + private volatile String subscriptionMode; + + + private volatile boolean debug = false; + + public XmppConnectionFactory() { + } + + public XmppConnectionFactory(String user, String password, String host, String serviceName, String resource, String saslMechanismSupported, int saslMechanismSupportedIndex, int port) { + this.user = user; + this.password = password; + this.host = host; + this.serviceName = serviceName; + this.resource = resource; + this.saslMechanismSupported = saslMechanismSupported; + this.saslMechanismSupportedIndex = saslMechanismSupportedIndex; + this.port = port; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getSaslMechanismSupported() { + return saslMechanismSupported; + } + + public void setSaslMechanismSupported(String saslMechanismSupported) { + this.saslMechanismSupported = saslMechanismSupported; + } + + public int getSaslMechanismSupportedIndex() { + return saslMechanismSupportedIndex; + } + + public void setSaslMechanismSupportedIndex(int saslMechanismSupportedIndex) { + this.saslMechanismSupportedIndex = saslMechanismSupportedIndex; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public boolean isDebug() { + return debug; + } + + public void setDebug(boolean debug) { + XMPPConnection.DEBUG_ENABLED = debug; + this.debug = debug; + } + + + public void setSubscriptionMode(final String subscriptionMode) { + this.subscriptionMode = subscriptionMode; + } + + @Override + public Class getObjectType() { + return XMPPConnection.class; + } + + private XMPPConnection configureAndConnect(String usr, String pw, String host, int port, String serviceName, String resource, String saslMechanismSupported, int saslMechanismSupportedIndex) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("usr=%s, pw=%s, host=%s, port=%s, serviceName=%s, resource=%s, saslMechanismSupported=%s, saslMechanismSupportedIndex=%s", usr, pw, host, port, serviceName, + resource, saslMechanismSupported, saslMechanismSupportedIndex)); + } + + XMPPConnection.DEBUG_ENABLED = false; // default + + ConnectionConfiguration connectionConfiguration = new ConnectionConfiguration(host, port, serviceName); + XMPPConnection connection = new XMPPConnection(connectionConfiguration); + + try { + connection.connect(); + + // You have to put this code before you login + if (StringUtils.hasText(saslMechanismSupported)) { + SASLAuthentication.supportSASLMechanism(saslMechanismSupported, saslMechanismSupportedIndex); + } + + // You have to specify the resoure (e.g. "@host.com") at the end + if (StringUtils.hasText(resource)) { + connection.login(usr, pw, resource); + } else { + connection.login(usr, pw); + } + + if( StringUtils.hasText( this.subscriptionMode)) { + Roster.SubscriptionMode subscriptionMode = Roster.SubscriptionMode.valueOf( this.subscriptionMode) ; + connection.getRoster().setSubscriptionMode( subscriptionMode ); + } + + if (logger.isDebugEnabled()) { + logger.debug("authenticated? " + connection.isAuthenticated()); + } + } catch (Exception e) { + logger.warn("failed to establish XMPP connnection", e); + } + + return connection; + } + + + + @Override + protected XMPPConnection createInstance() throws Exception { + return this.configureAndConnect(this.getUser(), this.getPassword(), this.getHost(), this.getPort(), this.getServiceName(), this.getResource(), this.getSaslMechanismSupported(), + this.getSaslMechanismSupportedIndex()); + } +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/XmppHeaders.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/XmppHeaders.java new file mode 100644 index 0000000000..a7444c528a --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/XmppHeaders.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.xmpp; + +import org.springframework.integration.core.MessageHeaders; + + +/** + * + * Used as keys for {@link org.springframework.integration.core.Message} objects + * that handle XMPP events. + * + * @author Mario Gray + * @author Josh Long + * @since 2.0 + */ +public class XmppHeaders { + private static final String PREFIX = MessageHeaders.PREFIX + "xmpp_"; + public static final String CHAT = PREFIX + "chat_key"; + public static final String CHAT_TO_USER = PREFIX + "chat_to_user"; + public static final String CHAT_THREAD_ID = PREFIX + "thread_id"; + public static final String TYPE = PREFIX + "type"; + public static final String ROSTER_CHANGE_TYPE = PREFIX + "roster_change_type"; + public static final String PRESENCE = PREFIX + "presence"; + public static final String ROSTER = PREFIX + "roster"; + + public static final String PRESENCE_LANGUAGE = PRESENCE + "language" ; + public static final String PRESENCE_PRIORITY = PRESENCE + "priority" ; + public static final String PRESENCE_MODE = PRESENCE + "mode" ; + public static final String PRESENCE_TYPE = PRESENCE + "type" ; + public static final String PRESENCE_STATUS = PRESENCE + "status" ; + public static final String PRESENCE_FROM = PRESENCE+ "from"; + + +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/XmppNamespaceHandler.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/XmppNamespaceHandler.java new file mode 100644 index 0000000000..7600ba3df3 --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/config/XmppNamespaceHandler.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.config; + +import org.jivesoftware.smack.packet.Presence; +import org.springframework.integration.config.xml.HeaderEnricherParserSupport; +import org.springframework.integration.xmpp.XmppHeaders; +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.util.StringUtils; + +/** + * This class parses the schema for XMPP support. + * + * @author Josh Long + * @author Mark Fisher + * @since 2.0 + */ +public class XmppNamespaceHandler extends NamespaceHandlerSupport { + + private static final String PACKAGE_NAME = "org.springframework.integration.xmpp"; + + private static String[] attributes = new String[] { + "user", "password", "host", "service-name", "resource", + "sasl-mechanism-supported", "sasl-mechanism-supported-index", "port", "subscription-mode" + }; + + + public void init() { + // connection + registerBeanDefinitionParser("xmpp-connection", new XmppConnectionParser()); + + // send/receive messages + registerBeanDefinitionParser("message-inbound-channel-adapter", new XmppMessageInboundEndpointParser()); + registerBeanDefinitionParser("message-outbound-channel-adapter", new XmppMessageOutboundEndpointParser()); + + // presence + registerBeanDefinitionParser("roster-event-inbound-channel-adapter", new XmppRosterEventInboundEndpointParser()); + registerBeanDefinitionParser("roster-event-outbound-channel-adapter", new XmppRosterEventOutboundEndpointParser()); + + registerBeanDefinitionParser("header-enricher", new XmppHeaderEnricherParser()); + } + + + private static void configureXMPPConnection(Element element, BeanDefinitionBuilder builder, ParserContext parserContext) { + String ref = element.getAttribute("xmpp-connection"); + if (StringUtils.hasText(ref)) { + builder.addPropertyReference("xmppConnection", ref); + } + else { + for (String attribute : attributes) { + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, attribute); + } + } + } + + + // connection management + private static class XmppConnectionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return PACKAGE_NAME + ".XmppConnectionFactory"; + } + + @Override + protected boolean shouldGenerateIdAsFallback() { + return true; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + configureXMPPConnection(element, builder, parserContext); + } + } + + // messages + private static class XmppMessageOutboundEndpointParser extends AbstractOutboundChannelAdapterParser { + + @Override + protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + PACKAGE_NAME + ".messages.XmppMessageSendingMessageHandler"); + configureXMPPConnection(element, builder, parserContext); + return builder.getBeanDefinition(); + } + } + + private static class XmppMessageInboundEndpointParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return PACKAGE_NAME + ".messages.XmppMessageDrivenEndpoint"; + } + + @Override + protected boolean shouldGenerateIdAsFallback() { + return true; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + configureXMPPConnection(element, builder, parserContext); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "channel", "requestChannel"); + } + } + + private static class XmppRosterEventOutboundEndpointParser extends AbstractOutboundChannelAdapterParser { + + @Override + protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + PACKAGE_NAME + ".presence.XmppRosterEventMessageSendingHandler"); + configureXMPPConnection(element, builder, parserContext); + return builder.getBeanDefinition(); + } + } + + private static class XmppRosterEventInboundEndpointParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return PACKAGE_NAME + ".presence.XmppRosterEventMessageDrivenEndpoint"; + } + + @Override + protected boolean shouldGenerateIdAsFallback() { + return true; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + configureXMPPConnection(element, builder, parserContext); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "channel", "requestChannel"); + } + } + + private static class XmppHeaderEnricherParser extends HeaderEnricherParserSupport { + + public XmppHeaderEnricherParser() { + + // chat headers + this.addElementToHeaderMapping("message-to", XmppHeaders.CHAT_TO_USER); + this.addElementToHeaderMapping("message-thread-id", XmppHeaders.CHAT_THREAD_ID); + + // presence headers + this.addElementToHeaderMapping("presence-mode", XmppHeaders.PRESENCE_MODE, Presence.Mode.class); + this.addElementToHeaderMapping("presence-type" , XmppHeaders.PRESENCE_TYPE, Presence.Type.class ); + this.addElementToHeaderMapping("presence-from", XmppHeaders.PRESENCE_FROM); + this.addElementToHeaderMapping("presence-status" , XmppHeaders.PRESENCE_STATUS); + this.addElementToHeaderMapping("presence-priority" , XmppHeaders.PRESENCE_PRIORITY, Integer.class); + } + } + +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/messages/XmppMessageDrivenEndpoint.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/messages/XmppMessageDrivenEndpoint.java new file mode 100644 index 0000000000..4a85b82979 --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/messages/XmppMessageDrivenEndpoint.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.jivesoftware.smack.Chat; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; + +import org.springframework.context.Lifecycle; +import org.springframework.integration.channel.MessageChannelTemplate; +import org.springframework.integration.core.MessageChannel; +import org.springframework.integration.endpoint.AbstractEndpoint; +import org.springframework.integration.message.MessageBuilder; +import org.springframework.integration.xmpp.XmppHeaders; + +/** + * This component logs in as a user and forwards any messages to that + * user on to downstream components. The component is an endpoint that has its + * own lifecycle and does not need any + * {@link org.springframework.integration.endpoint.AbstractPollingEndpoint.Poller} + * to work. It takes any message from a given XMPP session (as established by + * the current {@link org.jivesoftware.smack.XMPPConnection}) and forwards the + * {@link org.jivesoftware.smack.packet.Message} as the payload of the Spring + * Integration {@link org.springframework.integration.core.Message}. The + * {@link org.jivesoftware.smack.Chat} instance that's used is passed along as a + * header (under {@link org.springframework.integration.xmpp.XmppHeaders#CHAT}). + * Additionally, the {@link org.jivesoftware.smack.packet.Message.Type} is + * passed along under the header + * {@link org.springframework.integration.xmpp.XmppHeaders#TYPE}. Both of these + * pieces of metadata can be obtained directly from the payload, if required. + * They are here as a convenience. + *

+ * Note: the {@link org.jivesoftware.smack.ChatManager} + * maintains a Map<String, Chat> for threads and users, where the threadID + * ({@link String}) is the key or the userID {@link String} is the key. This + * {@link java.util.Map} is a Smack-specific implementation called + * {@link org.jivesoftware.smack.util.collections.ReferenceMap} that removes + * key/values as references are dereferenced. Take care to enable this garbage + * collection, taking what you need from the payload and the headers and + * discarding as soon as possible. + * + * @author Josh Long + * @author Mark Fisher + * @see {@link org.jivesoftware.smack.ChatManager} the ChatManager class that + * keeps watch over all Chats between the client and any other + * participants. + * @see {@link org.springframework.integration.channel.MessageChannelTemplate} + * handles all interesing operations on any Spring Integration channels. + * @see {@link org.jivesoftware.smack.XMPPConnection} the XMPPConnection (as + * created by + * {@link org.springframework.integration.xmpp.XmppConnectionFactory} + */ +public class XmppMessageDrivenEndpoint extends AbstractEndpoint implements Lifecycle { + + private static final Log logger = LogFactory.getLog(XmppMessageDrivenEndpoint.class); + + private final MessageChannelTemplate channelTemplate = new MessageChannelTemplate(); + + private volatile MessageChannel requestChannel; + + private volatile XMPPConnection xmppConnection; + + private volatile boolean extractPayload = true; + + /** + * This will be injected or configured via a xmpp-connection-factory element. + * + * @param xmppConnection + */ + public void setXmppConnection(final XMPPConnection xmppConnection) { + this.xmppConnection = xmppConnection; + } + + /** + * @param requestChannel the channel on which the inbound message should be sent + */ + public void setRequestChannel(final MessageChannel requestChannel) { + this.channelTemplate.setDefaultChannel(requestChannel); + this.requestChannel = requestChannel; + } + + + + /** + * 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. + */ + public void setExtractPayload(boolean extractPayload) { + this.extractPayload = extractPayload; + } + + @Override + protected void doStart() { + logger.debug("start: " + xmppConnection.isConnected() + ":" + xmppConnection.isAuthenticated()); + } + + @Override + protected void doStop() { + if (xmppConnection.isConnected()) { + logger.debug("shutting down " + XmppMessageDrivenEndpoint.class.getName() + "."); + xmppConnection.disconnect(); + } + } + + @Override + protected void onInit() throws Exception { + channelTemplate.afterPropertiesSet(); + xmppConnection.addPacketListener(new PacketListener() { + public void processPacket(final Packet packet) { + org.jivesoftware.smack.packet.Message message = (org.jivesoftware.smack.packet.Message) packet; + forwardXmppMessage(xmppConnection.getChatManager().getThreadChat(message.getThread()), message); + } + }, null); + } + + private void forwardXmppMessage(Chat chat, Message xmppMessage) { + Object payload = (this.extractPayload ? xmppMessage.getBody() : xmppMessage); + MessageBuilder messageBuilder = MessageBuilder.withPayload(payload) + .setHeader(XmppHeaders.TYPE, xmppMessage.getType()) + .setHeader(XmppHeaders.CHAT, chat); + channelTemplate.send(messageBuilder.build(), requestChannel); + } + +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/messages/XmppMessageSendingMessageHandler.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/messages/XmppMessageSendingMessageHandler.java new file mode 100644 index 0000000000..34e8d6eead --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/messages/XmppMessageSendingMessageHandler.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.Chat; +import org.jivesoftware.smack.XMPPConnection; +import org.springframework.context.Lifecycle; +import org.springframework.integration.message.MessageHandler; +import org.springframework.integration.xmpp.XmppHeaders; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Josh Long + * @author Mario Gray + * @since 2.0 + */ +public class XmppMessageSendingMessageHandler implements MessageHandler, Lifecycle { + + private static final Log logger = LogFactory.getLog(XmppMessageSendingMessageHandler.class); + + private volatile boolean running; + private volatile XMPPConnection xmppConnection; + + public void setXmppConnection(final XMPPConnection xmppConnection) { + this.xmppConnection = xmppConnection; + } + + public void handleMessage(final org.springframework.integration.core.Message message) { + try { + // pre-reqs: user to send, string to send as msg body + String messageBody = null; + String destinationUser = null; + Object payload = message.getPayload(); + if (payload instanceof String) { + messageBody = (String) payload; + } + destinationUser = (String) message.getHeaders().get(XmppHeaders.CHAT_TO_USER); + Assert.state(StringUtils.hasText(destinationUser), "the destination user must not be null"); + Assert.state(StringUtils.hasText(messageBody), "the message body must not be null"); + String threadId = (String) message.getHeaders().get(XmppHeaders.CHAT_THREAD_ID); + Chat chat = getOrCreateChatWithParticipant(destinationUser, threadId); + if (chat != null) { + chat.sendMessage(messageBody); + } + } + catch (Exception e) { + logger.debug("failed to send XMPP message", e); + } + } + + public boolean isRunning() { + return this.running; + } + + public void start() { + this.running = true; + } + + public void stop() { + this.running = false; + if (xmppConnection.isConnected()) { + if (logger.isInfoEnabled()) { + logger.info("shutting down XMPP connection"); + } + xmppConnection.disconnect(); + } + } + + private Chat getOrCreateChatWithParticipant(String userId, String thread) { + Chat chat = null; + if (!StringUtils.hasText(thread)) { + chat = xmppConnection.getChatManager().createChat(userId, null); + } + else { + chat = xmppConnection.getChatManager().getThreadChat(thread); + if (chat == null) { + chat = xmppConnection.getChatManager().createChat(userId, thread, null); + } + } + return chat; + } + +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppPresenceMessageMapper.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppPresenceMessageMapper.java new file mode 100644 index 0000000000..0523a2f772 --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppPresenceMessageMapper.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.xmpp.presence; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.jivesoftware.smack.packet.Presence; + +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; +import org.springframework.integration.message.InboundMessageMapper; +import org.springframework.integration.message.MessageBuilder; +import org.springframework.integration.message.OutboundMessageMapper; +import org.springframework.integration.xmpp.XmppHeaders; + +import org.springframework.util.StringUtils; + + +/** + * Implementation of the strategy interface {@link org.springframework.integration.message.OutboundMessageMapper}. This is the hook that lets the adapter receive various payloads from + * components inside Spring Integration and forward them correctly as {@link org.jivesoftware.smack.packet.Presence} instances. + * + * @author Josh Long + * @since 2.0 + */ +public class XmppPresenceMessageMapper implements OutboundMessageMapper, + InboundMessageMapper { + + private static final Log logger = LogFactory.getLog(XmppPresenceMessageMapper.class); + + /** + * Returns a {@link org.springframework.integration.core.Message} with payload {@link org.jivesoftware.smack.packet.Presence} + * + * @param presence the presence object that can be used to present the priority, status, mode, and type of a given roster entry. This will be decomposed into a series of headers, as well as a payload + * @return the Message + * @throws Exception thrown if conversion should fail + */ + public Message toMessage(Presence presence) throws Exception { + MessageBuilder presenceMessageBuilder = MessageBuilder.withPayload(presence); + presenceMessageBuilder.setHeader(XmppHeaders.PRESENCE_PRIORITY, presence.getPriority()); + presenceMessageBuilder.setHeader(XmppHeaders.PRESENCE_STATUS, presence.getStatus()); + presenceMessageBuilder.setHeader(XmppHeaders.PRESENCE_MODE, presence.getMode()); + presenceMessageBuilder.setHeader(XmppHeaders.PRESENCE_TYPE, presence.getType()); + presenceMessageBuilder.setHeader(XmppHeaders.PRESENCE_FROM, presence.getFrom()); + + return presenceMessageBuilder.build(); + } + + /** + * Builds a {@link org.jivesoftware.smack.packet.Presence} object from the inbound Message headers, if possible. + * + * @param message the Message whose headers and payload willl b + * @return the presence object as constructed from the {@link org.springframework.integration.core.Message} object + * @throws Exception + */ + public Presence fromMessage(Message message) throws Exception { + MessageHeaders messageHeaders = message.getHeaders(); + + Integer priority = (Integer) messageHeaders.get(XmppHeaders.PRESENCE_PRIORITY); + String status = (String) messageHeaders.get(XmppHeaders.PRESENCE_STATUS); + String language = (String) messageHeaders.get(XmppHeaders.PRESENCE_LANGUAGE); + String from = (String) messageHeaders.get(XmppHeaders.PRESENCE_FROM); + + // trickery afoot + Object modeObj = messageHeaders.get(XmppHeaders.PRESENCE_MODE); + Presence.Mode mode = null; + + Object typeObj = messageHeaders.get(XmppHeaders.PRESENCE_TYPE); + Presence.Type type = null; + + if (typeObj instanceof String) { + try { + type = Presence.Type.valueOf((String) typeObj); + } catch (Throwable th) { + logger.debug("couldn't convert type header into an object of type Presence.Type"); + } + } else if (modeObj instanceof Presence.Type) { + type = (Presence.Type) typeObj; + } + + if (modeObj instanceof String) { + try { + mode = Presence.Mode.valueOf((String) modeObj); + } catch (Throwable th) { + logger.debug("couldn't convert mode header into an object of type Presence.Mode "); + } + } else if (modeObj instanceof Presence.Mode) { + mode = (Presence.Mode) modeObj; + } + + Object payload = message.getPayload(); + + if (null != payload) { + if (payload instanceof Presence) { + return (Presence) payload; + } + + if (payload instanceof Presence.Type) { + type = (Presence.Type) payload; + } + } + + return this.factoryPresence(from, status, priority, type, mode, language); + } + + private Presence factoryPresence(String from, String status, Integer priority, + Presence.Type type, Presence.Mode mode, String language) { + if (null == type) { + type = Presence.Type.available; + } + + Presence presence = new Presence(type); + + if (null != priority) { + presence.setPriority(priority); + } + + if (StringUtils.hasText(status)) { + presence.setStatus(status); + } + + if (StringUtils.hasText(from)) { + presence.setFrom(from); + } + + if (null != mode) { + presence.setMode(mode); + } + + if (StringUtils.hasText(language)) { + presence.setLanguage(language); + } + + return presence; + } +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppRosterEventMessageDrivenEndpoint.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppRosterEventMessageDrivenEndpoint.java new file mode 100644 index 0000000000..b6833f9e40 --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppRosterEventMessageDrivenEndpoint.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.xmpp.presence; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.jivesoftware.smack.RosterListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Presence; + +import org.springframework.context.Lifecycle; + +import org.springframework.integration.channel.MessageChannelTemplate; +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageChannel; +import org.springframework.integration.endpoint.AbstractEndpoint; +import org.springframework.integration.message.InboundMessageMapper; + +import java.util.Collection; + + +/** + * Describes an endpoint that is able to login as usual with a {@link org.springframework.integration.xmpp.XmppConnectionFactory} and then emit {@link org.springframework.integration.core.Message}s when a particular event happens to the logged in users {@link org.jivesoftware.smack.Roster}. We try + * and generically propagate these events. In practical terms, there are a few events worth being notified of:

+ * + * @author Josh Long + * @since 2.0 + */ +public class XmppRosterEventMessageDrivenEndpoint extends AbstractEndpoint implements Lifecycle { + private static final Log logger = LogFactory.getLog(XmppRosterEventMessageDrivenEndpoint.class); + private volatile MessageChannel requestChannel; + private volatile XMPPConnection xmppConnection; + private InboundMessageMapper messageMapper; + private final MessageChannelTemplate channelTemplate = new MessageChannelTemplate(); + + /** + * This will be injected or configured via a xmpp-connection-factory element. + * + * @param xmppConnection + */ + public void setXmppConnection(final XMPPConnection xmppConnection) { + this.xmppConnection = xmppConnection; + } + + /** + * @param requestChannel the channel on which the inbound message should be sent + */ + public void setRequestChannel(final MessageChannel requestChannel) { + this.channelTemplate.setDefaultChannel(requestChannel); + this.requestChannel = requestChannel; + } + + @Override + protected void doStart() { + logger.debug("start: " + xmppConnection.isConnected() + ":" + + xmppConnection.isAuthenticated()); + } + + @Override + protected void doStop() { + if (this.xmppConnection.isConnected()) { + logger.debug("shutting down " + XmppRosterEventMessageDrivenEndpoint.class.getName() + + "."); + this.xmppConnection.disconnect(); + } + } + + @Override + protected void onInit() throws Exception { + if (null == this.messageMapper) { + this.messageMapper = new XmppPresenceMessageMapper(); + } + + this.channelTemplate.afterPropertiesSet(); + this.xmppConnection.getRoster().addRosterListener(new EventForwardingRosterListener()); + } + + /** + * Called whenever an event happesn related to the {@link org.jivesoftware.smack.Roster} + * + * @param presence the {@link org.jivesoftware.smack.packet.Presence} object representing the new state (optional) + */ + protected void forwardRosterEventMessage(Presence presence) { + try { + Message msg = this.messageMapper.toMessage(presence); + channelTemplate.send(msg, requestChannel); + } catch (Exception e) { + logger.error("Failed to map packet to message ", e); + } + } + + public void setMessageMapper(InboundMessageMapper messageMapper) { + this.messageMapper = messageMapper; + } + + /** + * Subscribes to a given {@link org.jivesoftware.smack.Roster}s events and forwards them to components on the bus. + */ + class EventForwardingRosterListener implements RosterListener { + public void entriesAdded(final Collection entries) { + logger.debug("entries added: " + StringUtils.join(entries.iterator(), ",")); + } + + public void entriesUpdated(final Collection entries) { + logger.debug("entries updated: " + StringUtils.join(entries.iterator(), ",")); + } + + public void entriesDeleted(final Collection entries) { + logger.debug("entries deleted: " + StringUtils.join(entries.iterator(), ",")); + } + + public void presenceChanged(final Presence presence) { + logger.debug("presence changed: " + ToStringBuilder.reflectionToString(presence)); + forwardRosterEventMessage(presence); + } + } +} diff --git a/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppRosterEventMessageSendingHandler.java b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppRosterEventMessageSendingHandler.java new file mode 100644 index 0000000000..5b2f5d0849 --- /dev/null +++ b/spring-integration-xmpp/src/main/java/org/springframework/integration/xmpp/presence/XmppRosterEventMessageSendingHandler.java @@ -0,0 +1,74 @@ +package org.springframework.integration.xmpp.presence; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Presence; + +import org.springframework.context.Lifecycle; + +import org.springframework.integration.core.Message; +import org.springframework.integration.message.*; + + +/** + * This class will facilitate publishing updated presence values for a given connection. This change happens on the {@link org.jivesoftware.smack.Roster#subscriptionMode} property. + * + * @author Josh Long + * @see {@link org.jivesoftware.smack.packet.Presence.Mode} the mode (i.e.: {@link org.jivesoftware.smack.packet.Presence.Mode#away}) + * @see {@link org.jivesoftware.smack.packet.Presence.Type} the type (i.e.: {@link org.jivesoftware.smack.packet.Presence.Type#available} ) + * @since 2.0 + */ +public class XmppRosterEventMessageSendingHandler implements MessageHandler, Lifecycle { + private static final Log logger = LogFactory.getLog(XmppRosterEventMessageDrivenEndpoint.class); + private volatile boolean running; + private OutboundMessageMapper messageMapper; + private volatile XMPPConnection xmppConnection; + + public void setXmppConnection(final XMPPConnection xmppConnection) { + this.xmppConnection = xmppConnection; + } + + public void handleMessage(final Message message) + throws MessageRejectedException, MessageHandlingException, MessageDeliveryException { + try { + Presence presence = this.messageMapper.fromMessage(message); + this.xmppConnection.sendPacket(presence); + } catch (Exception e) { + logger.error("Failed to map packet to message ", e); + } + } + + public boolean isRunning() { + return this.running; + } + + public void start() { + if (null == this.messageMapper) { + this.messageMapper = new XmppPresenceMessageMapper(); + } + + this.running = true; + } + + public void stop() { + this.running = false; + + if (xmppConnection.isConnected()) { + if (logger.isInfoEnabled()) { + logger.info("shutting down XMPP connection"); + } + + xmppConnection.disconnect(); + } + } + + /** + * the MessageMapper is responsible for converting outbound Messages into status updates of type {@link org.jivesoftware.smack.packet.Presence} + * @param messageMapper + */ + public void setMessageMapper(OutboundMessageMapper messageMapper) { + this.messageMapper = messageMapper; + } +} diff --git a/spring-integration-xmpp/src/main/resources/META-INF/spring.handlers b/spring-integration-xmpp/src/main/resources/META-INF/spring.handlers new file mode 100644 index 0000000000..740801b09c --- /dev/null +++ b/spring-integration-xmpp/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.springframework.org/schema/integration/xmpp=org.springframework.integration.xmpp.config.XmppNamespaceHandler diff --git a/spring-integration-xmpp/src/main/resources/META-INF/spring.schemas b/spring-integration-xmpp/src/main/resources/META-INF/spring.schemas new file mode 100644 index 0000000000..157fb68c23 --- /dev/null +++ b/spring-integration-xmpp/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/integration/xmpp/spring-integration-xmpp-2.0.xsd=org/springframework/integration/xmpp/config/spring-integration-xmpp-2.0.xsd +http\://www.springframework.org/schema/integration/xmpp/spring-integration-xmpp.xsd=org/springframework/integration/xmpp/config/spring-integration-xmpp-2.0.xsd diff --git a/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-2.0.xsd b/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-2.0.xsd new file mode 100644 index 0000000000..6d246ffc50 --- /dev/null +++ b/spring-integration-xmpp/src/main/resources/org/springframework/integration/xmpp/config/spring-integration-xmpp-2.0.xsd @@ -0,0 +1,259 @@ + + + + + + + + + + + + Configures an XMPP connection that can in turn be referenced by other components + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Configures an endpoint that will publish an updated {@link org.jivesoftware.smack.packet.Presence} state on your {@link XMPPConnection } object. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Configures an endpoint that will forward roster based events (entry managment and presence state changes) to components in the bus. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Configures an endpoint that will receive messages sent to a given account and then forward those messages to components in the bus. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Component that sends messages to an entry in the roster on behalf of components in the bus. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Defines a Transformer for adding XMPP headers. + + + + + + + + + + + + + + + + + + + + Specify the default boolean value for whether to overwrite existing header values. This will only take effect for + sub-elements that do not provide their own 'overwrite' attribute. If the 'default-overwrite' attribute is not + provided, then the specified header values will NOT overwrite any existing ones with the same header names. + + + + + + + + + + + + + + + + + + + Boolean value to indicate whether this header value should overwrite an existing header value for the same name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/XmppHeaderEnricherParserTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/XmppHeaderEnricherParserTests.java new file mode 100644 index 0000000000..1e44d90e04 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/config/XmppHeaderEnricherParserTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.xmpp.config; + +import org.junit.Test; + +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Value; + +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.channel.MessageChannelTemplate; +import org.springframework.integration.core.Message; +import org.springframework.integration.message.*; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + + +/** + * @author Mark Fisher + * @author Josh Long + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class XmppHeaderEnricherParserTests { + + @Value("#{input}") + private DirectChannel input; + + @Value("#{output}") + private DirectChannel output; + + @Test + public void to() { + MessageChannelTemplate messageChannelTemplate = new MessageChannelTemplate(); + + output.subscribe(new MessageHandler() { + public void handleMessage(Message message) + throws MessageRejectedException, MessageHandlingException, + MessageDeliveryException { + for (String h : message.getHeaders().keySet()) + System.out.println(String.format("%s=%s (class: %s)", h, message.getHeaders().get(h), message.getHeaders().get(h).getClass().toString())); + } + }); + messageChannelTemplate.send(MessageBuilder.withPayload("foo").build(), input); + } +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/ConsoleChatTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/ConsoleChatTests.java new file mode 100644 index 0000000000..ca8482af56 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/ConsoleChatTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Mark Fisher + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class ConsoleChatTests { + + @Test + public void run() throws Exception { + Thread.sleep( 10 * 1000 ); + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/InboundXmppEndpointTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/InboundXmppEndpointTests.java new file mode 100644 index 0000000000..4f7b5b241f --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/InboundXmppEndpointTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Josh Long + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class InboundXmppEndpointTests { + + @Test + public void run() throws Exception { + Thread.sleep( 10 * 1000 ); + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/OutboundXmppEndpointTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/OutboundXmppEndpointTests.java new file mode 100644 index 0000000000..0d846f1148 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/OutboundXmppEndpointTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Testing support for sending messages. + * + * @author Josh Long + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class OutboundXmppEndpointTests { + + @Test + public void run() throws Exception { + Thread.sleep(10 * 1000); + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/XmppMessageConsumer.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/XmppMessageConsumer.java new file mode 100644 index 0000000000..d2a59d9ac8 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/XmppMessageConsumer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.jivesoftware.smack.packet.Message; + +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.stereotype.Component; + +/** + * Handle display of incoming XMPP messages to this user. + * + * @author Josh Long + * @author Mark Fisher + * @since 2.0 + */ +@Component +public class XmppMessageConsumer { + + @ServiceActivator + public void consume(Object input) throws Throwable { + String text = null; + if (input instanceof Message) { + text = ((Message) input).getBody(); + } + else if (input instanceof String) { + text = (String) input; + } + else { + throw new IllegalArgumentException( + "expected either a Smack Message or a String, but received: " + input); + } + System.out.println("================================================================================"); + System.out.println("message: " + text); + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/XmppMessageProducer.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/XmppMessageProducer.java new file mode 100644 index 0000000000..26c82966cf --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/messages/XmppMessageProducer.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.xmpp.messages; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.integration.core.Message; +import org.springframework.integration.message.MessageBuilder; +import org.springframework.integration.message.MessageSource; +import org.springframework.integration.xmpp.XmppHeaders; + +import java.util.Date; + +/** + * Generates XMPP messages every 2s and forwards them on a channel which in turn publishes the message through XMPP. + * + * @author Josh Long + * @since 2.0 + */ +public class XmppMessageProducer implements MessageSource { + + private static final Log logger = LogFactory.getLog(XmppMessageProducer.class); + + private volatile int counter; + private String recipient; + + public void setRecipient(final String recipient) { + this.recipient = recipient; + } + + public Message receive() { + try { + if (counter > 10) { + logger.debug("return null"); + return null; + } + counter += 1; + Thread.sleep(1000 * 2); + + String msg = "the current time is " + new Date(); + + logger.info("sending message to recipient " + recipient); + + return MessageBuilder.withPayload(msg).setHeader(XmppHeaders.CHAT_TO_USER, recipient).build(); + } + catch (InterruptedException e) { + logger.debug("exception thrown when trying to receive a message", e); + } + return null; + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/InboundXmppRosterEventsEndpointTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/InboundXmppRosterEventsEndpointTests.java new file mode 100644 index 0000000000..51f741a25b --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/InboundXmppRosterEventsEndpointTests.java @@ -0,0 +1,24 @@ +package org.springframework.integration.xmpp.presence; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * this class demonstrates that when I launch this and then manipulate the status of the + * user using Pidgin, the updated state is immediately delivered to the Spring Integration bus. + * + * @author Josh Long + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class InboundXmppRosterEventsEndpointTests { + + @Test + public void run() throws Exception { + Thread.sleep( 60 * 1000); + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/OutboundXmppRosterEventsEndpointTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/OutboundXmppRosterEventsEndpointTests.java new file mode 100644 index 0000000000..4b42f0e8d6 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/OutboundXmppRosterEventsEndpointTests.java @@ -0,0 +1,23 @@ +package org.springframework.integration.xmpp.presence; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * + * Tests {@link XmppRosterEventMessageSendingHandler} to ensure that we are able to publish status. + * + * + * @author Josh Long + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class OutboundXmppRosterEventsEndpointTests { + @Test + public void testOutbound() throws Throwable { + Thread.sleep( 60 * 1000); + } +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/PresenceMessageComboTests.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/PresenceMessageComboTests.java new file mode 100644 index 0000000000..46f3a5d6e9 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/PresenceMessageComboTests.java @@ -0,0 +1,28 @@ +package org.springframework.integration.xmpp.presence; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * + * This class will demonstrate using both inbound adapter types in a 1-2 punch of: + *
    + *
  • notifying the bus of a user's sudden online availability using <xmpp:roster-event-inbound-channel-adapter>
  • + *
  • sending that user a message using the <xmpp:outbound-message-channel-adapter />
  • + *
+ * + * @author Josh Long + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class PresenceMessageComboTests { + + @Test + public void run () throws Throwable { + Thread.sleep( 60 * 1000); + } + +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/XmppRosterEventConsumer.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/XmppRosterEventConsumer.java new file mode 100644 index 0000000000..96fd1b52db --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/XmppRosterEventConsumer.java @@ -0,0 +1,29 @@ +package org.springframework.integration.xmpp.presence; + +import org.apache.commons.lang.StringUtils; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; +import org.springframework.integration.xmpp.XmppHeaders; + +import java.util.Collection; + +/** + * + * This class reacts to changes in {@link org.jivesoftware.smack.packet.Presence} objects for a given account. + * + * @author Josh Long + * @since 2.0 + */ +public class XmppRosterEventConsumer { + + @ServiceActivator + public void presenceChanged ( Message presenceEventMsg ) throws Exception { + System.out.println(StringUtils.repeat( "-" , 100)); + String whosePresence = (String)presenceEventMsg.getHeaders().get( XmppHeaders.PRESENCE_FROM); + System.out.println( "entries affected: " + whosePresence); + MessageHeaders messageHeaders = presenceEventMsg.getHeaders(); + for( String h : messageHeaders.keySet() ) + System.out.println( String.format( "%s = %s", h, messageHeaders.get(h))); + } +} diff --git a/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/XmppRosterEventProducer.java b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/XmppRosterEventProducer.java new file mode 100644 index 0000000000..e398546ba5 --- /dev/null +++ b/spring-integration-xmpp/src/test/java/org/springframework/integration/xmpp/presence/XmppRosterEventProducer.java @@ -0,0 +1,31 @@ +package org.springframework.integration.xmpp.presence; + +import org.apache.commons.lang.StringUtils; + +import org.jivesoftware.smack.packet.Presence; + +import org.springframework.integration.core.Message; +import org.springframework.integration.message.*; +import org.springframework.integration.xmpp.XmppHeaders; + + +/** + * This is used in {@link org.springframework.integration.xmpp.presence.OutboundXmppRosterEventsEndpointTests} to produce fake status / presence updates. + * + * @author Josh Long + * @since 2.0 + */ +public class XmppRosterEventProducer implements MessageSource { + public Message receive() { + try { + Thread.sleep(1000 * 10); + } catch (InterruptedException e) { + // eat it + } + return (Math.random() > .5) ? + MessageBuilder.withPayload(StringUtils.EMPTY).setHeader(XmppHeaders.PRESENCE_MODE, Presence.Mode.chat).setHeader(XmppHeaders.PRESENCE_TYPE, Presence.Type.available) + .setHeader(XmppHeaders.PRESENCE_STATUS, "She Loves me").build() : + MessageBuilder.withPayload(StringUtils.EMPTY).setHeader(XmppHeaders.PRESENCE_MODE, Presence.Mode.dnd).setHeader(XmppHeaders.PRESENCE_TYPE, Presence.Type.available) + .setHeader(XmppHeaders.PRESENCE_STATUS, "She Loves me not").build(); + } +} diff --git a/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/config/XmppHeaderEnricherParserTests-context.xml b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/config/XmppHeaderEnricherParserTests-context.xml new file mode 100644 index 0000000000..e23f2ccb07 --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/config/XmppHeaderEnricherParserTests-context.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/ConsoleChatTests-context.xml b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/ConsoleChatTests-context.xml new file mode 100644 index 0000000000..84da3ba91d --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/ConsoleChatTests-context.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/InboundXmppEndpointTests-context.xml b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/InboundXmppEndpointTests-context.xml new file mode 100644 index 0000000000..d7aa3cdc82 --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/InboundXmppEndpointTests-context.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/OutboundXmppEndpointTests-context.xml b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/OutboundXmppEndpointTests-context.xml new file mode 100644 index 0000000000..9c988b3fb8 --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/messages/OutboundXmppEndpointTests-context.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/presence/InboundXmppRosterEventsEndpointTests-context.xml b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/presence/InboundXmppRosterEventsEndpointTests-context.xml new file mode 100644 index 0000000000..4e558d9912 --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/presence/InboundXmppRosterEventsEndpointTests-context.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/presence/OutboundXmppRosterEventsEndpointTests-context.xml b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/presence/OutboundXmppRosterEventsEndpointTests-context.xml new file mode 100644 index 0000000000..82ce44f795 --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/org/springframework/integration/xmpp/presence/OutboundXmppRosterEventsEndpointTests-context.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xmpp/src/test/resources/test.properties b/spring-integration-xmpp/src/test/resources/test.properties new file mode 100644 index 0000000000..843eefdf52 --- /dev/null +++ b/spring-integration-xmpp/src/test/resources/test.properties @@ -0,0 +1,32 @@ +# +# Copyright 2010 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# to be able to run these tests, put this file on your desktop and configure as appropriate +user.1.login=user1@gmail.com +user.1.password=password +user.1.host=talk.google.com +user.1.service=gmail.com +user.1.sasl.mechanism=PLAIN +user.1.sasl.index=0 +user.1.resource=resource +user.1.port=5222 +user.2.login=user2@gmail.com +user.2.password=password +user.2.host=talk.google.com +user.2.service=gmail.com +user.2.sasl.mechanism=PLAIN +user.2.sasl.index=0 +user.2.resource=resource +user.2.port=5222 diff --git a/spring-integration-xmpp/template.mf b/spring-integration-xmpp/template.mf new file mode 100644 index 0000000000..095c7c0ae1 --- /dev/null +++ b/spring-integration-xmpp/template.mf @@ -0,0 +1,15 @@ +Bundle-SymbolicName: org.springframework.integration.xmpp +Bundle-Name: Spring Integration XMPP Support +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + org.apache.commons.logging;version="[1.1.1, 2.0.0)", + org.apache.commons.lang.*;version="[2.5.0, 3.0.0)", + org.springframework.integration.*;version="[2.0.0, 2.0.1)", + org.springframework.beans.*;version="[3.0.0, 4.0.0)", + org.springframework.context;version="[3.0.0, 4.0.0)", + org.springframework.core.*;version="[3.0.0, 4.0.0)", + org.springframework.util;version="[3.0.0, 4.0.0)", + org.jivesoftware.*;version="[3.1.0, 4.0.0)", + javax.*;version="0", + org.w3c.dom.*;version="0"