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
This commit is contained in:
Gary Russell
2014-04-11 16:12:38 +03:00
committed by Artem Bilan
parent 4577200e66
commit 7082b9e4cb
28 changed files with 1106 additions and 175 deletions

View File

@@ -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'

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beansProjectDescription>
<version>1</version>
<pluginVersion><![CDATA[2.5.0.201010221000-RELEASE]]></pluginVersion>
<configSuffixes>
<configSuffix><![CDATA[xml]]></configSuffix>
</configSuffixes>
<enableImports><![CDATA[false]]></enableImports>
<configs>
</configs>
<configSets>
</configSets>
</beansProjectDescription>

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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 <int-twitter:search-outbound-gateway/>}.
*
* @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;
}
}

View File

@@ -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() {}
}

View File

@@ -58,6 +58,8 @@ import org.springframework.util.StringUtils;
@SuppressWarnings("rawtypes")
abstract class AbstractTwitterMessageSource<T> 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<T> 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<T> 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();

View File

@@ -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<DirectMessage> {
@@ -42,7 +43,7 @@ public class DirectMessageReceivingMessageSource extends AbstractTwitterMessageS
@Override
protected List<DirectMessage> pollForTweets(long sinceId) {
return this.getTwitter().directMessageOperations().getDirectMessagesReceived(1, 20, sinceId, 0);
return this.getTwitter().directMessageOperations().getDirectMessagesReceived(1, this.getPageSize(), sinceId, 0);
}
}

View File

@@ -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<Tweet> {
@@ -41,7 +42,7 @@ public class MentionsReceivingMessageSource extends AbstractTwitterMessageSource
@Override
protected List<Tweet> pollForTweets(long sinceId) {
return this.getTwitter().timelineOperations().getMentions(20, sinceId, 0);
return this.getTwitter().timelineOperations().getMentions(this.getPageSize(), sinceId, 0);
}
}

View File

@@ -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<Tweet> {
@@ -46,13 +47,13 @@ public class SearchReceivingMessageSource extends AbstractTwitterMessageSource<T
}
@Override
public String getComponentType() {
public String getComponentType() {
return "twitter:search-inbound-channel-adapter";
}
@Override
protected List<Tweet> 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.<Tweet>emptyList();
}

View File

@@ -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<Tweet> {
@@ -42,7 +43,7 @@ public class TimelineReceivingMessageSource extends AbstractTwitterMessageSource
@Override
protected List<Tweet> pollForTweets(long sinceId) {
return this.getTwitter().timelineOperations().getHomeTimeline(20, sinceId, 0);
return this.getTwitter().timelineOperations().getHomeTimeline(this.getPageSize(), sinceId, 0);
}
}

View File

@@ -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.
* <p> When using a {@code SearchParameters} directly, it is not necessary
* to include the package: {@code "new SearchParameters("#foo").count(20)").
* <p> 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<Tweet> tweets = (results.getTweets() != null ? results.getTweets() : Collections.<Tweet>emptyList());
return this.getMessageBuilderFactory().withPayload(tweets)
.setHeader(TwitterHeaders.SEARCH_METADATA, results.getSearchMetadata());
}
else {
return null;
}
}
}

View File

