From 7082b9e4cbaecac9c93c4b5f8596392a0fd712db Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 11 Apr 2014 16:12:38 +0300 Subject: [PATCH] INT-1939 Twitter Search Outbound Gateway JIRA: https://jira.spring.io/browse/INT-1939 Outbound gateway to allow on-demand variable searches. Also add the `page-size` attribute to inbound adapters (previously hard-coded to 20). Also the inbound adapters now `require` a `TwitterTemplate` because even search requires authentication. Polishing - PR Comments - Bump to spring-social-twitter 1.0.0.RC1 - Remove `requires-reply` - Polishing - Add integration test INT-1939 Doc and Rework Now supports up to 4 search args (as well as a SearchParameters). INT-1939: Polishing --- build.gradle | 2 +- spring-integration-twitter/.springBeans | 13 - .../TwitterInboundChannelAdapterParser.java | 15 +- .../config/TwitterNamespaceHandler.java | 7 +- .../TwitterSearchOutboundGatewayParser.java | 61 ++++ .../twitter/core/TwitterHeaders.java | 9 +- .../inbound/AbstractTwitterMessageSource.java | 16 ++ .../DirectMessageReceivingMessageSource.java | 5 +- .../MentionsReceivingMessageSource.java | 5 +- .../inbound/SearchReceivingMessageSource.java | 7 +- .../TimelineReceivingMessageSource.java | 5 +- .../TwitterSearchOutboundGateway.java | 151 ++++++++++ .../config/spring-integration-twitter-4.0.xsd | 148 ++++++++-- ...stReceivingMessageSourceParser-context.xml | 21 +- ...TestReceivingMessageSourceParserTests.java | 17 +- ...chReceivingMessageSourceParser-context.xml | 59 ++-- ...archReceivingMessageSourceParserTests.java | 20 +- .../TestSendingMessageHandlerParserTests.java | 16 +- ...archOutboundGatewayParserTests-context.xml | 39 +++ ...itterSearchOutboundGatewayParserTests.java | 81 ++++++ .../TestReceivingUsingNamespace-context.xml | 84 +++--- .../ignored/TestReceivingUsingNamespace.java | 13 +- .../TestSearchOutboundGateway-context.xml | 40 +++ .../ignored/TestSearchOutboundGateway.java | 52 ++++ .../twitter/ignored/TwitterAnnouncer.java | 22 +- .../TwitterSearchOutboundGatewayTests.java | 266 ++++++++++++++++++ src/reference/docbook/twitter.xml | 98 ++++++- src/reference/docbook/whats-new.xml | 9 + 28 files changed, 1106 insertions(+), 175 deletions(-) delete mode 100644 spring-integration-twitter/.springBeans create mode 100644 spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParser.java create mode 100644 spring-integration-twitter/src/main/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGateway.java create mode 100644 spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests-context.xml create mode 100644 spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests.java create mode 100644 spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway-context.xml create mode 100644 spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway.java create mode 100644 spring-integration-twitter/src/test/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGatewayTests.java diff --git a/build.gradle b/build.gradle index cf046d9e99..4f5153ede9 100644 --- a/build.gradle +++ b/build.gradle @@ -103,7 +103,7 @@ subprojects { subproject -> springDataRedisVersion = '1.2.1.RELEASE' springGemfireVersion = '1.3.1.RELEASE' springSecurityVersion = '3.1.3.RELEASE' - springSocialTwitterVersion = '1.1.0.M4' + springSocialTwitterVersion = '1.1.0.RC1' springRetryVersion = '1.0.3.RELEASE' springVersion = project.hasProperty('springVersion') ? project.springVersion : '4.0.3.RELEASE' springWsVersion = '2.1.1.RELEASE' diff --git a/spring-integration-twitter/.springBeans b/spring-integration-twitter/.springBeans deleted file mode 100644 index 6c6cb5a284..0000000000 --- a/spring-integration-twitter/.springBeans +++ /dev/null @@ -1,13 +0,0 @@ - - - 1 - - - - - - - - - - diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java index 9538f12796..13ade8e560 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors + * Copyright 2002-2014 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. @@ -27,13 +27,12 @@ import org.springframework.integration.twitter.inbound.DirectMessageReceivingMes import org.springframework.integration.twitter.inbound.MentionsReceivingMessageSource; import org.springframework.integration.twitter.inbound.SearchReceivingMessageSource; import org.springframework.integration.twitter.inbound.TimelineReceivingMessageSource; -import org.springframework.social.twitter.api.impl.TwitterTemplate; -import org.springframework.util.StringUtils; /** * Parser for inbound Twitter Channel Adapters. * * @author Oleg Zhurakousky + * @author Gary Russell * @since 2.0 */ public class TwitterInboundChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser { @@ -42,17 +41,11 @@ public class TwitterInboundChannelAdapterParser extends AbstractPollingInboundCh protected BeanMetadataElement parseSource(Element element, ParserContext parserContext) { Class clazz = determineClass(element, parserContext); BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(clazz); - String templateBeanName = element.getAttribute("twitter-template"); - if (StringUtils.hasText(templateBeanName)) { - builder.addConstructorArgReference(templateBeanName); - } - else { - BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(TwitterTemplate.class); - builder.addConstructorArgValue(templateBuilder.getBeanDefinition()); - } + builder.addConstructorArgReference(element.getAttribute("twitter-template")); builder.addConstructorArgValue(element.getAttribute(ID_ATTRIBUTE)); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "query"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "page-size"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "metadata-store"); return builder.getBeanDefinition(); } 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 index b0dcb6698b..63380e6466 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors + * Copyright 2002-2014 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. @@ -20,13 +20,15 @@ import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHa /** * Namespace handler for the Twitter adapters. - * + * * @author Josh Long * @author Oleg Zhurakousky + * @author Gary Russell * @since 2.0 */ public class TwitterNamespaceHandler extends AbstractIntegrationNamespaceHandler { + @Override public void init() { // inbound registerBeanDefinitionParser("inbound-channel-adapter", new TwitterInboundChannelAdapterParser()); @@ -37,6 +39,7 @@ public class TwitterNamespaceHandler extends AbstractIntegrationNamespaceHandler // outbound registerBeanDefinitionParser("outbound-channel-adapter", new TwitterOutboundChannelAdapterParser()); registerBeanDefinitionParser("dm-outbound-channel-adapter", new TwitterOutboundChannelAdapterParser()); + registerBeanDefinitionParser("search-outbound-gateway", new TwitterSearchOutboundGatewayParser()); } } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParser.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParser.java new file mode 100644 index 0000000000..24cb8ca879 --- /dev/null +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParser.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 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.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.ExpressionFactoryBean; +import org.springframework.integration.config.xml.AbstractConsumerEndpointParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.twitter.outbound.TwitterSearchOutboundGateway; +import org.springframework.util.StringUtils; + +/** + * Parser for {@code }. + * + * @author Gary Russell + * @since 4.0 + * + */ +public class TwitterSearchOutboundGatewayParser extends AbstractConsumerEndpointParser { + + @Override + protected String getInputChannelAttributeName() { + return "request-channel"; + } + + @Override + protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(TwitterSearchOutboundGateway.class); + builder.addConstructorArgReference(element.getAttribute("twitter-template")); + String searchArgsExpression = element.getAttribute("search-args-expression"); + if (StringUtils.hasText(searchArgsExpression)) { + BeanDefinition expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class); + expressionDef.getConstructorArgumentValues().addGenericArgumentValue(searchArgsExpression); + builder.addPropertyValue("searchArgsExpression", expressionDef); + } + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout", "sendTimeout"); + + return builder; + } + +} diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/core/TwitterHeaders.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/core/TwitterHeaders.java index f6cf5b0161..9b1b46cfc5 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/core/TwitterHeaders.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/core/TwitterHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors + * Copyright 2002-2014 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. @@ -21,12 +21,17 @@ package org.springframework.integration.twitter.core; * * @author Josh Long * @author Oleg Zhurakousky + * @author Gary Russell * @since 2.0 */ -public abstract class TwitterHeaders { +public final class TwitterHeaders { private static final String PREFIX = "twitter_"; public static final String DM_TARGET_USER_ID = PREFIX + "dmTargetUserId"; + public static final String SEARCH_METADATA = PREFIX + "searchMetadata"; + + private TwitterHeaders() {} + } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java index 40b30039ee..621cfea187 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java @@ -58,6 +58,8 @@ import org.springframework.util.StringUtils; @SuppressWarnings("rawtypes") abstract class AbstractTwitterMessageSource extends IntegrationObjectSupport implements MessageSource { + private static final int DEFAULT_PAGE_SIZE = 20; + private final Twitter twitter; private final TweetComparator tweetComparator = new TweetComparator(); @@ -76,6 +78,8 @@ abstract class AbstractTwitterMessageSource extends IntegrationObjectSupport private volatile long lastProcessedId = -1; + private volatile int pageSize = DEFAULT_PAGE_SIZE; + public AbstractTwitterMessageSource(Twitter twitter, String metadataKey) { Assert.notNull(twitter, "twitter must not be null"); @@ -104,6 +108,18 @@ abstract class AbstractTwitterMessageSource extends IntegrationObjectSupport return this.twitter; } + protected int getPageSize() { + return this.pageSize; + } + + /** + * Set the limit for the number of results returned on each poll; default 20. + * @param pageSize The pageSize. + */ + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + @Override protected void onInit() throws Exception { super.onInit(); diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java index 758302aa6e..91a33bf1f8 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -27,6 +27,7 @@ import org.springframework.social.twitter.api.Twitter; * @author Josh Long * @author Oleg Zhurakousky * @author Mark Fisher + * @author Gary Russell * @since 2.0 */ public class DirectMessageReceivingMessageSource extends AbstractTwitterMessageSource { @@ -42,7 +43,7 @@ public class DirectMessageReceivingMessageSource extends AbstractTwitterMessageS @Override protected List pollForTweets(long sinceId) { - return this.getTwitter().directMessageOperations().getDirectMessagesReceived(1, 20, sinceId, 0); + return this.getTwitter().directMessageOperations().getDirectMessagesReceived(1, this.getPageSize(), sinceId, 0); } } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java index 5551f3de1a..4c2e98e11c 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -26,6 +26,7 @@ import org.springframework.social.twitter.api.Twitter; * * @author Josh Long * @author Oleg Zhurakousky + * @author Gary Russell * @since 2.0 */ public class MentionsReceivingMessageSource extends AbstractTwitterMessageSource { @@ -41,7 +42,7 @@ public class MentionsReceivingMessageSource extends AbstractTwitterMessageSource @Override protected List pollForTweets(long sinceId) { - return this.getTwitter().timelineOperations().getMentions(20, sinceId, 0); + return this.getTwitter().timelineOperations().getMentions(this.getPageSize(), sinceId, 0); } } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java index 6014c69457..5b52dfd98d 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java @@ -19,16 +19,17 @@ package org.springframework.integration.twitter.inbound; import java.util.Collections; import java.util.List; +import org.springframework.social.twitter.api.SearchParameters; import org.springframework.social.twitter.api.SearchResults; import org.springframework.social.twitter.api.Tweet; import org.springframework.social.twitter.api.Twitter; -import org.springframework.social.twitter.api.SearchParameters; import org.springframework.util.Assert; /** * @author Oleg Zhurakousky * @author Mark Fisher * @author Gunnar Hillert + * @author Gary Russell * @since 2.0 */ public class SearchReceivingMessageSource extends AbstractTwitterMessageSource { @@ -46,13 +47,13 @@ public class SearchReceivingMessageSource extends AbstractTwitterMessageSource pollForTweets(long sinceId) { - SearchParameters searchParameters = new SearchParameters(query).count(20).sinceId(sinceId); + SearchParameters searchParameters = new SearchParameters(query).count(this.getPageSize()).sinceId(sinceId); SearchResults results = this.getTwitter().searchOperations().search(searchParameters); return (results != null) ? results.getTweets() : Collections.emptyList(); } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSource.java index 3349f6e392..42e68e070d 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2014 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. @@ -27,6 +27,7 @@ import org.springframework.social.twitter.api.Twitter; * * @author Josh Long * @author Oleg Zhurakousky + * @author Gary Russell * @since 2.0 */ public class TimelineReceivingMessageSource extends AbstractTwitterMessageSource { @@ -42,7 +43,7 @@ public class TimelineReceivingMessageSource extends AbstractTwitterMessageSource @Override protected List pollForTweets(long sinceId) { - return this.getTwitter().timelineOperations().getHomeTimeline(20, sinceId, 0); + return this.getTwitter().timelineOperations().getHomeTimeline(this.getPageSize(), sinceId, 0); } } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGateway.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGateway.java new file mode 100644 index 0000000000..ae7918c1e2 --- /dev/null +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGateway.java @@ -0,0 +1,151 @@ +/* + * Copyright 2014 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.outbound; + +import java.util.Collections; +import java.util.List; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.integration.expression.IntegrationEvaluationContextAware; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.twitter.core.TwitterHeaders; +import org.springframework.messaging.Message; +import org.springframework.social.twitter.api.SearchParameters; +import org.springframework.social.twitter.api.SearchResults; +import org.springframework.social.twitter.api.Tweet; +import org.springframework.social.twitter.api.Twitter; +import org.springframework.util.Assert; + +/** + * The {@link AbstractReplyProducingMessageHandler} implementation to perform request/reply + * Twitter search with {@link SearchParameters} as the result of {@link #searchArgsExpression} + * expression evaluation. + * + * @author Gary Russell + * @since 4.0 + * + */ +public class TwitterSearchOutboundGateway extends AbstractReplyProducingMessageHandler + implements IntegrationEvaluationContextAware { + + private static final int DEFAULT_PAGE_SIZE = 20; + + private final Twitter twitter; + + private volatile Expression searchArgsExpression = new SpelExpressionParser().parseExpression("payload"); + + private volatile EvaluationContext evaluationContext; + + public TwitterSearchOutboundGateway(Twitter twitter) { + Assert.notNull(twitter, "'twitter' must not be null"); + this.twitter = twitter; + } + + @Override + public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) { + TypeLocator typeLocator = evaluationContext.getTypeLocator(); + if (typeLocator instanceof StandardTypeLocator) { + /* + * Register the twitter api package so they don't need a FQCN for SearchParameters. + */ + ((StandardTypeLocator) typeLocator).registerImport("org.springframework.social.twitter.api"); + } + this.evaluationContext = evaluationContext; + } + + /** + * An expression that is used to build the search; must resolve to a + * {@code SearchParameters} object, or a + * {@link String}, in which case the default page size of 20 is applied, + * or a list of up to 4 arguments, such as + * {@code "{payload, headers.pageSize, headers.sinceId, headers.maxId}"}. + * The first (required) argument must resolve to a String (query), the + * optional arguments must resolve to an Number and represent the + * page size, sinceId, and maxId respectively. Refer to the 'Spring + * Social Twitter' documentation for more details. + *

