INT-3065: Add tweet-data-expression for Update

JIRA: https://jira.spring.io/browse/INT-3065

Improve `TwitterSearchOutboundGateway` to use an expression
to build the tweet instead of just using the payload (which
remains the default).
This commit is contained in:
Artem Bilan
2014-04-22 20:41:43 +03:00
committed by Gary Russell
parent 9052377bf5
commit 97d1d389a9
8 changed files with 201 additions and 23 deletions

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.
@@ -21,15 +21,18 @@ import org.w3c.dom.Element;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.twitter.outbound.DirectMessageSendingMessageHandler;
import org.springframework.integration.twitter.outbound.StatusUpdatingMessageHandler;
import org.springframework.util.StringUtils;
/**
* Parser for all outbound Twitter adapters
*
*
* @author Josh Long
* @author Oleg Zhurakousky
* @author Artem Bilan
* @since 2.0
*/
public class TwitterOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser {
@@ -39,6 +42,13 @@ public class TwitterOutboundChannelAdapterParser extends AbstractOutboundChannel
Class<?> clazz = determineClass(element, parserContext);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
builder.addConstructorArgReference(element.getAttribute("twitter-template"));
String tweetDataExpression = element.getAttribute("tweet-data-expression");
if (StringUtils.hasText(tweetDataExpression)) {
builder.addPropertyValue("tweetDataExpression",
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(tweetDataExpression)
.getBeanDefinition());
}
return builder.getBeanDefinition();
}

View File

@@ -16,10 +16,16 @@
package org.springframework.integration.twitter.outbound;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.integration.expression.IntegrationEvaluationContextAware;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.TweetData;
import org.springframework.social.twitter.api.Twitter;
import org.springframework.util.Assert;
@@ -28,37 +34,80 @@ import org.springframework.util.Assert;
*
* @author Josh Long
* @author Oleg Zhurakousky
* @author Artem Bilan
* @since 2.0
*/
public class StatusUpdatingMessageHandler extends AbstractMessageHandler {
public class StatusUpdatingMessageHandler extends AbstractMessageHandler
implements IntegrationEvaluationContextAware {
private final Twitter twitter;
private volatile Expression tweetDataExpression;
private EvaluationContext evaluationContext;
public StatusUpdatingMessageHandler(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 TweetData.
*/
((StandardTypeLocator) typeLocator).registerImport("org.springframework.social.twitter.api");
}
this.evaluationContext = evaluationContext;
}
@Override
public String getComponentType() {
return "twitter:outbound-channel-adapter";
}
/**
* An expression that is used to build the {@link TweetData}; must resolve to a
* {@link TweetData} object, or a {@link String}, or a {@link Tweet}.
* <p> When using a {@code TweetData} directly in the expression, it is not necessary
* to include the package:
* {@code "new TweetData("test").withMedia(headers.mediaResource).displayCoordinates(true)")}.
* @param tweetDataExpression The expression.
* @since 4.0
*/
public void setTweetDataExpression(Expression tweetDataExpression) {
this.tweetDataExpression = tweetDataExpression;
}
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
Object payload = message.getPayload();
String statusText = null;
if (payload instanceof Tweet) {
statusText = ((Tweet) payload).getText();
}
else if (payload instanceof String) {
statusText = (String) payload;
Object value;
if (this.tweetDataExpression != null) {
value = this.tweetDataExpression.getValue(this.evaluationContext, message);
}
else {
throw new MessageHandlingException(message, "Unsupported payload type '" + payload.getClass().getName() + "'");
value = message.getPayload();
}
this.twitter.timelineOperations().updateStatus(statusText);
Assert.notNull(value, "The tweetData cannot evaluate to 'null'.");
TweetData tweetData = null;
if (value instanceof TweetData) {
tweetData = (TweetData) value;
}
else if (value instanceof Tweet) {
tweetData = new TweetData(((Tweet) value).getText());
}
else if (value instanceof String) {
tweetData = new TweetData((String) value);
}
else {
throw new MessageHandlingException(message, "Unsupported tweetData: " + value);
}
this.twitter.timelineOperations().updateStatus(tweetData);
}
}

View File