@@ -96,7 +96,9 @@
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="outbound-twitter-type"/>
<xsd:extension base="outbound-twitter-type">
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
@@ -110,7 +112,94 @@
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="outbound-twitter-type"/>
<xsd:extension base="outbound-twitter-type">
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="search-outbound-gateway">
<xsd:annotation>
<xsd:documentation><![CDATA[
Builds an outbound gateway used to issue Twitter searches.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="outbound-twitter-type">
<xsd:attribute name="id" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
The bean id of this gateway; the MessageHandler is also registered with this id
plus a suffix '.handler'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="search-args-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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".
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-channel" use="required" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.messaging.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies the request channel attached to this gateway.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.messaging.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies the reply channel attached to this
gateway.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-timeout" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Allows you to specify how long this gateway will wait for
the reply message to be sent successfully to the reply channel
before throwing an exception. This attribute only applies when the
channel might block, for example when using a bounded queue channel that
is currently full.
Also, keep in mind that when sending to a DirectChannel, the
invocation will occur in the sender's thread. Therefore,
the failing of the send operation may be caused by other
components further downstream.
The "reply-timeout" attribute maps to the "sendTimeout" property of the
underlying 'MessagingTemplate' instance (org.springframework.integration.core.MessagingTemplate).
The attribute will default, if not specified, to '-1', meaning that
by default, the Gateway will wait indefinitely. The value is
specified in milliseconds.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
@@ -145,7 +234,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
<xsd:attribute name="twitter-template" type="xsd:string">
<xsd:attribute name="twitter-template" type="xsd:string" use="required">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
@@ -172,32 +261,39 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="page-size" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Limits the number of tweets retrieved on each poll; default: 20.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="outbound-twitter-type">
<xsd:all>
<xsd:choice minOccurs="0" maxOccurs="2">
<xsd:element name="request-handler-advice-chain" type="integration:handlerAdviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
<xsd:attribute name="twitter-template" use="required" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.social.twitter.api.Twitter"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Reference to a TwitterTemplate bean provided by the Spring Social project.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order">
<xsd:annotation>
<xsd:documentation>
Specifies the order for invocation when this endpoint is connected as a
subscriber to a SubscribableChannel.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
</xsd:choice>
<xsd:attribute name="twitter-template" use="required" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.social.twitter.api.Twitter"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Reference to a TwitterTemplate bean provided by the Spring Social project.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order">
<xsd:annotation>
<xsd:documentation>
Specifies the order for invocation when this endpoint is connected as a
subscriber to a SubscribableChannel.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:schema>

View File

@@ -23,22 +23,25 @@
<channel id="inbound_mentions"/>
<twitter:mentions-inbound-channel-adapter id="mentionAdapter"
twitter-template="twitter"
channel="inbound_mentions"
<twitter:mentions-inbound-channel-adapter id="mentionAdapter"
twitter-template="twitter"
channel="inbound_mentions"
page-size="23"
auto-startup="false">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:mentions-inbound-channel-adapter>
<twitter:dm-inbound-channel-adapter id="dmAdapter"
twitter-template="twitter"
channel="inbound_mentions"
<twitter:dm-inbound-channel-adapter id="dmAdapter"
twitter-template="twitter"
channel="inbound_mentions"
page-size="45"
auto-startup="false">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:dm-inbound-channel-adapter>
<twitter:inbound-channel-adapter id="updateAdapter"
twitter-template="twitter"
channel="inbound_mentions"
<twitter:inbound-channel-adapter id="updateAdapter"
twitter-template="twitter"
channel="inbound_mentions"
page-size="67"
auto-startup="false">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:inbound-channel-adapter>

View File

@@ -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();
}
}

View File

@@ -1,44 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
xmlns="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
xmlns="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
http://www.springframework.org/schema/integration/twitter http://www.springframework.org/schema/integration/twitter/spring-integration-twitter.xsd">
<channel id="searchChannel"/>
<twitter:search-inbound-channel-adapter id="searchAdapter"
channel="searchChannel"
query="#springintegration"
auto-startup="false">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:search-inbound-channel-adapter>
<beans:bean id="twitter" class="org.springframework.integration.twitter.config.TestReceivingMessageSourceParserTests.TwitterTemplateFactoryBean"/>
<twitter:search-inbound-channel-adapter id="searchAdapterWithTemplate"
channel="searchChannel"
twitter-template="twitter"
query="#springintegration"
auto-startup="false">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:search-inbound-channel-adapter>
<beans:bean id="twitter" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.springframework.social.twitter.api.Twitter" />
</beans:bean>
<twitter:search-inbound-channel-adapter id="searchAdapterWithTemplate"
channel="searchChannel"
twitter-template="twitter"
page-size="23"
query="#springintegration"
auto-startup="false">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:search-inbound-channel-adapter>
</beans:beans>

View File

@@ -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();
}
}

View File

@@ -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<String>("foo"));
assertEquals(2, adviceCalled);
ac.close();
}
@Test

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/twitter http://www.springframework.org/schema/integration/twitter/spring-integration-twitter.xsd">
<bean id="tt" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.social.twitter.api.Twitter" />
</bean>
<int:channel id="in" />
<int-twitter:search-outbound-gateway id="defaultTSOG" twitter-template="tt" request-channel="in" />
<int-twitter:search-outbound-gateway id="allAttsTSOG"
request-channel="in"
twitter-template="tt"
search-args-expression="'foo'"
reply-channel="out"
order="23"
reply-timeout="123"
auto-startup="false"
phase="100" />
<int-twitter:search-outbound-gateway id="polledAndAdvisedTSOG" twitter-template="tt" request-channel="out">
<int-twitter:request-handler-advice-chain>
<bean class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice" />
</int-twitter:request-handler-advice-chain>
<int:poller fixed-rate="1000" />
</int-twitter:search-outbound-gateway>
<int:channel id="out">
<int:queue />
</int:channel>
</beans>