When using a {@code SearchParameters} directly, it is not necessary + * to include the package: {@code "new SearchParameters("#foo").count(20)"). + *

Default: {@code "payload"}. + * @param searchArgsExpression The expression. + */ + public void setSearchArgsExpression(Expression searchArgsExpression) { + Assert.notNull(searchArgsExpression, "'searchArgsExpression' must not be null"); + this.searchArgsExpression = searchArgsExpression; + } + + @Override + public String getComponentType() { + return "twitter:search-outbound-gateway"; + } + + protected Twitter getTwitter() { + return twitter; + } + + @Override + protected Object handleRequestMessage(Message requestMessage) { + Object args = this.searchArgsExpression.getValue(this.evaluationContext, requestMessage); + Assert.notNull(args, "The twitter search expression cannot evaluate to 'null'."); + SearchParameters searchParameters; + if (args instanceof SearchParameters) { + searchParameters = (SearchParameters) args; + } + else if (args instanceof String) { + searchParameters = new SearchParameters((String) args).count(DEFAULT_PAGE_SIZE); + } + else if (args instanceof List) { + List list = (List) args; + Assert.isTrue(list.size() > 0 && list.size() < 5, "Between 1 and 4 search arguments are required"); + Assert.isInstanceOf(String.class, list.get(0), "The first search argument (query) must be a String"); + searchParameters = new SearchParameters((String) list.get(0)); + if (list.size() > 1) { + Assert.isInstanceOf(Number.class, list.get(1), + "The second search argument (pageSize) must be a Number"); + searchParameters.count(((Number) list.get(1)).intValue()); + if (list.size() > 2) { + Assert.isInstanceOf(Number.class, list.get(2), + "The third search argument (sinceId) must be a Number"); + searchParameters.sinceId(((Number) list.get(2)).longValue()); + } + if (list.size() > 3) { + Assert.isInstanceOf(Number.class, list.get(3), + "The fourth search argument (maxId) must be a Number"); + searchParameters.maxId(((Number) list.get(3)).longValue()); + } + } + } + else { + throw new IllegalArgumentException( + "Search Expression must evaluate to a 'SearchParameters', 'String' or 'List'."); + } + SearchResults results = this.getTwitter().searchOperations().search(searchParameters); + if (results != null) { + List tweets = (results.getTweets() != null ? results.getTweets() : Collections.emptyList()); + return this.getMessageBuilderFactory().withPayload(tweets) + .setHeader(TwitterHeaders.SEARCH_METADATA, results.getSearchMetadata()); + } + else { + return null; + } + + } + +} diff --git a/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-4.0.xsd b/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-4.0.xsd index b2b9856daf..5fc97d0120 100644 --- a/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-4.0.xsd +++ b/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-4.0.xsd @@ -96,7 +96,9 @@ - + + + @@ -110,7 +112,94 @@ - + + + + + + + + + + + + + + + + + + The bean id of this gateway; the MessageHandler is also registered with this id + plus a suffix '.handler'. + + + + + + + A SpEL expression that evaluates to search arguments; the evaluation result type can be + an 'org.springframework.social.twitter.api.SearchParameters', a 'String', in + which case the default page size of 20 is used, or the expression can evaluate to + a list of search + arguments, for example: "{payload, headers.pageSize, headers.sinceId, headers.maxId}". + Default: "payload". + + + + + + + + + + + + Identifies the request channel attached to this gateway. + + + + + + + + + + + + Identifies the reply channel attached to this + gateway. + + + + + + + + + + @@ -145,7 +234,7 @@ - + @@ -172,32 +261,39 @@ + + + + Limits the number of tweets retrieved on each poll; default: 20. + + + - + - - - - - - - - - - - Reference to a TwitterTemplate bean provided by the Spring Social project. - - - - - - - Specifies the order for invocation when this endpoint is connected as a - subscriber to a SubscribableChannel. - - - + + + + + + + + + + + Reference to a TwitterTemplate bean provided by the Spring Social project. + + + + + + + Specifies the order for invocation when this endpoint is connected as a + subscriber to a SubscribableChannel. + + + diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParser-context.xml b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParser-context.xml index fdb1b43838..14212c9e08 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParser-context.xml +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParser-context.xml @@ -23,22 +23,25 @@ - - - diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java index ec97511362..54de8046a9 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,11 +16,12 @@ package org.springframework.integration.twitter.config; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import org.junit.Test; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.integration.endpoint.SourcePollingChannelAdapter; import org.springframework.integration.test.util.TestUtils; @@ -32,30 +33,37 @@ import org.springframework.integration.twitter.inbound.TimelineReceivingMessageS /** * @author Oleg Zhurakousky * @author Gunnar Hillert + * @author Gary Russell */ public class TestReceivingMessageSourceParserTests { @Test public void testReceivingAdapterConfigurationAutoStartup(){ - ApplicationContext ac = new ClassPathXmlApplicationContext("TestReceivingMessageSourceParser-context.xml", this.getClass()); + ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext( + "TestReceivingMessageSourceParser-context.xml", this.getClass()); SourcePollingChannelAdapter spca = ac.getBean("mentionAdapter", SourcePollingChannelAdapter.class); MentionsReceivingMessageSource ms = TestUtils.getPropertyValue(spca, "source", MentionsReceivingMessageSource.class); + assertEquals(Integer.valueOf(23), TestUtils.getPropertyValue(ms, "pageSize", Integer.class)); assertNotNull(ms); spca = ac.getBean("dmAdapter", SourcePollingChannelAdapter.class); DirectMessageReceivingMessageSource dms = TestUtils.getPropertyValue(spca, "source", DirectMessageReceivingMessageSource.class); assertNotNull(dms); + assertEquals(Integer.valueOf(45), TestUtils.getPropertyValue(dms, "pageSize", Integer.class)); spca = ac.getBean("updateAdapter", SourcePollingChannelAdapter.class); spca = ac.getBean("updateAdapter", SourcePollingChannelAdapter.class); TimelineReceivingMessageSource tms = TestUtils.getPropertyValue(spca, "source", TimelineReceivingMessageSource.class); + assertEquals(Integer.valueOf(67), TestUtils.getPropertyValue(tms, "pageSize", Integer.class)); assertNotNull(tms); + ac.close(); } @Test public void testThatMessageSourcesAreRegisteredAsBeans(){ - ApplicationContext ac = new ClassPathXmlApplicationContext("TestReceivingMessageSourceParser-context.xml", this.getClass()); + ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext( + "TestReceivingMessageSourceParser-context.xml", this.getClass()); MentionsReceivingMessageSource ms = ac.getBean("mentionAdapter.source", MentionsReceivingMessageSource.class); assertNotNull(ms); @@ -65,6 +73,7 @@ public class TestReceivingMessageSourceParserTests { TimelineReceivingMessageSource tms = ac.getBean("updateAdapter.source", TimelineReceivingMessageSource.class); assertNotNull(tms); + ac.close(); } } diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParser-context.xml b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParser-context.xml index 534c7a7a7f..9c79a93d05 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParser-context.xml +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParser-context.xml @@ -1,44 +1,39 @@ - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParserTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParserTests.java index 6e7d3ef38a..6213856251 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParserTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSearchReceivingMessageSourceParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2014 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. @@ -16,30 +16,34 @@ package org.springframework.integration.twitter.config; -import org.junit.Ignore; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + import org.junit.Test; -import org.springframework.context.ApplicationContext; + +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.integration.endpoint.SourcePollingChannelAdapter; import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.twitter.inbound.SearchReceivingMessageSource; import org.springframework.social.twitter.api.Twitter; -import static org.junit.Assert.assertNotNull; - /** * @author Oleg Zhurakousky + * @author Gary Russell */ public class TestSearchReceivingMessageSourceParserTests { @Test - @Ignore // because userOpoeration.getProfile() throws exception where it doesn't have to since its a search public void testSearchReceivingDefaultTemplate(){ - ApplicationContext ac = new ClassPathXmlApplicationContext("TestSearchReceivingMessageSourceParser-context.xml", this.getClass()); - SourcePollingChannelAdapter spca = ac.getBean("searchAdapter", SourcePollingChannelAdapter.class); + ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext( + "TestSearchReceivingMessageSourceParser-context.xml", this.getClass()); + SourcePollingChannelAdapter spca = ac.getBean("searchAdapterWithTemplate", SourcePollingChannelAdapter.class); SearchReceivingMessageSource ms = (SearchReceivingMessageSource) TestUtils.getPropertyValue(spca, "source"); + assertEquals(Integer.valueOf(23), TestUtils.getPropertyValue(ms, "pageSize", Integer.class)); Twitter template = (Twitter) TestUtils.getPropertyValue(ms, "twitter"); assertNotNull(template); + ac.close(); } } diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSendingMessageHandlerParserTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSendingMessageHandlerParserTests.java index a65c371840..45d4a5ad14 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSendingMessageHandlerParserTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestSendingMessageHandlerParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -22,22 +22,24 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice; -import org.springframework.messaging.support.GenericMessage; import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.twitter.outbound.DirectMessageSendingMessageHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.support.GenericMessage; /** * @author Oleg Zhurakousky * @author Gary Russell * @author Artem Bilan + * @author Gary Russell * @since 2.0 */ public class TestSendingMessageHandlerParserTests { @@ -46,7 +48,8 @@ public class TestSendingMessageHandlerParserTests { @Test public void testSendingMessageHandlerSuccessfulBootstrap(){ - ApplicationContext ac = new ClassPathXmlApplicationContext("TestSendingMessageHandlerParser-context.xml", this.getClass()); + ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext( + "TestSendingMessageHandlerParser-context.xml", this.getClass()); EventDrivenConsumer dmAdapter = ac.getBean("dmAdapter", EventDrivenConsumer.class); MessageHandler handler = TestUtils.getPropertyValue(dmAdapter, "handler", MessageHandler.class); assertEquals(DirectMessageSendingMessageHandler.class, handler.getClass()); @@ -59,6 +62,7 @@ public class TestSendingMessageHandlerParserTests { assertNotSame(handler, handler2); handler2.handleMessage(new GenericMessage("foo")); assertEquals(2, adviceCalled); + ac.close(); } @Test diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests-context.xml b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests-context.xml new file mode 100644 index 0000000000..6c4d15a455 --- /dev/null +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests.java new file mode 100644 index 0000000000..a4eaf1c28c --- /dev/null +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TwitterSearchOutboundGatewayParserTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2014 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.endpoint.PollingConsumer; +import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.twitter.outbound.TwitterSearchOutboundGateway; +import org.springframework.social.twitter.api.Twitter; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Gary Russell + * @since 4.0 + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext +public class TwitterSearchOutboundGatewayParserTests { + + @Autowired + @Qualifier("defaultTSOG.handler") + private TwitterSearchOutboundGateway defaultTSOG; + + @Autowired + @Qualifier("allAttsTSOG.handler") + private TwitterSearchOutboundGateway allAttsTSOG; + + @Autowired + private PollingConsumer polledAndAdvisedTSOG; + + @Autowired + private Twitter twitter; + + @Test + public void testDefault() { + assertSame(twitter, TestUtils.getPropertyValue(defaultTSOG, "twitter")); + } + + @Test + public void testAllAtts() { + assertSame(twitter, TestUtils.getPropertyValue(allAttsTSOG, "twitter")); + assertEquals("'foo'", TestUtils.getPropertyValue(allAttsTSOG, "searchArgsExpression.expression")); + } + + @Test + public void testAdvised() { + assertSame(twitter, TestUtils.getPropertyValue(polledAndAdvisedTSOG, "handler.twitter")); + assertThat(TestUtils.getPropertyValue(polledAndAdvisedTSOG, "handler.adviceChain", ArrayList.class).get(0), + Matchers.instanceOf(RequestHandlerRetryAdvice.class)); + } + +} diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace-context.xml b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace-context.xml index 4fafcdd8d6..a415a48789 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace-context.xml +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace-context.xml @@ -1,60 +1,60 @@ - + - - - - - - + + + + + + - - - - + + + + - - - - - + + + - - - - - - - - - + - - - - + + + + + + + + + + + + + + + - diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace.java index cfb80691a1..b5e5b5a535 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestReceivingUsingNamespace.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2014 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. @@ -20,10 +20,13 @@ import java.util.concurrent.TimeUnit; import org.junit.Ignore; import org.junit.Test; + +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author Oleg Zhurakousky + * @author Gary Russell * */ public class TestReceivingUsingNamespace { @@ -31,13 +34,13 @@ public class TestReceivingUsingNamespace { @Test @Ignore /* - * In order to run this test you need to provide values to the twitter.properties file + * In order to run this test you need to provide oauth properties in sample.properties on the classpath. */ public void testUpdatesWithRealTwitter() throws Exception{ CountDownLatch latch = new CountDownLatch(1); - new ClassPathXmlApplicationContext("TestReceivingUsingNamespace-context.xml", this.getClass()); - System.out.println("done"); + ConfigurableApplicationContext ctx = + new ClassPathXmlApplicationContext("TestReceivingUsingNamespace-context.xml", this.getClass()); latch.await(10000, TimeUnit.SECONDS); - + ctx.close(); } } diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway-context.xml b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway-context.xml new file mode 100644 index 0000000000..40e3678d84 --- /dev/null +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway-context.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway.java new file mode 100644 index 0000000000..fea4a0be51 --- /dev/null +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TestSearchOutboundGateway.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014 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.ignored; + +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.social.twitter.api.SearchParameters; + +/** + * @author Gary Russell + * + * @since 4.0 + * + */ +public class TestSearchOutboundGateway { + + @Test + @Ignore + /* + * In order to run this test you need to provide oauth properties in sample.properties on the classpath. + */ + public void testSearch() throws Exception{ + ConfigurableApplicationContext ctx = + new ClassPathXmlApplicationContext("TestSearchOutboundGateway-context.xml", this.getClass()); + MessageChannel search = ctx.getBean("search", MessageChannel.class); + search.send(new GenericMessage("#springintegration")); + Thread.sleep(10000); + search.send(new GenericMessage(new SearchParameters("#springintegration").count(5))); + Thread.sleep(10000); + search.send(new GenericMessage(new SearchParameters("#jjjjunk").count(5))); + Thread.sleep(10000); + ctx.close(); + } +} diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TwitterAnnouncer.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TwitterAnnouncer.java index e005d72d8a..a808938898 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TwitterAnnouncer.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/ignored/TwitterAnnouncer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2014 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. @@ -16,12 +16,20 @@ package org.springframework.integration.twitter.ignored; -import org.springframework.messaging.Message; +import java.util.Collection; + import org.springframework.integration.history.MessageHistory; +import org.springframework.messaging.Message; import org.springframework.social.twitter.api.DirectMessage; import org.springframework.social.twitter.api.Tweet; import org.springframework.stereotype.Component; +/** + * @author Oleg Zhurakousky + * @author Mark Fisher + * @author Gary Russell + * + */ @Component public class TwitterAnnouncer { @@ -43,6 +51,16 @@ public class TwitterAnnouncer { + s.getFromUser() + "-" + s.getText() + " from " + s.getSource()); } + public void searchResult(Collection tweets) { + if (tweets.size() == 0) { + System.out.println("No results"); + } + for (Tweet s : tweets) { + System.out.println("Search result: " + + s.getFromUser() + "-" + s.getText() + " from " + s.getSource()); + } + } + public void updates(Tweet t) { System.out.println("Received timeline update: " + t.getText() + " from " + t.getSource()); } diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGatewayTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGatewayTests.java new file mode 100644 index 0000000000..b83bfd2e8e --- /dev/null +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/outbound/TwitterSearchOutboundGatewayTests.java @@ -0,0 +1,266 @@ +/* + * Copyright 2014 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.outbound; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.twitter.core.TwitterHeaders; +import org.springframework.integration.twitter.outbound.TwitterSearchOutboundGatewayTests.TwitterConfig; +import org.springframework.messaging.Message; +import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.social.twitter.api.SearchMetadata; +import org.springframework.social.twitter.api.SearchOperations; +import org.springframework.social.twitter.api.SearchParameters; +import org.springframework.social.twitter.api.SearchResults; +import org.springframework.social.twitter.api.Tweet; +import org.springframework.social.twitter.api.Twitter; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Gary Russell + * @since 4.0 + * + */ +@ContextConfiguration(classes=TwitterConfig.class) +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD) +public class TwitterSearchOutboundGatewayTests { + + @Autowired + private SearchOperations searchOps; + + @Autowired + private TwitterSearchOutboundGateway gateway; + + @Autowired + private PollableChannel outputChannel; + + @Test + public void testStringQuery() { + Tweet tweet = new Tweet(1L, "foo", new Date(), "bar", "baz", 0L, 0L, "qux", "fiz"); + SearchMetadata searchMetadata = mock(SearchMetadata.class); + final SearchResults searchResults = new SearchResults(Collections.singletonList(tweet), searchMetadata); + doAnswer(new Answer() { + + @Override + public SearchResults answer(InvocationOnMock invocation) throws Throwable { + SearchParameters searchParameters = (SearchParameters) invocation.getArguments()[0]; + assertEquals("foo", searchParameters.getQuery()); + assertEquals(Integer.valueOf(20), searchParameters.getCount()); + return searchResults; + } + }).when(this.searchOps).search(Matchers.any(SearchParameters.class)); + this.gateway.handleMessage(new GenericMessage("foo")); + Message reply = this.outputChannel.receive(0); + assertNotNull(reply); + @SuppressWarnings("unchecked") + List tweets = (List) reply.getPayload(); + assertEquals(1, tweets.size()); + assertSame(tweet, tweets.get(0)); + assertSame(searchMetadata, reply.getHeaders().get(TwitterHeaders.SEARCH_METADATA)); + } + + @Test + public void testStringQueryCustomLimit() { + this.gateway.setSearchArgsExpression(new SpelExpressionParser() + .parseExpression("{payload, 30}")); + Tweet tweet = new Tweet(1L, "foo", new Date(), "bar", "baz", 0L, 0L, "qux", "fiz"); + SearchMetadata searchMetadata = mock(SearchMetadata.class); + final SearchResults searchResults = new SearchResults(Collections.singletonList(tweet), searchMetadata); + doAnswer(new Answer() { + + @Override + public SearchResults answer(InvocationOnMock invocation) throws Throwable { + SearchParameters searchParameters = (SearchParameters) invocation.getArguments()[0]; + assertEquals("foo", searchParameters.getQuery()); + assertEquals(Integer.valueOf(30), searchParameters.getCount()); + return searchResults; + } + }).when(this.searchOps).search(Matchers.any(SearchParameters.class)); + this.gateway.handleMessage(new GenericMessage("foo")); + Message reply = this.outputChannel.receive(0); + assertNotNull(reply); + @SuppressWarnings("unchecked") + List tweets = (List) reply.getPayload(); + assertEquals(1, tweets.size()); + assertSame(tweet, tweets.get(0)); + assertSame(searchMetadata, reply.getHeaders().get(TwitterHeaders.SEARCH_METADATA)); + } + + @Test + public void testStringQueryCustomExpression() { + this.gateway.setSearchArgsExpression(new SpelExpressionParser() + .parseExpression("{'bar', 1, 2, 3}")); + Tweet tweet = new Tweet(1L, "foo", new Date(), "bar", "baz", 0L, 0L, "qux", "fiz"); + SearchMetadata searchMetadata = mock(SearchMetadata.class); + final SearchResults searchResults = new SearchResults(Collections.singletonList(tweet), searchMetadata); + doAnswer(new Answer() { + + @Override + public SearchResults answer(InvocationOnMock invocation) throws Throwable { + SearchParameters searchParameters = (SearchParameters) invocation.getArguments()[0]; + assertEquals("bar", searchParameters.getQuery()); + assertEquals(Integer.valueOf(1), searchParameters.getCount()); + assertEquals(Long.valueOf(2), searchParameters.getSinceId()); + assertEquals(Long.valueOf(3), searchParameters.getMaxId()); + return searchResults; + } + }).when(this.searchOps).search(Matchers.any(SearchParameters.class)); + this.gateway.handleMessage(new GenericMessage("foo")); + Message reply = this.outputChannel.receive(0); + assertNotNull(reply); + @SuppressWarnings("unchecked") + List tweets = (List) reply.getPayload(); + assertEquals(1, tweets.size()); + assertSame(tweet, tweets.get(0)); + assertSame(searchMetadata, reply.getHeaders().get(TwitterHeaders.SEARCH_METADATA)); + } + + @Test + public void testSearchParamsQuery() { + Tweet tweet = new Tweet(1L, "foo", new Date(), "bar", "baz", 0L, 0L, "qux", "fiz"); + SearchMetadata searchMetadata = mock(SearchMetadata.class); + final SearchResults searchResults = new SearchResults(Collections.singletonList(tweet), searchMetadata); + final SearchParameters parameters = new SearchParameters("bar"); + doAnswer(new Answer() { + + @Override + public SearchResults answer(InvocationOnMock invocation) throws Throwable { + SearchParameters searchParameters = (SearchParameters) invocation.getArguments()[0]; + assertSame(parameters, searchParameters); + return searchResults; + } + }).when(this.searchOps).search(Matchers.any(SearchParameters.class)); + this.gateway.handleMessage(new GenericMessage(parameters)); + Message reply = this.outputChannel.receive(0); + assertNotNull(reply); + @SuppressWarnings("unchecked") + List tweets = (List) reply.getPayload(); + assertEquals(1, tweets.size()); + assertSame(tweet, tweets.get(0)); + assertSame(searchMetadata, reply.getHeaders().get(TwitterHeaders.SEARCH_METADATA)); + } + + @Test + public void testSearchParamsQueryCustomExpression() { + this.gateway.setSearchArgsExpression(new SpelExpressionParser() + .parseExpression("new SearchParameters('foo' + payload).count(5).sinceId(11)")); + Tweet tweet = new Tweet(1L, "foo", new Date(), "bar", "baz", 0L, 0L, "qux", "fiz"); + SearchMetadata searchMetadata = mock(SearchMetadata.class); + final SearchResults searchResults = new SearchResults(Collections.singletonList(tweet), searchMetadata); + doAnswer(new Answer() { + + @Override + public SearchResults answer(InvocationOnMock invocation) throws Throwable { + SearchParameters searchParameters = (SearchParameters) invocation.getArguments()[0]; + assertEquals("foobar", searchParameters.getQuery()); + assertEquals(Integer.valueOf(5), searchParameters.getCount()); + assertEquals(Long.valueOf(11), searchParameters.getSinceId()); + return searchResults; + } + }).when(this.searchOps).search(Matchers.any(SearchParameters.class)); + this.gateway.handleMessage(new GenericMessage("bar")); + Message reply = this.outputChannel.receive(0); + assertNotNull(reply); + @SuppressWarnings("unchecked") + List tweets = (List) reply.getPayload(); + assertEquals(1, tweets.size()); + assertSame(tweet, tweets.get(0)); + assertSame(searchMetadata, reply.getHeaders().get(TwitterHeaders.SEARCH_METADATA)); + } + + @Test + public void testEmptyResult() { + SearchMetadata searchMetadata = mock(SearchMetadata.class); + List empty = new ArrayList(0); + final SearchResults searchResults = new SearchResults(empty, searchMetadata); + doAnswer(new Answer() { + + @Override + public SearchResults answer(InvocationOnMock invocation) throws Throwable { + SearchParameters searchParameters = (SearchParameters) invocation.getArguments()[0]; + assertEquals("foo", searchParameters.getQuery()); + assertEquals(Integer.valueOf(20), searchParameters.getCount()); + return searchResults; + } + }).when(this.searchOps).search(Matchers.any(SearchParameters.class)); + this.gateway.handleMessage(new GenericMessage("foo")); + Message reply = this.outputChannel.receive(0); + assertNotNull(reply); + @SuppressWarnings("unchecked") + List tweets = (List) reply.getPayload(); + assertEquals(0, tweets.size()); + assertSame(searchMetadata, reply.getHeaders().get(TwitterHeaders.SEARCH_METADATA)); + } + + @Configuration + @EnableIntegration + public static class TwitterConfig { + + @Bean + public TwitterSearchOutboundGateway gateway() { + TwitterSearchOutboundGateway gateway = new TwitterSearchOutboundGateway(twitter()); + gateway.setOutputChannel(outputChannel()); + return gateway; + } + + @Bean + public PollableChannel outputChannel() { + return new QueueChannel(); + } + + @Bean + public Twitter twitter() { + Twitter twitter = mock(Twitter.class); + when(twitter.searchOperations()).thenReturn(searchOps()); + return twitter; + } + + @Bean + public SearchOperations searchOps() { + return mock(SearchOperations.class); + } + + } + +} diff --git a/src/reference/docbook/twitter.xml b/src/reference/docbook/twitter.xml index 0555ce44e5..7e2fe2c3ee 100644 --- a/src/reference/docbook/twitter.xml +++ b/src/reference/docbook/twitter.xml @@ -2,11 +2,12 @@ - Twitter Adapter + Twitter Support Spring Integration provides support for interacting with Twitter. With the Twitter adapters you can both receive and send Twitter messages. You can also perform a Twitter search based on a schedule and publish - the search results within Messages. + the search results within Messages. Since version 4.0, a search outbound + gateway is provided to perform dynamic searches.

@@ -21,6 +22,8 @@ Versions of Spring Integration prior to 2.1 were dependent upon the Twitter4J API, but with the release of Spring Social 1.0 GA, Spring Integration, as of version 2.1, now builds directly upon Spring Social's Twitter support, instead of Twitter4J. + All Twitter endpoints require the configuration of a TwitterTemplate because even + search operations require an authenticated template. @@ -37,7 +40,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter Twitter OAuth Configuration - The Twitter API allows for both authenticated and anonymous operations. For authenticated operations Twitter uses OAuth + For authenticated operations, Twitter uses OAuth - an authentication protocol that allows users to approve an application to act on their behalf without sharing their password. More information can be found at http://oauth.net or in this article http://hueniverse.com/oauth from Hueniverse. @@ -156,6 +159,10 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]>id attribute of the Twitter Inbound Channel Adapter component plus the profileId of the Twitter user. + + Prior to version 4.0, the page size was hard-coded to 20. This is now configurable + using the page-size attribute (defaults to 20). +
Inbound Message Channel Adapter @@ -291,4 +298,89 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]>
+
+ Twitter Search Outbound Gateway + + In Spring Integration, an outbound gateway is used for two-way request/response communication with + an external service. The Twitter Search Outbound Gateway allows you to issue dynamic twitter + searches. The reply message payload is a collection of Tweet objects. + If the search returns no results, the payload is an empty collection. You can limit the number + of tweets and you can page through a larger set of tweets by making multiple calls. To facilitate this, search + reply messages contain a header twitter_searchMetadata with its value being + a SearchMetadata object. For more information + on the Tweet, SearchParameters and + SearchMetadata classes, refer to the Spring Social Twitter + documentation. + + + Configuring the Outbound Gateway + + +]]> + + + The channel used to send search requests to this gateway. + + + A reference to a TwitterTemplate with authentication configuration. + + + + A SpEL expression that evaluates to argument(s) for the search. Default: + "payload" - in which case the payload can be a String + (e.g "#springintegration") and the gateway limits the query to 20 tweets, or the payload can be a + SearchParameters object. + + + The expression can also be specified as a SpEL List. The first element (String) is the query, the remaining elements (Numbers) + are pageSize, sinceId, maxId respectively - refer to the Spring Social Twitter + documentation for more information about these parameters. + When specifying a SearchParameters object directly in the SpEL + expression, you do not have to fully qualify the class name. Some examples: + "new SearchParameters(payload).count(5).sinceId(headers.sinceId)" +"{payload, 30}" +"{payload, headers.pageSize, headers.sinceId, headers.maxId}" + + + + + The channel to which to send the reply; if omitted, the replyChannel header + is used. + + + + + The timeout when sending the reply message to the reply channel; only applies if the reply + channel can block, for example a bounded queue channel that is full. + + + + + When subscribed to a publish/subscribe channel, the order in which this endpoint will + be invoked. + + + + + SmartLifecycle method. + + + + + SmartLifecycle method. + + + +
diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 6ba9b4b459..9c19451999 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -163,6 +163,15 @@ For more information, see .
+
+ Twitter Search Outbound Gateway + + A new twitter endpoint <int-twitter-search-outbound-gateway/> + has been added. Unlike the search inbound adapter which polls using the same search + query each time, the outbound gateway allows on-demand customized queries. + For more information, see . + +