@@ -22,7 +22,6 @@ 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;
@@ -50,7 +49,7 @@ public class TwitterSearchOutboundGateway extends AbstractReplyProducingMessageH
private final Twitter twitter;
private volatile Expression searchArgsExpression = new SpelExpressionParser().parseExpression("payload");
private volatile Expression searchArgsExpression;
private volatile EvaluationContext evaluationContext;
@@ -102,7 +101,13 @@ public class TwitterSearchOutboundGateway extends AbstractReplyProducingMessageH
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
Object args = this.searchArgsExpression.getValue(this.evaluationContext, requestMessage);
Object args;
if (this.searchArgsExpression != null) {
args = this.searchArgsExpression.getValue(this.evaluationContext, requestMessage);
}
else {
args = requestMessage.getPayload();
}
Assert.notNull(args, "The twitter search expression cannot evaluate to 'null'.");
SearchParameters searchParameters;
if (args instanceof SearchParameters) {

View File

@@ -113,6 +113,16 @@
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="outbound-twitter-type">
<xsd:attribute name="tweet-data-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
A SpEL expression that evaluates to tweetData; the evaluation result type can be
an 'org.springframework.social.twitter.api.TweetData', a 'String' or
'org.springframework.social.twitter.api.Tweet'.
Default: "payload".
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
</xsd:extension>
</xsd:complexContent>

View File

@@ -0,0 +1,21 @@
<?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-twitter:outbound-channel-adapter id="in1" twitter-template="tt" />
<int-twitter:outbound-channel-adapter
id="in2"
twitter-template="tt"
tweet-data-expression="new TweetData(payload.foo).withMedia(headers.media)"/>
</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.
@@ -16,37 +16,93 @@
package org.springframework.integration.twitter.outbound;
import static org.junit.Assert.assertEquals;
import java.util.Collections;
import java.util.Properties;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.messaging.Message;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.social.twitter.api.TimelineOperations;
import org.springframework.social.twitter.api.TweetData;
import org.springframework.social.twitter.api.Twitter;
import org.springframework.social.twitter.api.impl.TwitterTemplate;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.MultiValueMap;
/**
* @author Oleg Zhurakousky
* @author Artem Bilan
* @since 2.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class StatusUpdatingMessageHandlerTests {
@Autowired
MessageChannel in1;
@Autowired
MessageChannel in2;
@Autowired
Twitter twitter;
@Test @Ignore
public void demoSendStatusMessage() throws Exception{
PropertiesFactoryBean pf = new PropertiesFactoryBean();
pf.setLocation(new ClassPathResource("sample.properties"));
pf.afterPropertiesSet();
Properties prop = pf.getObject();
TwitterTemplate template = new TwitterTemplate(prop.getProperty("z_oleg.oauth.consumerKey"),
prop.getProperty("z_oleg.oauth.consumerSecret"),
prop.getProperty("z_oleg.oauth.accessToken"),
TwitterTemplate template = new TwitterTemplate(prop.getProperty("z_oleg.oauth.consumerKey"),
prop.getProperty("z_oleg.oauth.consumerSecret"),
prop.getProperty("z_oleg.oauth.accessToken"),
prop.getProperty("z_oleg.oauth.accessTokenSecret"));
Message<?> message1 = MessageBuilder.withPayload("Ppolishing #springintegration migration to Spring Social. test").build();
Message<?> message1 = MessageBuilder.withPayload("Polishing #springintegration migration to Spring Social. test").build();
StatusUpdatingMessageHandler handler = new StatusUpdatingMessageHandler(template);
handler.afterPropertiesSet();
handler.handleMessage(message1);
}
@Test
public void testStatusUpdatingMessageHandler() {
TimelineOperations timelineOperations = Mockito.mock(TimelineOperations.class);
Mockito.when(this.twitter.timelineOperations()).thenReturn(timelineOperations);
ArgumentCaptor<TweetData> argument = ArgumentCaptor.forClass(TweetData.class);
this.in1.send(new GenericMessage<String>("foo"));
Mockito.verify(timelineOperations).updateStatus(argument.capture());
assertEquals("foo", argument.getValue().toRequestParameters().getFirst("status"));
Mockito.reset(timelineOperations);
ClassPathResource media = new ClassPathResource("log4j.properties");
this.in2.send(MessageBuilder.withPayload(Collections.singletonMap("foo", "bar"))
.setHeader("media", media)
.build());
Mockito.verify(timelineOperations).updateStatus(argument.capture());
MultiValueMap<String, Object> requestParameters = argument.getValue().toRequestParameters();
assertEquals("bar", requestParameters.getFirst("status"));
assertEquals(media, requestParameters.getFirst("media"));
}
}

View File

@@ -253,7 +253,24 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]></programlis
channel="twitterChannel"/>]]></programlisting>
The only extra configuration that is required for this adapter is the <code>twitter-template</code> reference.
</para>
</section>
<para>
Starting with <emphasis>version 4.0</emphasis> the <code>&lt;int-twitter:outbound-channel-adapter&gt;</code>
supports a <code>tweet-data-expression</code> to populate the <classname>TweetData</classname> argument
(<ulink url="http://projects.spring.io/spring-social-twitter/">Spring Social Twitter</ulink>) using the
message as the root object of the expression evaluation context. The result can be a <classname>String</classname>,
which will be used for the <classname>TweetData</classname> message; a <classname>Tweet</classname> object, the
<code>text</code> of which will be used for the <classname>TweetData</classname> message; or an entire
<classname>TweetData</classname> object. For convenience, the <classname>TweetData</classname> can be built
from the expression directly without needing a fully qualified class name:
<programlisting language="xml"><![CDATA[<int-twitter:outbound-channel-adapter
twitter-template="twitterTemplate"
channel="twitterChannel"
tweet-data-expression="new TweetData(payload).withMedia(headers.media).displayCoordinates(true)/>]]></programlisting>
</para>
<para>
This allows, for example, attaching an image to the tweet.
</para>
</section>
<section id="outbound-twitter-direct">
<title>Twitter Outbound Direct Message Channel Adapter</title>

View File

@@ -322,5 +322,15 @@
advanced configuration. See <xref linkend="ftp-session-factory"/> for more information.
</para>
</section>
<section id="4.0-twitter-status-updating">
<title>Twitter: StatusUpdatingMessageHandler</title>
<para>
The <classname>StatusUpdatingMessageHandler</classname> (<code>&lt;int-twitter:outbound-channel-adapter&gt;</code>)
now supports the <code>tweet-data-expression</code> attribute to build a
<classname>org.springframework.social.twitter.api.TweetData</classname> object for updating the
timeline status allowing, for example, attaching an image.
See <xref linkend="outbound-twitter-update"/> for more information.
</para>
</section>
</section>
</chapter>