View File

@@ -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));
}
}

View File

@@ -1,60 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
xmlns="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
xmlns="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
http://www.springframework.org/schema/integration/twitter http://www.springframework.org/schema/integration/twitter/spring-integration-twitter.xsd">
<message-history/>
<message-history/>
<context:property-placeholder location="classpath:sample.properties"/>
<channel id="inbound_dm"/>
<channel id="inbound_mentions"/>
<channel id="inbound_updates"/>
<channel id="inbound_search"/>
<context:property-placeholder location="classpath:sample.properties"/>
<channel id="inbound_dm"/>
<channel id="inbound_mentions"/>
<channel id="inbound_updates"/>
<channel id="inbound_search"/>
<beans:bean id="twitterTemplate" class="org.springframework.social.twitter.api.impl.TwitterTemplate">
<beans:constructor-arg value="${z_oleg.oauth.consumerKey}"/>
<beans:constructor-arg value="${z_oleg.oauth.consumerSecret}"/>
<beans:constructor-arg value="${z_oleg.oauth.accessToken}"/>
<beans:constructor-arg value="${z_oleg.oauth.accessTokenSecret}"/>
<beans:constructor-arg value="${twitter.oauth.consumerKey}"/>
<beans:constructor-arg value="${twitter.oauth.consumerSecret}"/>
<beans:constructor-arg value="${twitter.oauth.accessToken}"/>
<beans:constructor-arg value="${twitter.oauth.accessTokenSecret}"/>
</beans:bean>
<!-- <twitter:mentions-inbound-channel-adapter twitter-template="twitterTemplate" channel="inbound_mentions"> -->
<!-- <poller fixed-rate="5000" max-messages-per-poll="-1"/> -->
<!-- </twitter:mentions-inbound-channel-adapter> -->
<!-- <service-activator input-channel="inbound_mentions" ref="twitterAnnouncer" method="mention"/> -->
<!-- <twitter:mentions-inbound-channel-adapter twitter-template="twitterTemplate" channel="inbound_mentions"> -->
<!-- <poller fixed-rate="5000" max-messages-per-poll="-1"/> -->
<!-- </twitter:mentions-inbound-channel-adapter> -->
<!-- <twitter:dm-inbound-channel-adapter twitter-template="twitterTemplate" channel="inbound_dm"> -->
<!-- <poller fixed-rate="5000" max-messages-per-poll="-1"/> -->
<!-- </twitter:dm-inbound-channel-adapter> -->
<!-- <service-activator input-channel="inbound_dm" ref="twitterAnnouncer" method="dm"/> -->
<!-- <twitter:search-inbound-channel-adapter id="searchAdapter" twitter-template="twitterTemplate" channel="inbound_search" query="#springintegration"> -->
<!-- <poller fixed-rate="5000" max-messages-per-poll="5"/> -->
<!-- </twitter:search-inbound-channel-adapter> -->
<!-- <service-activator input-channel="inbound_search" ref="twitterAnnouncer" method="search"/> -->
<!-- <service-activator input-channel="inbound_mentions" ref="twitterAnnouncer" method="mention"/> -->
<twitter:inbound-channel-adapter id="twitterInbound" twitter-template="twitterTemplate" channel="inbound_updates">
<poller fixed-rate="1000" max-messages-per-poll="3"/>
</twitter:inbound-channel-adapter>
<service-activator input-channel="inbound_updates" ref="twitterAnnouncer" method="updates"/>
<!-- <twitter:dm-inbound-channel-adapter twitter-template="twitterTemplate" channel="inbound_dm"> -->
<!-- <poller fixed-rate="5000" max-messages-per-poll="-1"/> -->
<!-- </twitter:dm-inbound-channel-adapter> -->
<!-- <service-activator input-channel="inbound_dm" ref="twitterAnnouncer" method="dm"/> -->
<!-- <twitter:search-inbound-channel-adapter id="searchAdapter" twitter-template="twitterTemplate" channel="inbound_search" query="#springintegration"> -->
<!-- <poller fixed-rate="5000" max-messages-per-poll="5"/> -->
<!-- </twitter:search-inbound-channel-adapter> -->
<!-- <service-activator input-channel="inbound_search" ref="twitterAnnouncer" method="search"/> -->
<twitter:inbound-channel-adapter id="twitterInbound" twitter-template="twitterTemplate" channel="inbound_updates">
<poller fixed-rate="1000" max-messages-per-poll="3"/>
</twitter:inbound-channel-adapter>
<service-activator input-channel="inbound_updates" ref="twitterAnnouncer" method="updates"/>
<beans:bean id="twitterAnnouncer" class="org.springframework.integration.twitter.ignored.TwitterAnnouncer"/>
</beans:beans>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
xmlns="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
http://www.springframework.org/schema/integration/twitter http://www.springframework.org/schema/integration/twitter/spring-integration-twitter.xsd">
<message-history/>
<context:property-placeholder location="classpath:sample.properties"/>
<beans:bean id="twitterTemplate" class="org.springframework.social.twitter.api.impl.TwitterTemplate">
<beans:constructor-arg value="${twitter.oauth.consumerKey}"/>
<beans:constructor-arg value="${twitter.oauth.consumerSecret}"/>
<beans:constructor-arg value="${twitter.oauth.accessToken}"/>
<beans:constructor-arg value="${twitter.oauth.accessTokenSecret}"/>
</beans:bean>
<channel id="search" />
<twitter:search-outbound-gateway request-channel="search" twitter-template="twitterTemplate"
reply-channel="inbound" />
<service-activator input-channel="inbound" ref="twitterAnnouncer" method="searchResult"/>
<beans:bean id="twitterAnnouncer" class="org.springframework.integration.twitter.ignored.TwitterAnnouncer" />
</beans:beans>

