diff --git a/pom.xml b/pom.xml
index 3492a1ce69..da9bab8d09 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
spring-integration-xmpp
spring-integration-ftp
spring-integration-sftp
+ spring-integration-twitter
UTF-8
diff --git a/spring-integration-twitter/pom.xml b/spring-integration-twitter/pom.xml
new file mode 100644
index 0000000000..ef954be9f0
--- /dev/null
+++ b/spring-integration-twitter/pom.xml
@@ -0,0 +1,100 @@
+
+
+ 4.0.0
+
+ org.springframework.integration
+ spring-integration-parent
+ 2.0.0.BUILD-SNAPSHOT
+
+ org.springframework.integration
+ spring-integration-twitter
+ jar
+ Spring Integration Twitter Support
+
+
+ javax.activation
+ activation
+ 1.1.1
+ true
+
+
+ org.twitter4j
+ twitter4j-core
+
+ 2.1.3
+
+
+ org.slf4j
+ nlog4j
+ 1.2.25
+ true
+
+
+ cglib
+ cglib-nodep
+ ${cglib.version}
+
+
+ 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-twitter/src/main/java/org/springframework/integration/twitter/AbstractInboundTwitterEndpointSupport.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractInboundTwitterEndpointSupport.java
new file mode 100644
index 0000000000..1c75947dfe
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractInboundTwitterEndpointSupport.java
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.apache.commons.lang.exception.ExceptionUtils;
+
+import org.springframework.context.Lifecycle;
+
+import org.springframework.integration.Message;
+import org.springframework.integration.core.MessageBuilder;
+import org.springframework.integration.core.MessageChannel;
+import org.springframework.integration.core.MessagingTemplate;
+import org.springframework.integration.endpoint.AbstractEndpoint;
+import org.springframework.integration.twitter.oauth.OAuthConfiguration;
+
+import org.springframework.util.Assert;
+
+import twitter4j.RateLimitStatus;
+import twitter4j.ResponseList;
+import twitter4j.Twitter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * There are a lot of operations that are common to receiving the various types of messages when using the Twitter API, and this
+ * class abstracts most of them for you. Implementers must take note of {@link org.springframework.integration.twitter.AbstractInboundTwitterEndpointSupport#runAsAPIRateLimitsPermit(org.springframework.integration.twitter.AbstractInboundTwitterEndpointSupport.ApiCallback)}
+ * which will invoke the instance of {@link org.springframework.integration.twitter.AbstractInboundTwitterEndpointSupport.ApiCallback} when the rate-limit API
+ * deems that its OK to do so. This class handles keeping tabs on that and on spacing out requests as required.
+ *
+ * Simialarly, this class handles keeping track on the latest inbound message its received and avoiding, where possible, redelivery of
+ * common messages. This functionality is enabled using the {@link org.springframework.integration.context.metadata.MetadataPersister} implementation
+ *
+ * @author Josh Long
+ * @since 2.0
+ */
+public abstract class AbstractInboundTwitterEndpointSupport extends AbstractEndpoint implements Lifecycle {
+ protected volatile OAuthConfiguration configuration;
+ protected final MessagingTemplate messagingTemplate = new MessagingTemplate();
+ private volatile MessageChannel requestChannel;
+ protected volatile long markerId = -1;
+ protected Twitter twitter;
+ private final Object markerGuard = new Object();
+ private final Object apiPermitGuard = new Object();
+
+ @SuppressWarnings("unused")
+ public void setConfiguration(OAuthConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ abstract protected void markLastStatusId(T statusId);
+
+ abstract protected List sort(List rl);
+
+ protected void forwardAll(ResponseList tResponses) {
+ List stats = new ArrayList();
+
+ for (T t : tResponses)
+ stats.add(t);
+
+ for (T twitterResponse : sort(stats))
+ forward(twitterResponse);
+ }
+
+ public long getMarkerId() {
+ return markerId;
+ }
+
+ @Override
+ protected void doStart() {
+ try {
+ refresh();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected void forward(T status) {
+ synchronized (this.markerGuard) {
+ Message twtMsg = MessageBuilder.withPayload(status).build();
+ messagingTemplate.send(requestChannel, twtMsg);
+ markLastStatusId(status);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void runAsAPIRateLimitsPermit(ApiCallback cb)
+ throws Exception {
+ synchronized (this.apiPermitGuard) {
+ while (waitUntilPullAvailable()) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("have room to make an API request now");
+ }
+
+ cb.run(this, twitter);
+ }
+ }
+ }
+
+ protected boolean handleReceivingRateLimitStatus(RateLimitStatus rateLimitStatus) {
+ try {
+ int secondsUntilReset = rateLimitStatus.getSecondsUntilReset();
+ int remainingHits = rateLimitStatus.getRemainingHits();
+
+ if (remainingHits == 0) {
+ logger.debug("rate status limit service returned 0 for the remaining hits value");
+
+ return false;
+ }
+
+ if (secondsUntilReset == 0) {
+ logger.debug("rate status limit service returned 0 for the seconds until reset period value");
+
+ return false;
+ }
+
+ int secondsUntilWeCanPullAgain = secondsUntilReset / remainingHits;
+ long msUntilWeCanPullAgain = secondsUntilWeCanPullAgain * 1000;
+
+ logger.debug("need to Thread.sleep() " + secondsUntilWeCanPullAgain + " seconds until the next timeline pull. Have " + remainingHits +
+ " remaining pull this rate period. The period ends in " + secondsUntilReset);
+
+ Thread.sleep(msUntilWeCanPullAgain);
+ } catch (Throwable throwable) {
+ logger.debug("encountered an error when" + " trying to refresh the timeline: " + ExceptionUtils.getFullStackTrace(throwable));
+ }
+
+ return true;
+ }
+
+ protected boolean waitUntilPullAvailable() throws Exception {
+ return this.handleReceivingRateLimitStatus(this.twitter.getRateLimitStatus());
+ }
+
+ protected boolean hasMarkedStatus() {
+ return markerId > -1;
+ }
+
+ abstract protected void refresh() throws Exception;
+
+ @Override
+ protected void onInit() throws Exception {
+ messagingTemplate.afterPropertiesSet();
+ Assert.notNull(this.configuration, "'configuration' can't be null");
+ this.twitter = this.configuration.getTwitter();
+ Assert.notNull(this.twitter, "'twitter' instance can't be null");
+ }
+
+ @Override
+ protected void doStop() {
+ }
+
+ @SuppressWarnings("unused")
+ public void setRequestChannel(MessageChannel requestChannel) {
+ this.messagingTemplate.setDefaultChannel(requestChannel);
+ this.requestChannel = requestChannel;
+ }
+
+ /**
+ * Hook for clients to run logic when the API rate limiting lets us
+ *
+ * Simply register your callback using #runAsAPIRateLimitsPermit
+ *
+ * @param
+ */
+ public static interface ApiCallback {
+ void run(C t, Twitter twitter) throws Exception;
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractInboundTwitterStatusEndpointSupport.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractInboundTwitterStatusEndpointSupport.java
new file mode 100644
index 0000000000..955015c76e
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractInboundTwitterStatusEndpointSupport.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import twitter4j.Status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+
+/**
+ * Simple base class for the reply and timeline cases (as well as any other {@link twitter4j.Status} implementations of
+ * {@link twitter4j.TwitterResponse}.
+ *
+ * @author Josh Long
+ */
+abstract public class AbstractInboundTwitterStatusEndpointSupport extends AbstractInboundTwitterEndpointSupport {
+ private Comparator statusComparator = new Comparator() {
+ public int compare(Status status, Status status1) {
+ return status.getCreatedAt().compareTo(status1.getCreatedAt());
+ }
+ };
+
+ @Override
+ protected void markLastStatusId(Status statusId) {
+ this.markerId = statusId.getId();
+ }
+
+ @Override
+ protected List sort(List rl) {
+ List statusArrayList = new ArrayList();
+ statusArrayList.addAll(rl);
+ Collections.sort(statusArrayList, statusComparator);
+
+ return statusArrayList;
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractOutboundTwitterEndpointSupport.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractOutboundTwitterEndpointSupport.java
new file mode 100644
index 0000000000..06635ad690
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/AbstractOutboundTwitterEndpointSupport.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.springframework.integration.core.MessageHandler;
+import org.springframework.integration.endpoint.AbstractEndpoint;
+import org.springframework.integration.twitter.oauth.OAuthConfiguration;
+import org.springframework.util.Assert;
+import twitter4j.Twitter;
+
+
+/**
+ * The adapters that support 'sending' / 'updating status' messages will do so on top of this implementation for convenience, only.
+ *
+ * @author Josh Long
+ */
+public abstract class AbstractOutboundTwitterEndpointSupport extends AbstractEndpoint implements MessageHandler {
+ protected volatile OAuthConfiguration configuration;
+ protected volatile Twitter twitter;
+ protected volatile StatusUpdateSupport statusUpdateSupport = new StatusUpdateSupport();
+
+ @SuppressWarnings("unused")
+ public void setConfiguration(OAuthConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ protected void onInit() throws Exception {
+ Assert.notNull(this.configuration, "'configuration' can't be null");
+ this.twitter = this.configuration.getTwitter();
+ Assert.notNull(this.twitter, "'twitter' can't be null");
+ }
+
+
+ @Override
+ protected void doStart() {
+ }
+
+ @Override
+ protected void doStop() {
+ }
+
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundDMStatusEndpoint.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundDMStatusEndpoint.java
new file mode 100644
index 0000000000..de3a622bc9
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundDMStatusEndpoint.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import twitter4j.DirectMessage;
+import twitter4j.Paging;
+import twitter4j.Twitter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+
+/**
+ * This class handles support for receiving DMs (direct messages) using Twitter.
+ *
+ * @author Josh Long
+ */
+public class InboundDMStatusEndpoint extends AbstractInboundTwitterEndpointSupport {
+ private Comparator dmComparator = new Comparator() {
+ public int compare(DirectMessage directMessage, DirectMessage directMessage1) {
+ return directMessage.getCreatedAt().compareTo(directMessage1.getCreatedAt());
+ }
+ };
+
+ @Override
+ protected void markLastStatusId(DirectMessage dm) {
+ this.markerId = dm.getId();
+ }
+
+ @Override
+ protected List sort(List rl) {
+ List dms = new ArrayList();
+ dms.addAll(rl);
+ Collections.sort(dms, dmComparator);
+
+ return dms;
+ }
+
+ @Override
+ protected void refresh() throws Exception {
+ this.runAsAPIRateLimitsPermit(new ApiCallback() {
+ public void run(InboundDMStatusEndpoint t, Twitter twitter)
+ throws Exception {
+ forwardAll((!hasMarkedStatus()) ? t.twitter.getDirectMessages() : t.twitter.getDirectMessages(new Paging(t.getMarkerId())));
+ }
+ });
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundMentionStatusEndpoint.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundMentionStatusEndpoint.java
new file mode 100644
index 0000000000..41e033536d
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundMentionStatusEndpoint.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import twitter4j.Paging;
+import twitter4j.Twitter;
+
+
+/**
+ * Handles forwarding all new {@link twitter4j.Status} that are 'replies' or 'mentions' to some other tweet.
+ *
+ * @author Josh Long
+ */
+public class InboundMentionStatusEndpoint extends AbstractInboundTwitterStatusEndpointSupport {
+ @Override
+ protected void refresh() throws Exception {
+ this.runAsAPIRateLimitsPermit(new ApiCallback() {
+ public void run(InboundMentionStatusEndpoint ctx, Twitter twitter)
+ throws Exception {
+ forwardAll((!hasMarkedStatus()) ? twitter.getMentions() : twitter.getMentions(new Paging(ctx.getMarkerId())));
+ }
+ });
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundUpdatedStatusEndpoint.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundUpdatedStatusEndpoint.java
new file mode 100644
index 0000000000..6eeb92e85b
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/InboundUpdatedStatusEndpoint.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import twitter4j.Paging;
+import twitter4j.Twitter;
+
+
+/**
+ * This {@link org.springframework.integration.core.MessageSource} lets Spring Integration consume a given account's timeline
+ * as messages. It has support for dynamic throttling of API requests.
+ *
+ * @author Josh Long
+ * @since 2.0
+ */
+public class InboundUpdatedStatusEndpoint extends AbstractInboundTwitterStatusEndpointSupport {
+ @Override
+ protected void refresh() throws Exception {
+ this.runAsAPIRateLimitsPermit(new ApiCallback() {
+ public void run(InboundUpdatedStatusEndpoint t, Twitter twitter)
+ throws Exception {
+ forwardAll((!t.hasMarkedStatus()) ? twitter.getFriendsTimeline() : twitter.getFriendsTimeline(new Paging(t.getMarkerId())));
+ }
+ });
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/OutboundDMStatusMessageHandler.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/OutboundDMStatusMessageHandler.java
new file mode 100644
index 0000000000..84968bcb55
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/OutboundDMStatusMessageHandler.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.springframework.integration.Message;
+import org.springframework.integration.MessageDeliveryException;
+import org.springframework.integration.MessageHandlingException;
+import org.springframework.integration.MessageRejectedException;
+
+import org.springframework.util.Assert;
+
+import twitter4j.TwitterException;
+
+
+/**
+ * Simple adapter to support sending outbound direct messages ("DM"s) using twitter
+ *
+ * @author Josh Long
+ * @see org.springframework.integration.twitter.TwitterHeaders
+ * @see twitter4j.Twitter
+ */
+public class OutboundDMStatusMessageHandler extends AbstractOutboundTwitterEndpointSupport {
+ public void handleMessage(Message> message) throws MessageRejectedException, MessageHandlingException, MessageDeliveryException {
+ try {
+ String txt = (String) message.getPayload();
+ Object toUser =
+ message.getHeaders().containsKey(TwitterHeaders.TWITTER_DM_TARGET_USER_ID)?
+ message.getHeaders().get(TwitterHeaders.TWITTER_DM_TARGET_USER_ID) :
+ null;
+
+ Assert.notNull(txt, "the message payload must be a String to be used as the direct message body text");
+
+ Assert.notNull(toUser, "the header '" + TwitterHeaders.TWITTER_DM_TARGET_USER_ID + "' must be present");
+
+ Assert.state(toUser instanceof String || toUser instanceof Integer,
+ "the header '" + TwitterHeaders.TWITTER_DM_TARGET_USER_ID + "' must be either a String (a screenname) or an int (a user ID)");
+
+ if (toUser instanceof Integer) {
+ this.twitter.sendDirectMessage((Integer) toUser, txt);
+ } else if (toUser instanceof String) {
+ this.twitter.sendDirectMessage((String) toUser, txt);
+ }
+ } catch (TwitterException e) {
+ logger.debug(e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/OutboundUpdatedStatusMessageHandler.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/OutboundUpdatedStatusMessageHandler.java
new file mode 100644
index 0000000000..e7ece7b595
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/OutboundUpdatedStatusMessageHandler.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.springframework.integration.Message;
+import org.springframework.integration.MessageDeliveryException;
+import org.springframework.integration.MessageHandlingException;
+import org.springframework.integration.MessageRejectedException;
+import org.springframework.util.Assert;
+import twitter4j.StatusUpdate;
+
+
+/**
+ * This class is useful for both sending regular status updates as well as 'replies' or 'mentions'
+ *
+ * @author Josh Long
+ * @since 2.0
+ */
+public class OutboundUpdatedStatusMessageHandler extends AbstractOutboundTwitterEndpointSupport {
+ public void handleMessage(Message> message) throws MessageRejectedException, MessageHandlingException, MessageDeliveryException {
+ try {
+ StatusUpdate statusUpdate = this.statusUpdateSupport.fromMessage(message);
+ Assert.notNull(statusUpdate, "couldn't send message, unable to build a StatusUpdate instance correctly");
+ this.twitter.updateStatus(statusUpdate);
+ } catch (Throwable e) {
+ this.logger.debug(e);
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/StatusUpdateSupport.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/StatusUpdateSupport.java
new file mode 100644
index 0000000000..e2fa10485a
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/StatusUpdateSupport.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.springframework.integration.Message;
+
+import org.springframework.util.StringUtils;
+
+import twitter4j.GeoLocation;
+import twitter4j.StatusUpdate;
+
+
+/**
+ * Convenience class that can take a seemingly disparate jumble of headers and a payload and do a best-faith attempt at vending a {@link twitter4j.StatusUpdate} instance
+ *
+ * @author Josh Long
+ * @see twitter4j.StatusUpdate
+ * @see org.springframework.integration.twitter.TwitterHeaders
+ * @since 2.0
+ */
+public class StatusUpdateSupport {
+ /**
+ * {@link StatusUpdate} instances are used to drive status updates.
+ *
+ * @param message the inbound messages
+ * @return a {@link StatusUpdate} that's been materialized from the inbound message
+ * @throws Throwable thrown if something goes wrong
+ */
+ public StatusUpdate fromMessage(Message> message)
+ throws Throwable {
+ Object payload = message.getPayload();
+ StatusUpdate statusUpdate = null;
+
+ if (payload instanceof String) {
+ statusUpdate = new StatusUpdate((String) payload);
+
+ if (message.getHeaders().containsKey(TwitterHeaders.TWITTER_IN_REPLY_TO_STATUS_ID)) {
+ Long replyId = (Long) message.getHeaders().get(TwitterHeaders.TWITTER_IN_REPLY_TO_STATUS_ID);
+
+ if ((replyId != null) && (replyId > 0)) {
+ statusUpdate.inReplyToStatusId(replyId);
+ }
+ }
+
+ if (message.getHeaders().containsKey(TwitterHeaders.TWITTER_PLACE_ID)) {
+ String placeId = (String) message.getHeaders().get(TwitterHeaders.TWITTER_PLACE_ID);
+
+ if (StringUtils.hasText(placeId)) {
+ statusUpdate.placeId(placeId);
+ }
+ }
+
+ if (message.getHeaders().containsKey(TwitterHeaders.TWITTER_GEOLOCATION)) {
+ GeoLocation geoLocation = (GeoLocation) message.getHeaders().get(TwitterHeaders.TWITTER_GEOLOCATION);
+
+ if (null != geoLocation) {
+ statusUpdate.location(geoLocation);
+ }
+ }
+
+ if (message.getHeaders().containsKey(TwitterHeaders.TWITTER_DISPLAY_COORDINATES)) {
+ Boolean displayCoords = (Boolean) message.getHeaders().get(TwitterHeaders.TWITTER_DISPLAY_COORDINATES);
+
+ if (displayCoords != null) {
+ statusUpdate.displayCoordinates(displayCoords);
+ }
+ }
+ }
+
+ if (payload instanceof StatusUpdate) {
+ statusUpdate = (StatusUpdate) payload;
+ }
+
+ return statusUpdate;
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/TwitterHeaders.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/TwitterHeaders.java
new file mode 100644
index 0000000000..f6b80d4816
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/TwitterHeaders.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+
+/**
+ * An enum to allow users to express interest in particular kinds of tweets.
+ *
+ * Contains header keys used by the various adapters.
+ *
+ * @author Josh Long
+ * @since 2.0
+ */
+public class TwitterHeaders {
+ public static final String TWITTER_IN_REPLY_TO_STATUS_ID = "TWITTER_IN_REPLY_TO_STATUS_ID";
+ public static final String TWITTER_PLACE_ID = "TWITTER_PLACE_ID";
+ public static final String TWITTER_GEOLOCATION = "TWITTER_GEOLOCATION";
+ public static final String TWITTER_DISPLAY_COORDINATES = "TWITTER_DISPLAY_COORDINATES";
+ public static final String TWITTER_DM_TARGET_USER_ID = "TWITTER_DM_TARGET_USER_ID";
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterNamespaceHandler.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterNamespaceHandler.java
new file mode 100644
index 0000000000..58ca2b1912
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterNamespaceHandler.java
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.config;
+
+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.ParserContext;
+
+import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
+import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
+import org.springframework.integration.twitter.*;
+import org.springframework.integration.twitter.oauth.OAuthConfigurationFactoryBean;
+
+import org.springframework.util.StringUtils;
+
+import org.w3c.dom.Element;
+
+
+/**
+ * @author Josh Long
+ * @since 2.0
+ */
+@SuppressWarnings("unused")
+public class TwitterNamespaceHandler extends org.springframework.beans.factory.xml.NamespaceHandlerSupport {
+ public static final String DIRECT_MESSAGES = "direct-messages";
+ public static final String MENTIONS = "mentions";
+ public static final String FRIENDS = "friends";
+
+ public void init() {
+ // twitter connections
+ registerBeanDefinitionParser("twitter-connection", new TwitterConnectionParser());
+
+ // inbound
+ registerBeanDefinitionParser("inbound-update-channel-adapter", new TwitterUpdatedStatusInboundEndpointParser());
+ registerBeanDefinitionParser("inbound-dm-channel-adapter", new TwitterDMInboundEndpointParser());
+ registerBeanDefinitionParser("inbound-mention-channel-adapter", new TwitterMentionInboundEndpointParser());
+
+ // outbound
+ registerBeanDefinitionParser("outbound-update-channel-adapter", new TwitterUpdatedStatusOutboundEndpointParser());
+ registerBeanDefinitionParser("outbound-dm-channel-adapter", new TwitterDMOutboundEndpointParser());
+ }
+
+ private static void configureTwitterConnection(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ String ref = element.getAttribute("twitter-connection");
+
+ if (org.springframework.util.StringUtils.hasText(ref)) {
+ builder.addPropertyReference("twitterConnection", ref);
+ } else {
+ for (String attribute : new String[] { "consumer-key", "consumer-secret", "access-token", "access-token-secret" }) {
+ IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, attribute);
+ }
+ }
+ }
+
+ // twitter:twitter-connection
+ private static class TwitterConnectionParser extends AbstractSingleBeanDefinitionParser {
+ @Override
+ protected Class getBeanClass(Element element) {
+ return OAuthConfigurationFactoryBean.class;
+ }
+
+ @Override
+ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ configureTwitterConnection(element, parserContext, builder);
+ }
+
+ @Override
+ protected boolean shouldGenerateIdAsFallback() {
+ return true;
+ }
+ }
+
+ // twitter:inbound-mention-channel-adapter
+ private static class TwitterMentionInboundEndpointParser extends AbstractSingleBeanDefinitionParser {
+ @Override
+ protected String getBeanClassName(Element element) {
+ return InboundMentionStatusEndpoint.class.getName();
+ }
+
+ @Override
+ protected boolean shouldGenerateIdAsFallback() {
+ return true;
+ }
+
+ @Override
+ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "channel", "requestChannel");
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "twitter-connection", "configuration");
+ }
+ }
+
+ // twitter:inbound-dm-channel-adapter
+ private static class TwitterDMInboundEndpointParser extends AbstractSingleBeanDefinitionParser {
+ @Override
+ protected String getBeanClassName(Element element) {
+ return InboundDMStatusEndpoint.class.getName();
+ }
+
+ @Override
+ protected boolean shouldGenerateIdAsFallback() {
+ return true;
+ }
+
+ @Override
+ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "channel", "requestChannel");
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "twitter-connection", "configuration");
+ }
+ }
+
+ // twitter:inbound-update-channel-adapter
+ private static class TwitterUpdatedStatusInboundEndpointParser extends AbstractSingleBeanDefinitionParser {
+ @Override
+ protected String getBeanClassName(Element element) {
+ return InboundUpdatedStatusEndpoint.class.getName();
+ }
+
+ @Override
+ protected boolean shouldGenerateIdAsFallback() {
+ return true;
+ }
+
+ @Override
+ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "channel", "requestChannel");
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "twitter-connection", "configuration");
+ }
+ }
+
+ // twitter:outbound-update-channel-adapter
+ private static class TwitterUpdatedStatusOutboundEndpointParser extends AbstractOutboundChannelAdapterParser {
+ @Override
+ protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(OutboundUpdatedStatusMessageHandler.class.getName());
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "twitter-connection", "configuration");
+ return builder.getBeanDefinition();
+ }
+ }
+
+ // twitter:outbound-dm-channel-adapter
+ private static class TwitterDMOutboundEndpointParser extends AbstractOutboundChannelAdapterParser {
+ @Override
+ protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(OutboundDMStatusMessageHandler.class.getName());
+ IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "twitter-connection", "configuration");
+ return builder.getBeanDefinition();
+ }
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/AbstractOAuthAccessTokenBasedFactoryBean.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/AbstractOAuthAccessTokenBasedFactoryBean.java
new file mode 100644
index 0000000000..914019607c
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/AbstractOAuthAccessTokenBasedFactoryBean.java
@@ -0,0 +1,213 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.oauth;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.config.PropertiesFactoryBean;
+
+import org.springframework.core.io.Resource;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import twitter4j.http.AccessToken;
+import twitter4j.http.RequestToken;
+
+import java.util.Properties;
+
+
+/**
+ * base-class for {@link org.springframework.integration.twitter.oauth.OAuthAccessTokenBasedTwitterFactoryBean}.
+ *
+ * Provides hooks so that subclasses able to reference a concrete implementation of the generic type parameter can call methods for us. Most hooks are to handle the case
+ * of the first time subscription, where no accessToken is present.
+ *
+ * @author Josh Long
+ * @param
+ * @see org.springframework.integration.twitter.oauth.OAuthAccessTokenBasedTwitterFactoryBean
+ * @since 2.0
+ */
+abstract public class AbstractOAuthAccessTokenBasedFactoryBean implements InitializingBean, FactoryBean {
+ protected OAuthConfiguration configuration;
+ protected final Object monitor = new Object();
+ protected volatile T twitter;
+ protected volatile AccessTokenInitialRequestProcessListener accessTokenInitialRequestProcessListener;
+ protected volatile boolean initialized = false;
+
+ /**
+ * Nasty little bit of circular indirection here: the {@link org.springframework.integration.twitter.oauth.OAuthConfiguration} hosts the String values for authentication,
+ * which we need to build up this instance, but the {@link org.springframework.integration.twitter.oauth.OAuthConfiguration} in turn needs references to the instances provided by
+ * this {@link org.springframework.beans.factory.FactoryBean}. So, they collaborate and guard each others state. Ultimately, clients should use {@link org.springframework.integration.twitter.oauth.OAuthConfiguration}
+ * to correctly any implementations of this factory bean as well as the {@link org.springframework.integration.twitter.oauth.OAuthConfiguration} reference itself.
+ *
+ * @param configuration the configuration object
+ */
+ protected AbstractOAuthAccessTokenBasedFactoryBean(OAuthConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ /**
+ * This probably doesn't belong here. It's more for support for running the {@link OAuthAccessTokenBasedTwitterFactoryBean#main(String[])} or
+ * {@link OAuthAccessTokenBasedTwitterFactoryBean#main(String[])} methods that run the user through a command line tool to approve a user for the first
+ * time if the user hasn't obtained her {@code accessToken } yet
+ *
+ * @param resource the resource where properties file lives
+ * @return returns a fully configured {@link java.util.Properties} instance
+ * @throws Exception thrown if anythign goes wrong
+ */
+ @SuppressWarnings("unused")
+ protected static Properties fromResource(Resource resource)
+ throws Exception {
+ PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
+ propertiesFactoryBean.setLocation(resource);
+ propertiesFactoryBean.setSingleton(true);
+ propertiesFactoryBean.afterPropertiesSet();
+
+ return propertiesFactoryBean.getObject();
+ }
+
+ /**
+ * provides lifecycle for initiation of the reference. By the time this method is left we should have a fully configured twitter connection that can connect and make calls
+ * @throws Exception
+ */
+ public void afterPropertiesSet() throws Exception {
+ synchronized (this.monitor) {
+ if (this.accessTokenInitialRequestProcessListener == null) {
+ accessTokenInitialRequestProcessListener = new ConsoleBasedAccessTokenInitialRequestProcessListener();
+ }
+
+ Assert.notNull(this.configuration.getConsumerKey(), "'consumerKey' mustn't be null");
+ Assert.notNull(this.configuration.getConsumerSecret(), "'consumerSecret' mustn't be null");
+
+ AccessToken accessTokenObj;
+
+ if (StringUtils.hasText(this.configuration.getAccessToken()) && StringUtils.hasText(this.configuration.getAccessTokenSecret())) {
+ accessTokenObj = new AccessToken(this.configuration.getAccessToken(), this.configuration.getAccessTokenSecret());
+ } else {
+ accessTokenObj = initialAuthorizationWizard();
+ }
+
+ establishTwitterObject(accessTokenObj);
+
+ Assert.notNull(accessTokenObj, "'accessTokenObj' can't be null");
+
+ this.initialized = true;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void setAccessTokenInitialRequestProcessListener(AccessTokenInitialRequestProcessListener accessTokenInitialRequestProcessListener) {
+ this.accessTokenInitialRequestProcessListener = accessTokenInitialRequestProcessListener;
+ }
+
+ public abstract void establishTwitterObject(AccessToken accessToken)
+ throws Exception;
+
+ /**
+ * because we are not able to dereference the {@link twitter4j.Twitter} or {@link twitter4j.AsyncTwitter} instances, we need to ask subclasses to call
+ * us how to call {@link twitter4j.AsyncTwitter#getOAuthRequestToken()} or {@link twitter4j.Twitter#getOAuthRequestToken()} for us.This method
+ * will never be evaluated as long as the {@link org.springframework.integration.twitter.oauth.OAuthConfiguration#accessToken}
+ * and {@link org.springframework.integration.twitter.oauth.OAuthConfiguration#accessTokenSecret} beans are not null.
+ *
+ * @return the {@link twitter4j.http.RequestToken} as vended by the service. Ths will contain a verification URl required to obtain an access key and secret.
+ * @throws Exception thrown if anything should go wrong
+ */
+ public abstract RequestToken getOAuthRequestToken()
+ throws Exception;
+
+ /**
+ * Only used if the impementation is trying to get an {@link twitter4j.http.AccessToken} for the first time. This method
+ * will never be evaluated as long as the {@link org.springframework.integration.twitter.oauth.OAuthConfiguration#accessToken}
+ * and {@link org.springframework.integration.twitter.oauth.OAuthConfiguration#accessTokenSecret} beans are not null.
+ *
+ * @param token the initiating {@link twitter4j.http.RequestToken}
+ * @param pin the string returned from the verification URL
+ * @return returns the {@link twitter4j.http.AccessToken} fetched from the twitter service.
+ * @throws Exception thrown if anything should go wrong
+ */
+ public abstract AccessToken getOAuthAccessToken(RequestToken token, String pin)
+ throws Exception;
+
+ /**
+ * Only used if the impementation is trying to get an {@link twitter4j.http.AccessToken} for the first time. This method
+ * will never be evaluated as long as the {@link org.springframework.integration.twitter.oauth.OAuthConfiguration#accessToken}
+ * and {@link org.springframework.integration.twitter.oauth.OAuthConfiguration#accessTokenSecret} beans are not null.
+ *
+ * @return returns the {@link twitter4j.http.AccessToken} fetched from the twitter service.
+ * @throws Exception thrown if anything should go wrong
+ */
+ public abstract AccessToken getOAuthAccessToken() throws Exception;
+
+ /**
+ * @return returns the freshly created {@link twitter4j.http.AccessToken} object from the service
+ * @throws Exception for just about any deviation from the expected
+ */
+ private AccessToken initialAuthorizationWizard() throws Exception {
+ Assert.notNull(this.accessTokenInitialRequestProcessListener, "'accessTokenInitialRequestProcessListener' can't be null");
+
+ try {
+ RequestToken requestToken = getOAuthRequestToken();
+ String pin = this.accessTokenInitialRequestProcessListener.openUrlAndReturnPin(requestToken.getAuthorizationURL());
+ AccessToken at = StringUtils.hasText(pin) ? getOAuthAccessToken(requestToken, pin) : getOAuthAccessToken();
+ this.accessTokenInitialRequestProcessListener.persistReturnedAccessToken(at);
+
+ return at;
+ } catch (Throwable th) {
+ this.accessTokenInitialRequestProcessListener.failure(th);
+ }
+
+ return null;
+ }
+
+ /**
+ * Responsibility of subclasses to call this because we cant dereference the generic type appropriately. The responsibility is
+ * to call {@link twitter4j.Twitter#verifyCredentials()} or {@link twitter4j.AsyncTwitter#verifyCredentials()} as appropriate
+ *
+ * @throws Exception if there's an inability to authenticate
+ */
+ public abstract void verifyCredentials() throws Exception;
+
+ /**
+ * Rubber meets the road: builds up a reference to the twitter4j.(Async)Twitter instance
+ *
+ * @return the instance
+ * @throws Exception thrown in case some condition isn't met correctly in construction
+ */
+ public T getObject() throws Exception {
+ if (!initialized) {
+ afterPropertiesSet();
+ }
+
+ return this.twitter;
+ }
+
+ /**
+ * this method is delegated to implementations because we can't correctly dereference the generic type's class
+ *
+ * @return a class
+ */
+ abstract public Class> getObjectType();
+
+ /**
+ * Standard {@link org.springframework.beans.factory.FactoryBean} method. Implementations may override if there's a specific method
+ *
+ * @return whether or not this is a singleton
+ */
+ public boolean isSingleton() {
+ return true;
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/AccessTokenInitialRequestProcessListener.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/AccessTokenInitialRequestProcessListener.java
new file mode 100644
index 0000000000..14a323272e
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/AccessTokenInitialRequestProcessListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.oauth;
+
+import twitter4j.http.AccessToken;
+
+
+/**
+ * if the factory bean isn't provided a 'accesstoken' and 'accesstokensecret', then it will try to obtain those bits of informatio on the users behalf.
+ *
+ * In doing so it will need input fro the user (automatic or human intervention is required)
+ *
+ * @author Josh Long
+ */
+public interface AccessTokenInitialRequestProcessListener {
+ String openUrlAndReturnPin(String urlToOpen) throws Exception;
+
+ void persistReturnedAccessToken(AccessToken accessToken)
+ throws Exception;
+
+ /**
+ * Callback in case something goes wrong at any point in the process. Up to client to intervene
+ * @param t the exception thrown (if any). Could be null if none available
+ */
+ void failure(Throwable t);
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/ConsoleBasedAccessTokenInitialRequestProcessListener.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/ConsoleBasedAccessTokenInitialRequestProcessListener.java
new file mode 100644
index 0000000000..af1ec9cc39
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/ConsoleBasedAccessTokenInitialRequestProcessListener.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.oauth;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.SystemUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
+
+import twitter4j.http.AccessToken;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.InputStreamReader;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+
+/**
+ * Default System.(out|in) based implementation of the {@link org.springframework.integration.twitter.oauth.AccessTokenInitialRequestProcessListener} interface
+ *
+ * @author Josh Long
+ */
+public class ConsoleBasedAccessTokenInitialRequestProcessListener implements AccessTokenInitialRequestProcessListener {
+ public String openUrlAndReturnPin(String urlToOpen)
+ throws Exception {
+ BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+ System.out.println("Open the following URL and grant access to your account:");
+ System.out.println(urlToOpen);
+ System.out.print("Enter the PIN(if aviailable) or just hit enter.[PIN]:");
+
+ return StringUtils.trim(br.readLine());
+ }
+
+ public void persistReturnedAccessToken(AccessToken accessToken)
+ throws Exception {
+ Map output = new HashMap();
+ output.put(OAuthConfigurationFactoryBean.WELL_KNOWN_CONSUMER_ACCESS_TOKEN, accessToken.getToken());
+ output.put(OAuthConfigurationFactoryBean.WELL_KNOWN_CONSUMER_ACCESS_TOKEN_SECRET, accessToken.getTokenSecret());
+
+ File accessTokenCreds = new File(SystemUtils.getJavaIoTmpDir(), "twitter-accesstoken.properties");
+ FileWriter writer = new FileWriter(accessTokenCreds);
+
+ Properties props = new Properties();
+ props.putAll(output);
+ props.store(writer, "oauth-access-token");
+ IOUtils.closeQuietly(writer);
+
+ System.out.println("The oauth accesstoken credentials have been written to " + accessTokenCreds.getAbsolutePath());
+ }
+
+ public void failure(Throwable t) {
+ System.err.println("Exception occurred when trying to retrieve credentials: " + ExceptionUtils.getFullStackTrace(t));
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthAccessTokenBasedTwitterFactoryBean.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthAccessTokenBasedTwitterFactoryBean.java
new file mode 100644
index 0000000000..0f4ad1daa1
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthAccessTokenBasedTwitterFactoryBean.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.oauth;
+
+import org.apache.commons.lang.SystemUtils;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import org.springframework.core.io.FileSystemResource;
+
+import twitter4j.ResponseList;
+import twitter4j.Status;
+import twitter4j.Twitter;
+import twitter4j.TwitterFactory;
+
+import twitter4j.http.AccessToken;
+import twitter4j.http.RequestToken;
+
+import java.io.File;
+
+import java.util.Properties;
+
+
+public class OAuthAccessTokenBasedTwitterFactoryBean extends AbstractOAuthAccessTokenBasedFactoryBean {
+ protected OAuthAccessTokenBasedTwitterFactoryBean(OAuthConfiguration configuration) {
+ super(configuration);
+ }
+
+ @Override
+ public void establishTwitterObject(AccessToken accessToken)
+ throws Exception {
+ this.twitter = new TwitterFactory().getOAuthAuthorizedInstance(this.configuration.getConsumerKey(), this.configuration.getConsumerSecret(), accessToken);
+ }
+
+ @Override
+ public RequestToken getOAuthRequestToken() throws Exception {
+ return twitter.getOAuthRequestToken();
+ }
+
+ @Override
+ public void verifyCredentials() throws Exception {
+ this.twitter.verifyCredentials();
+ }
+
+ @Override
+ public AccessToken getOAuthAccessToken(RequestToken token, String pin)
+ throws Exception {
+ return this.twitter.getOAuthAccessToken(token, pin);
+ }
+
+ @Override
+ public AccessToken getOAuthAccessToken() throws Exception {
+ return this.twitter.getOAuthAccessToken();
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return Twitter.class;
+ }
+
+ public static void main(String[] args) throws Throwable {
+ File propsFile = new File(new File(SystemUtils.getUserHome(), "Desktop"), "twitter.properties");
+ FileSystemResource fileSystemResource = new FileSystemResource(propsFile);
+ Properties properties = fromResource(fileSystemResource);
+
+ OAuthConfigurationFactoryBean oAuthConfigurationFactoryBean = new OAuthConfigurationFactoryBean();
+ oAuthConfigurationFactoryBean.bootstrapFromProperties(properties);
+
+ OAuthConfiguration configuration = oAuthConfigurationFactoryBean.getObject();
+
+ Twitter twitter = configuration.getTwitter();
+
+ System.out.println("Used the " + OAuthAccessTokenBasedTwitterFactoryBean.class.getName() + " and arrived at " + ToStringBuilder.reflectionToString(twitter));
+
+ ResponseList friendsTimeline = twitter.getFriendsTimeline();
+ System.out.println("Friends' timeline " + friendsTimeline);
+ Thread.sleep(10000);
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthConfiguration.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthConfiguration.java
new file mode 100644
index 0000000000..4bb1cc5a23
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthConfiguration.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.oauth;
+
+import twitter4j.Twitter;
+
+
+/**
+ * Simple bean we can use to store the configuration and store shared references to both an {@link twitter4j.AsyncTwitter}
+ * and an {@link twitter4j.Twitter} instance.
+ *
+ * client should store this bean and simply lookup the Twitter configuration from there
+ */
+public class OAuthConfiguration {
+ //
+ // private AsyncTwitter asyncTwitter;
+ private Twitter twitter;
+ private volatile String consumerKey;
+ private volatile String consumerSecret;
+ private volatile String accessToken;
+ private volatile String accessTokenSecret;
+
+ public OAuthConfiguration(String consumerKey, String consumerSecret, String accessToken, String accessTokenSecret) {
+ this.consumerKey = consumerKey;
+ this.consumerSecret = consumerSecret;
+ this.accessToken = accessToken;
+ this.accessTokenSecret = accessTokenSecret;
+ }
+
+ /** package friendly */
+ /* void setAsyncTwitter(AsyncTwitter asyncTwitter) {
+ this.asyncTwitter = asyncTwitter;
+ }*/
+
+ /** package friendly */
+ void setTwitter(Twitter twitter) {
+ this.twitter = twitter;
+ }
+
+ /**
+ * @return
+ */
+ public Twitter getTwitter() {
+ return twitter;
+ }
+
+ /*
+ public AsyncTwitter getAsyncTwitter() {
+ return asyncTwitter;
+ }*/
+ public String getConsumerKey() {
+ return consumerKey;
+ }
+
+ public void setConsumerKey(String consumerKey) {
+ this.consumerKey = consumerKey;
+ }
+
+ public String getConsumerSecret() {
+ return consumerSecret;
+ }
+
+ public void setConsumerSecret(String consumerSecret) {
+ this.consumerSecret = consumerSecret;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public String getAccessTokenSecret() {
+ return accessTokenSecret;
+ }
+
+ public void setAccessTokenSecret(String accessTokenSecret) {
+ this.accessTokenSecret = accessTokenSecret;
+ }
+}
diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthConfigurationFactoryBean.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthConfigurationFactoryBean.java
new file mode 100644
index 0000000000..e5dd2db8b8
--- /dev/null
+++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/oauth/OAuthConfigurationFactoryBean.java
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter.oauth;
+
+import org.springframework.beans.factory.FactoryBean;
+
+import org.springframework.util.Assert;
+
+import twitter4j.Twitter;
+
+import java.util.Properties;
+
+
+/**
+ * Center piece for configuration for all the twitter adapters
+ *
+ * @author Josh Long
+ * @since 2.0
+ */
+public class OAuthConfigurationFactoryBean implements FactoryBean {
+ public static final String WELL_KNOWN_CONSUMER_KEY = "twitter.oauth.consumerKey";
+ public static final String WELL_KNOWN_CONSUMER_KEY_SECRET = "twitter.oauth.consumerSecret";
+ public static final String WELL_KNOWN_CONSUMER_ACCESS_TOKEN = "twitter.oauth.accessToken";
+ public static final String WELL_KNOWN_CONSUMER_ACCESS_TOKEN_SECRET = "twitter.oauth.accessTokenSecret";
+ private volatile String consumerKey;
+ private volatile String consumerSecret;
+ private volatile String accessToken;
+ private volatile String accessTokenSecret;
+ private Twitter twitter;
+ private volatile OAuthConfiguration oAuthConfiguration;
+ private final Object guard = new Object();
+
+ private Twitter twitter(OAuthConfiguration oAuthConfiguration)
+ throws Exception {
+ OAuthAccessTokenBasedTwitterFactoryBean twitterFactoryBean = new OAuthAccessTokenBasedTwitterFactoryBean(oAuthConfiguration);
+ twitterFactoryBean.afterPropertiesSet();
+ twitterFactoryBean.verifyCredentials();
+
+ return twitterFactoryBean.getObject();
+ }
+
+ protected void bootstrapFromProperties(Properties props)
+ throws Throwable {
+ Assert.notNull(props, "'properties' must not be null");
+ this.setAccessToken(props.getProperty(WELL_KNOWN_CONSUMER_ACCESS_TOKEN));
+ this.setAccessTokenSecret(props.getProperty(WELL_KNOWN_CONSUMER_ACCESS_TOKEN_SECRET));
+ this.setConsumerKey(props.getProperty(WELL_KNOWN_CONSUMER_KEY));
+ this.setConsumerSecret(props.getProperty(WELL_KNOWN_CONSUMER_KEY_SECRET));
+ }
+
+ public OAuthConfiguration getObject() throws Exception {
+ return build();
+ }
+
+ public Class> getObjectType() {
+ return OAuthConfiguration.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+ public void setConsumerKey(String consumerKey) {
+ this.consumerKey = consumerKey;
+ }
+
+ public void setConsumerSecret(String consumerSecret) {
+ this.consumerSecret = consumerSecret;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public void setAccessTokenSecret(String accessTokenSecret) {
+ this.accessTokenSecret = accessTokenSecret;
+ }
+
+ private OAuthConfiguration build() throws Exception {
+ synchronized (this.guard) {
+ if (oAuthConfiguration == null) {
+ oAuthConfiguration = new OAuthConfiguration(this.consumerKey, this.consumerSecret, this.accessToken, this.accessTokenSecret);
+ twitter = this.twitter(oAuthConfiguration);
+ oAuthConfiguration.setTwitter(twitter);
+ }
+ }
+
+ return this.oAuthConfiguration;
+ }
+}
diff --git a/spring-integration-twitter/src/main/resources/META-INF/spring.handlers b/spring-integration-twitter/src/main/resources/META-INF/spring.handlers
new file mode 100644
index 0000000000..cce51179e9
--- /dev/null
+++ b/spring-integration-twitter/src/main/resources/META-INF/spring.handlers
@@ -0,0 +1,2 @@
+http\://www.springframework.org/schema/integration/twitter=org.springframework.integration.twitter.config.TwitterNamespaceHandler
+
diff --git a/spring-integration-twitter/src/main/resources/META-INF/spring.schemas b/spring-integration-twitter/src/main/resources/META-INF/spring.schemas
new file mode 100644
index 0000000000..b2ebc5bb0d
--- /dev/null
+++ b/spring-integration-twitter/src/main/resources/META-INF/spring.schemas
@@ -0,0 +1,2 @@
+http\://www.springframework.org/schema/integration/twitter/spring-integration-twitter-2.0.xsd=org/springframework/integration/twitter/config/spring-integration-twitter-2.0.xsd
+http\://www.springframework.org/schema/integration/twitter/spring-integration-twitter.xsd=org/springframework/integration/twitter/config/spring-integration-twitter-2.0.xsd
diff --git a/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-2.0.xsd b/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-2.0.xsd
new file mode 100644
index 0000000000..790a4c5264
--- /dev/null
+++ b/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-2.0.xsd
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sets up an instance of OAuthConfiguration which all adapters need to function properly
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configures an inbound channel adapter that consumes message (representing mentions of your handle)
+ from twitter and sends Messages whose payloads are Status objects.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configures an inbound channel adapter that consumes direct messages and forwards them to Spring Integration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configures an inbound channel adapter that consumes message (representing your friends' timeline updates)
+ from twitter and sends Messages whose payloads are Tweet objects.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configures an inbound channel adapter that consumes message (representing your friends' timeline updates)
+ from twitter and sends Messages whose payloads are Tweet objects.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configures an inbound channel adapter that consumes message (representing your friends' timeline updates)
+ from twitter and sends Messages whose payloads are Tweet objects.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/SimpleTwitterTestClient.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/SimpleTwitterTestClient.java
new file mode 100644
index 0000000000..bd4802f13c
--- /dev/null
+++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/SimpleTwitterTestClient.java
@@ -0,0 +1,43 @@
+package org.springframework.integration.twitter;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.integration.twitter.oauth.OAuthConfiguration;
+
+import org.springframework.test.context.ContextConfiguration;
+
+import twitter4j.Status;
+import twitter4j.Twitter;
+
+import java.util.Collection;
+
+import javax.annotation.PostConstruct;
+
+
+/**
+ * This class is used to simply demonstrating correctly factory-ing a {@link twitter4j.Twitter} instance
+ */
+@ContextConfiguration(locations = "twitter_connection_using_ns.xml")
+public class SimpleTwitterTestClient {
+ private Twitter twitter;
+ @Autowired
+ private volatile OAuthConfiguration oAuthConfiguration;
+
+ @PostConstruct
+ public void begin() throws Exception {
+ this.twitter = oAuthConfiguration.getTwitter();
+ }
+
+ @Test
+ @Ignore
+ public void testConnectivity() throws Throwable {
+ Collection responses;
+ Assert.assertNotNull(this.twitter);
+ Assert.assertNotNull(responses = this.twitter.getFriendsTimeline());
+ Assert.assertTrue(responses.size() > 0);
+ }
+}
diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestRecievingUsingNamespace.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestRecievingUsingNamespace.java
new file mode 100644
index 0000000000..3655fed20f
--- /dev/null
+++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestRecievingUsingNamespace.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
+
+
+/**
+ * @author Josh Long
+ */
+@ContextConfiguration(locations = {
+ "/receiving_dms_using_ns.xml"}
+)
+public class TestRecievingUsingNamespace extends AbstractJUnit4SpringContextTests {
+ @Autowired
+ private TwitterAnnouncer twitterAnnouncer;
+
+ @Test
+ @Ignore
+ public void testIt() throws Throwable {
+ long ctr = 0;
+ long s = 1000;
+
+ while (ctr < (s * 60 * 3)) {
+ Thread.sleep(s);
+ ctr += s;
+ }
+ }
+}
diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestSendingDMsUsingNamespace.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestSendingDMsUsingNamespace.java
new file mode 100644
index 0000000000..ad307d9007
--- /dev/null
+++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestSendingDMsUsingNamespace.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+
+import org.springframework.integration.Message;
+import org.springframework.integration.core.MessageBuilder;
+import org.springframework.integration.core.MessageChannel;
+import org.springframework.integration.core.MessagingTemplate;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
+
+import org.springframework.util.StringUtils;
+
+import twitter4j.GeoLocation;
+
+
+/**
+ * @author Josh Long
+ */
+@ContextConfiguration(locations = {
+ "/sending_dms_using_ns.xml"}
+)
+public class TestSendingDMsUsingNamespace extends AbstractJUnit4SpringContextTests {
+ private volatile MessagingTemplate messagingTemplate = new MessagingTemplate();
+ @Value("#{out}")
+ private MessageChannel channel;
+
+ @Test
+ @Ignore
+ public void testSendingATweet() throws Throwable {
+ String dmUsr = System.getProperties().getProperty("twitter.dm.user");
+ MessageBuilder mb = MessageBuilder.withPayload("'Hello world!', from the Spring Integration outbound Twitter adapter")
+ .setHeader(TwitterHeaders.TWITTER_GEOLOCATION, new GeoLocation(-76.226823, 23.642465)) // antarctica
+ .setHeader(TwitterHeaders.TWITTER_DISPLAY_COORDINATES, true);
+
+ if (StringUtils.hasText(dmUsr)) {
+ mb.setHeader(TwitterHeaders.TWITTER_DM_TARGET_USER_ID, dmUsr);
+ }
+
+ Message m = mb.build();
+
+ this.messagingTemplate.send(this.channel, m);
+ }
+}
diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestSendingUpdatesUsingNamespace.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestSendingUpdatesUsingNamespace.java
new file mode 100644
index 0000000000..2d3605dcae
--- /dev/null
+++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TestSendingUpdatesUsingNamespace.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.springframework.integration.twitter;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+
+import org.springframework.integration.Message;
+import org.springframework.integration.core.MessageBuilder;
+import org.springframework.integration.core.MessageChannel;
+import org.springframework.integration.core.MessagingTemplate;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
+
+import org.springframework.util.StringUtils;
+
+import twitter4j.GeoLocation;
+
+
+/**
+ * @author Josh Long
+ */
+@ContextConfiguration(locations = {
+ "/sending_updates_using_ns.xml"}
+)
+public class TestSendingUpdatesUsingNamespace extends AbstractJUnit4SpringContextTests {
+ private MessagingTemplate messagingTemplate = new MessagingTemplate();
+ @Value("#{out}")
+ private MessageChannel channel;
+
+ @Test
+ @Ignore
+ public void testSendingATweet() throws Throwable {
+ MessageBuilder mb = MessageBuilder.withPayload("'Hello world!', from the Spring Integration outbound Twitter adapter")
+ .setHeader(TwitterHeaders.TWITTER_IN_REPLY_TO_STATUS_ID, 21927437001L)
+ .setHeader(TwitterHeaders.TWITTER_GEOLOCATION, new GeoLocation(-76.226823, 23.642465)) // antarctica
+ .setHeader(TwitterHeaders.TWITTER_DISPLAY_COORDINATES, true);
+ Message m = mb.build();
+ this.messagingTemplate.send(this.channel, m);
+ }
+}
diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TwitterAnnouncer.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TwitterAnnouncer.java
new file mode 100644
index 0000000000..ca37a02ae7
--- /dev/null
+++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/TwitterAnnouncer.java
@@ -0,0 +1,22 @@
+package org.springframework.integration.twitter;
+
+import org.springframework.stereotype.Component;
+
+import twitter4j.DirectMessage;
+import twitter4j.Status;
+
+
+@Component
+public class TwitterAnnouncer {
+ public void dm(DirectMessage directMessage) {
+ System.out.println("A direct message has been received from " + directMessage.getSenderScreenName() + " with text " + directMessage.getText());
+ }
+
+ public void mention(Status s) {
+ System.out.println("A tweet mentioning (or replying) to " + "you was received having text " + s.getText() + " from " + s.getSource());
+ }
+
+ public void friendsTimelineUpdated(Status t) {
+ System.out.println("Received " + t.getText() + " from " + t.getSource());
+ }
+}
diff --git a/spring-integration-twitter/src/test/resources/receiving_dms_using_ns.xml b/spring-integration-twitter/src/test/resources/receiving_dms_using_ns.xml
new file mode 100644
index 0000000000..0b82b84d88
--- /dev/null
+++ b/spring-integration-twitter/src/test/resources/receiving_dms_using_ns.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/src/test/resources/receiving_replies_using_ns.xml b/spring-integration-twitter/src/test/resources/receiving_replies_using_ns.xml
new file mode 100644
index 0000000000..5d477ec2e2
--- /dev/null
+++ b/spring-integration-twitter/src/test/resources/receiving_replies_using_ns.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/src/test/resources/receiving_updates_using_ns.xml b/spring-integration-twitter/src/test/resources/receiving_updates_using_ns.xml
new file mode 100644
index 0000000000..06d799aa17
--- /dev/null
+++ b/spring-integration-twitter/src/test/resources/receiving_updates_using_ns.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/src/test/resources/sending_dms_using_ns.xml b/spring-integration-twitter/src/test/resources/sending_dms_using_ns.xml
new file mode 100644
index 0000000000..c5c0438f75
--- /dev/null
+++ b/spring-integration-twitter/src/test/resources/sending_dms_using_ns.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/src/test/resources/sending_updates_using_ns.xml b/spring-integration-twitter/src/test/resources/sending_updates_using_ns.xml
new file mode 100644
index 0000000000..8a251de4d2
--- /dev/null
+++ b/spring-integration-twitter/src/test/resources/sending_updates_using_ns.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/src/test/resources/twitter_connection_using_ns.xml b/spring-integration-twitter/src/test/resources/twitter_connection_using_ns.xml
new file mode 100644
index 0000000000..436ef86fd9
--- /dev/null
+++ b/spring-integration-twitter/src/test/resources/twitter_connection_using_ns.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-twitter/template.mf b/spring-integration-twitter/template.mf
new file mode 100644
index 0000000000..dbe783159f
--- /dev/null
+++ b/spring-integration-twitter/template.mf
@@ -0,0 +1,17 @@
+Bundle-SymbolicName: org.springframework.integration.twitter
+Bundle-Name: Spring Integration Twitter 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)",
+ org.apache.commons.io.*;version="[1.4,3.0)",
+ twitter4j.*;version="[2.1.0,2.2.0]",
+ javax.*;version="0",
+ org.w3c.dom.*;version="0"