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"