View File

@@ -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<String>("#springintegration"));
Thread.sleep(10000);
search.send(new GenericMessage<SearchParameters>(new SearchParameters("#springintegration").count(5)));
Thread.sleep(10000);
search.send(new GenericMessage<SearchParameters>(new SearchParameters("#jjjjunk").count(5)));
Thread.sleep(10000);
ctx.close();
}
}

View File

@@ -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<Tweet> 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());
}

View File

@@ -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<SearchResults>() {
@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<String>("foo"));
Message<?> reply = this.outputChannel.receive(0);
assertNotNull(reply);
@SuppressWarnings("unchecked")
List<Tweet> tweets = (List<Tweet>) 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<SearchResults>() {
@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<String>("foo"));
Message<?> reply = this.outputChannel.receive(0);
assertNotNull(reply);
@SuppressWarnings("unchecked")
List<Tweet> tweets = (List<Tweet>) 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<SearchResults>() {
@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<String>("foo"));
Message<?> reply = this.outputChannel.receive(0);
assertNotNull(reply);
@SuppressWarnings("unchecked")
List<Tweet> tweets = (List<Tweet>) 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<SearchResults>() {
@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<SearchParameters>(parameters));
Message<?> reply = this.outputChannel.receive(0);
assertNotNull(reply);
@SuppressWarnings("unchecked")
List<Tweet> tweets = (List<Tweet>) 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<SearchResults>() {
@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<String>("bar"));
Message<?> reply = this.outputChannel.receive(0);
assertNotNull(reply);
@SuppressWarnings("unchecked")
List<Tweet> tweets = (List<Tweet>) 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<Tweet> empty = new ArrayList<Tweet>(0);
final SearchResults searchResults = new SearchResults(empty, searchMetadata);
doAnswer(new Answer<SearchResults>() {
@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<String>("foo"));
Message<?> reply = this.outputChannel.receive(0);
assertNotNull(reply);
@SuppressWarnings("unchecked")
List<Tweet> tweets = (List<Tweet>) 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);
}
}
}

View File

@@ -2,11 +2,12 @@
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="twitter"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Twitter Adapter</title>
<title>Twitter Support</title>
<para>
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 <emphasis>version 4.0</emphasis>, a search outbound
gateway is provided to perform dynamic searches.
</para>
<section id="twitter-intro">
@@ -21,6 +22,8 @@
Versions of Spring Integration prior to 2.1 were dependent upon the <ulink url="http://twitter4j.org">Twitter4J API</ulink>,
but with the release of <ulink url="http://projects.spring.io/spring-social">Spring Social 1.0 GA</ulink>,
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 <classname>TwitterTemplate</classname> because even
search operations require an authenticated template.
</important>
</para>
@@ -37,7 +40,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter
<title>Twitter OAuth Configuration</title>
<para>
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 <ulink url="http://oauth.net">http://oauth.net</ulink> or
in this article <ulink url="http://hueniverse.com/oauth">http://hueniverse.com/oauth</ulink> from Hueniverse.
@@ -156,6 +159,10 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]></programlis
<code>id</code> attribute of the Twitter Inbound Channel Adapter component plus the <code>profileId</code>
of the Twitter user.
</note>
<para>
Prior to <emphasis>version 4.0</emphasis>, the page size was hard-coded to 20. This is now configurable
using the <code>page-size</code> attribute (defaults to 20).
</para>
<section id="inbound-twitter-update">
<title>Inbound Message Channel Adapter</title>
@@ -291,4 +298,89 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]></programlis
</important>
</para>
</section>
<section id="twitter-sog">
<title>Twitter Search Outbound Gateway</title>
<para>
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 <classname>Tweet</classname> 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 <code>twitter_searchMetadata</code> with its value being
a <classname>SearchMetadata</classname> object. For more information
on the <classname>Tweet</classname>, <classname>SearchParameters</classname> and
<classname>SearchMetadata</classname> classes, refer to the <ulink
url="http://projects.spring.io/spring-social-twitter/">Spring Social Twitter</ulink>
documentation.
</para>
<para>
<emphasis role="bold">Configuring the Outbound Gateway</emphasis>
</para>
<programlisting language="xml"><![CDATA[<int-twitter:search-outbound-gateway id="twitter"
request-channel="in" ]]><co id="tsog010" /><![CDATA[
twitter-template="twitterTemplate" ]]><co id="tsog020" /><![CDATA[
search-args-expression="payload" ]]><co id="tsog030" /><![CDATA[
reply-channel="out" ]]><co id="tsog040" /><![CDATA[
reply-timeout="123" ]]><co id="tsog050" /><![CDATA[
order="1" ]]><co id="tsog060" /><![CDATA[
auto-startup="false" ]]><co id="tsog070" /><![CDATA[
phase="100" ]]><co id="tsog080" /><![CDATA[ />
]]></programlisting>
<calloutlist>
<callout arearefs="tsog010">
<para>The channel used to send search requests to this gateway.</para>
</callout>
<callout arearefs="tsog020">
<para>A reference to a <classname>TwitterTemplate</classname> with authentication configuration.</para>
</callout>
<callout arearefs="tsog030">
<para>
A SpEL expression that evaluates to argument(s) for the search. Default:
<emphasis role="bold">"payload"</emphasis> - in which case the payload can be a <classname>String</classname>
(e.g "#springintegration") and the gateway limits the query to 20 tweets, or the payload can be a
<classname>SearchParameters</classname> object.
</para>
<para>
The expression can also be specified as a <ulink url=
"http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-inline-lists"
>SpEL List</ulink>. The first element (String) is the query, the remaining elements (Numbers)
are <code>pageSize, sinceId, maxId</code> respectively - refer to the Spring Social Twitter
documentation for more information about these parameters.
When specifying a <classname>SearchParameters</classname> object directly in the SpEL
expression, you do not have to fully qualify the class name. Some examples:
<programlisting language="xml">"new SearchParameters(payload).count(5).sinceId(headers.sinceId)"
"{payload, 30}"
"{payload, headers.pageSize, headers.sinceId, headers.maxId}"</programlisting>
</para>
</callout>
<callout arearefs="tsog040">
<para>
The channel to which to send the reply; if omitted, the <code>replyChannel</code> header
is used.
</para>
</callout>
<callout arearefs="tsog050">
<para>
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.
</para>
</callout>
<callout arearefs="tsog060">
<para>
When subscribed to a publish/subscribe channel, the order in which this endpoint will
be invoked.
</para>
</callout>
<callout arearefs="tsog070">
<para>
<interfacename>SmartLifecycle</interfacename> method.
</para>
</callout>
<callout arearefs="tsog080">
<para>
<interfacename>SmartLifecycle</interfacename> method.
</para>
</callout>
</calloutlist>
</section>
</chapter>

View File

@@ -163,6 +163,15 @@
For more information, see <xref linkend="annotations"/>.
</para>
</section>
<section id="4.0-twitter-sog">
<title>Twitter Search Outbound Gateway</title>
<para>
A new twitter endpoint <classname>&lt;int-twitter-search-outbound-gateway/&gt;</classname>
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 <xref linkend="twitter-sog"/>.
</para>
</section>
</section>
<section id="4.0-general">