Merge remote-tracking branch 'upstream/master' into 4.0.0-WIP

Conflicts:
	spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java
	spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java
	spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java
	spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java
	spring-integration-core/src/main/java/org/springframework/integration/metadata/package-info.java
	spring-integration-feed/src/test/java/org/springframework/integration/feed/inbound/FeedEntryMessageSourceTests.java
	spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapter.java
	spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandler.java
	spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests.java
	spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java
	spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests.java
	spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java
	spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java
	spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java
	spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceTests.java
	spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests.java

Resolved.
This commit is contained in:
Gary Russell
2013-11-05 14:30:50 -05:00
98 changed files with 2277 additions and 504 deletions

View File

@@ -56,7 +56,6 @@ subprojects { subproject ->
junitVersion = '4.11'
log4jVersion = '1.2.12'
mockitoVersion = '1.9.5'
eaioUUIDVersion = '3.2'
ftpServerVersion = '1.0.6'
@@ -195,7 +194,6 @@ project('spring-integration-core') {
compile "org.springframework:spring-messaging:$springVersion"
compile "org.springframework:spring-tx:$springVersion"
compile "org.springframework.retry:spring-retry:$springRetryVersion"
compile "com.eaio.uuid:uuid:$eaioUUIDVersion"
compile("org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion", optional)
compile("com.fasterxml.jackson.core:jackson-databind:$jackson2Version", optional)
compile('com.jayway.jsonpath:json-path:0.8.1', optional)
@@ -644,6 +642,7 @@ task api(type: Javadoc) {
options.author = true
options.header = rootProject.description
options.overview = 'src/api/overview.html'
options.stylesheetFile = file("src/api/stylesheet.css")
source subprojects.collect { project ->
project.sourceSets.main.allJava
}

View File

@@ -26,8 +26,8 @@ import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.integration.EiMessageHeaderAccessor;
import org.springframework.integration.amqp.AmqpHeaders;
import org.springframework.integration.json.JsonHeaders;
import org.springframework.integration.mapping.AbstractHeaderMapper;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.util.StringUtils;
/**

View File

@@ -0,0 +1,336 @@
/*
* Copyright 2002-2013 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;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* The headers for a {@link Message}.<br>
* IMPORTANT: MessageHeaders are immutable. Any mutating operation (e.g., put(..), putAll(..) etc.)
* will result in {@link UnsupportedOperationException}
* To create MessageHeaders instance use fluent MessageBuilder API
* <pre class="code">
* MessageBuilder.withPayload("foo").setHeader("key1", "value1").setHeader("key2", "value2");
* </pre>
* or create an instance of GenericMessage passing payload as {@link Object} and headers as a regular {@link Map}
* <pre class="code">
* Map headers = new HashMap();
* headers.put("key1", "value1");
* headers.put("key2", "value2");
* new GenericMessage("foo", headers);
* </pre>
*
* @author Arjen Poutsma
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Rossen Stoyanchev
*/
public final class MessageHeaders implements Map<String, Object>, Serializable {
private static final long serialVersionUID = 6901029029524535147L;
private static final Log logger = LogFactory.getLog(MessageHeaders.class);
private static volatile IdGenerator idGenerator = null;
private static final IdGenerator defaultIdGenerator = new AlternativeJdkIdGenerator();
/**
* The key for the Message ID. This is an automatically generated UUID and
* should never be explicitly set in the header map <b>except</b> in the
* case of Message deserialization where the serialized Message's generated
* UUID is being restored.
*/
public static final String ID = "id";
public static final String TIMESTAMP = "timestamp";
public static final String CORRELATION_ID = "correlationId";
public static final String REPLY_CHANNEL = "replyChannel";
public static final String ERROR_CHANNEL = "errorChannel";
public static final String EXPIRATION_DATE = "expirationDate";
public static final String PRIORITY = "priority";
public static final String SEQUENCE_NUMBER = "sequenceNumber";
public static final String SEQUENCE_SIZE = "sequenceSize";
public static final String SEQUENCE_DETAILS = "sequenceDetails";
public static final String CONTENT_TYPE = "content-type";
public static final String POSTPROCESS_RESULT = "postProcessResult";
private final Map<String, Object> headers;
public MessageHeaders(Map<String, Object> headers) {
this.headers = (headers != null) ? new HashMap<String, Object>(headers) : new HashMap<String, Object>();
IdGenerator generatorToUse = (idGenerator != null) ? idGenerator : defaultIdGenerator;
this.headers.put(ID, generatorToUse.generateId());
this.headers.put(TIMESTAMP, new Long(System.currentTimeMillis()));
}
public UUID getId() {
return this.get(ID, UUID.class);
}
public Long getTimestamp() {
return this.get(TIMESTAMP, Long.class);
}
public Long getExpirationDate() {
return this.get(EXPIRATION_DATE, Long.class);
}
public Object getCorrelationId() {
return this.get(CORRELATION_ID);
}
public Object getReplyChannel() {
return this.get(REPLY_CHANNEL);
}
public Object getErrorChannel() {
return this.get(ERROR_CHANNEL);
}
public Integer getSequenceNumber() {
Integer sequenceNumber = this.get(SEQUENCE_NUMBER, Integer.class);
return (sequenceNumber != null ? sequenceNumber : 0);
}
public Integer getSequenceSize() {
Integer sequenceSize = this.get(SEQUENCE_SIZE, Integer.class);
return (sequenceSize != null ? sequenceSize : 0);
}
public Integer getPriority() {
return this.get(PRIORITY, Integer.class);
}
@SuppressWarnings("unchecked")
public <T> T get(Object key, Class<T> type) {
Object value = this.headers.get(key);
if (value == null) {
return null;
}
if (!type.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException("Incorrect type specified for header '" + key + "'. Expected [" + type
+ "] but actual type is [" + value.getClass() + "]");
}
return (T) value;
}
@Override
public int hashCode() {
return this.headers.hashCode();
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object != null && object instanceof MessageHeaders) {
MessageHeaders other = (MessageHeaders) object;
return this.headers.equals(other.headers);
}
return false;
}
@Override
public String toString() {
return this.headers.toString();
}
/*
* Map implementation
*/
public boolean containsKey(Object key) {
return this.headers.containsKey(key);
}
public boolean containsValue(Object value) {
return this.headers.containsValue(value);
}
public Set<Map.Entry<String, Object>> entrySet() {
return Collections.unmodifiableSet(this.headers.entrySet());
}
public Object get(Object key) {
return this.headers.get(key);
}
public boolean isEmpty() {
return this.headers.isEmpty();
}
public Set<String> keySet() {
return Collections.unmodifiableSet(this.headers.keySet());
}
public int size() {
return this.headers.size();
}
public Collection<Object> values() {
return Collections.unmodifiableCollection(this.headers.values());
}
/*
* Unsupported operations
*/
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public Object put(String key, Object value) {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public void putAll(Map<? extends String, ? extends Object> t) {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public Object remove(Object key) {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public void clear() {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/*
* Serialization methods
*/
private void writeObject(ObjectOutputStream out) throws IOException {
List<String> keysToRemove = new ArrayList<String>();
for (Map.Entry<String, Object> entry : this.headers.entrySet()) {
if (!(entry.getValue() instanceof Serializable)) {
keysToRemove.add(entry.getKey());
}
}
for (String key : keysToRemove) {
if (logger.isInfoEnabled()) {
logger.info("removing non-serializable header: " + key);
}
this.headers.remove(key);
}
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}
public static interface IdGenerator {
UUID generateId();
}
public static class JdkIdGenerator implements IdGenerator {
@Override
public UUID generateId() {
return UUID.randomUUID();
}
}
/**
* A variation of {@link UUID#randomUUID()} that uses {@link SecureRandom} only for
* the initial seed and {@link Random} thereafter, which provides better performance
* in exchange for less securely random id's.
*/
public static class AlternativeJdkIdGenerator implements IdGenerator {
private final Random random;
public AlternativeJdkIdGenerator() {
byte[] seed = new SecureRandom().generateSeed(8);
this.random = new Random(new BigInteger(seed).longValue());
}
public UUID generateId() {
byte[] randomBytes = new byte[16];
this.random.nextBytes(randomBytes);
long mostSigBits = 0;
for (int i = 0; i < 8; i++) {
mostSigBits = (mostSigBits << 8) | (randomBytes[i] & 0xff);
}
long leastSigBits = 0;
for (int i = 8; i < 16; i++) {
leastSigBits = (leastSigBits << 8) | (randomBytes[i] & 0xff);
}
return new UUID(mostSigBits, leastSigBits);
}
}
public static class SimpleIncrementingIdGenerator implements IdGenerator {
private final AtomicLong topBits = new AtomicLong();
private final AtomicLong bottomBits = new AtomicLong();
@Override
public UUID generateId() {
long bottomBits = this.bottomBits.incrementAndGet();
if (bottomBits == 0) {
this.topBits.incrementAndGet();
}
return new UUID(this.topBits.get(), bottomBits);
}
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.integration.config.xml;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
@@ -31,18 +32,25 @@ import org.springframework.util.xml.DomUtils;
* @author Mark Fisher
* @author Gary Russell
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
public abstract class AbstractPollingInboundChannelAdapterParser extends AbstractChannelAdapterParser {
@Override
@SuppressWarnings("unchecked")
protected AbstractBeanDefinition doParse(Element element, ParserContext parserContext, String channelName) {
BeanMetadataElement source = this.parseSource(element, parserContext);
if (source == null) {
parserContext.getReaderContext().error("failed to parse source", element);
}
String channelAdapterId = this.resolveId(element, (AbstractBeanDefinition) source, parserContext);
String sourceBeanName = channelAdapterId + ".source";
parserContext.getRegistry().registerBeanDefinition(sourceBeanName, (BeanDefinition) source);
BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder
.genericBeanDefinition(SourcePollingChannelAdapterFactoryBean.class);
adapterBuilder.addPropertyValue("source", source);
adapterBuilder.addPropertyReference("source", sourceBeanName);
adapterBuilder.addPropertyReference("outputChannel", channelName);
IntegrationNamespaceUtils.setValueIfAttributeDefined(adapterBuilder, element, "send-timeout");
Element pollerElement = DomUtils.getChildElementByTagName(element, "poller");

View File

@@ -172,9 +172,19 @@ public abstract class IntegrationNamespaceUtils {
*/
public static void setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element,
String attributeName, String propertyName) {
String attributeValue = element.getAttribute(attributeName);
if (StringUtils.hasText(attributeValue)) {
builder.addPropertyReference(propertyName, attributeValue);
setReferenceIfAttributeDefined(builder, element, attributeName, propertyName, false);
}
public static void setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element,
String attributeName, String propertyName, boolean emptyStringAllowed) {
if (element.hasAttribute(attributeName)) {
String attributeValue = element.getAttribute(attributeName);
if (StringUtils.hasText(attributeValue)) {
builder.addPropertyReference(propertyName, attributeValue);
}
else if (emptyStringAllowed) {
builder.addPropertyValue(propertyName, null);
}
}
}
@@ -198,8 +208,13 @@ public abstract class IntegrationNamespaceUtils {
*/
public static void setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element,
String attributeName) {
setReferenceIfAttributeDefined(builder, element, attributeName, false);
}
public static void setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element,
String attributeName, boolean emptyStringAllowed) {
setReferenceIfAttributeDefined(builder, element, attributeName,
Conventions.attributeNameToPropertyName(attributeName));
Conventions.attributeNameToPropertyName(attributeName), emptyStringAllowed);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2013 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,6 +22,7 @@ import org.w3c.dom.Element;
/**
* @author Oleg Zhurakousky
* @author Mauro Franceschini
* @since 2.0
*/
public class ObjectToMapTransformerParser extends AbstractTransformerParser {
@@ -33,5 +34,6 @@ public class ObjectToMapTransformerParser extends AbstractTransformerParser {
@Override
protected void parseTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "flatten", "shouldFlattenKeys");
}
}

View File

@@ -19,7 +19,7 @@ package org.springframework.integration.context;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.store.metadata.MetadataStore;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.messaging.MessageChannel;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;

View File

@@ -52,13 +52,15 @@ import org.springframework.util.Assert;
* the configuration by removing channels that can be created implicitly.
* <p>
*
* <pre>
* &lt;chain&gt;
* &lt;filter ref=&quot;someFilter&quot;/&gt;
* &lt;bean class=&quot;SomeMessageHandlerImplementation&quot;/&gt;
* &lt;transformer ref=&quot;someTransformer&quot;/&gt;
* &lt;aggregator ... /&gt;
* &lt;/chain&gt;
* <pre class="code">
* {@code
* <chain>
* <filter ref="someFilter"/>
* <bean class="SomeMessageHandlerImplementation"/>
* <transformer ref="someTransformer"/>
* <aggregator ... />
* </chain>
* }
* </pre>
*
* @author Mark Fisher

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.json;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.json.JacksonJsonObjectMapper;
import org.springframework.integration.support.json.JacksonJsonObjectMapperProvider;

View File

@@ -27,7 +27,7 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.json.JsonHeaders;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.json;
package org.springframework.integration.mapping.support;
import java.util.Arrays;
import java.util.Collection;

View File

@@ -0,0 +1,4 @@
/**
* Support classes for mapping.
*/
package org.springframework.integration.mapping.support;

View File

@@ -14,7 +14,10 @@
* limitations under the License.
*/
package org.springframework.integration.store.metadata;
package org.springframework.integration.metadata;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
/**
* Strategy interface for storing metadata from certain adapters
@@ -25,6 +28,7 @@ package org.springframework.integration.store.metadata;
* @author Mark Fisher
* @since 2.0
*/
@ManagedResource
public interface MetadataStore {
/**
@@ -35,6 +39,15 @@ public interface MetadataStore {
/**
* Reads a value for the given key from this MetadataStore.
*/
@ManagedAttribute
String get(String key);
/**
* Remove a value for the given key from this MetadataStore.
* return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@ManagedAttribute
String remove(String key);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.store.metadata;
package org.springframework.integration.metadata;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -86,6 +86,12 @@ public class PropertiesPersistingMetadataStore implements MetadataStore, Initial
return this.metadata.getProperty(key);
}
@Override
@SuppressWarnings("uchecked")
public String remove(String key) {
return (String) this.metadata.remove(key);
}
public void destroy() throws Exception {
this.saveMetadata();
}

View File

@@ -11,7 +11,7 @@
* specific language governing permissions and limitations under the License.
*/
package org.springframework.integration.store.metadata;
package org.springframework.integration.metadata;
import java.util.HashMap;
import java.util.Map;
@@ -37,4 +37,9 @@ public class SimpleMetadataStore implements MetadataStore {
return this.metadata.get(key);
}
@Override
public String remove(String key) {
return metadata.remove(key);
}
}

View File

@@ -1,4 +1,4 @@
/**
* Provides classes supporting metadata stores.
*/
package org.springframework.integration.store.metadata;
package org.springframework.integration.metadata;

View File

@@ -27,11 +27,13 @@ import org.springframework.integration.core.MessageSelector;
import org.springframework.util.Assert;
/**
* <pre>
* &lt;recipient-list-router id="simpleRouter" input-channel="routingChannelA"&gt;
* &lt;recipient channel="channel1"/&gt;
* &lt;recipient channel="channel2"/&gt;
* &lt;/recipient-list-router&gt;
* <pre class="code">
* {@code
* <recipient-list-router id="simpleRouter" input-channel="routingChannelA">
* <recipient channel="channel1"/>
* <recipient channel="channel2"/>
* </recipient-list-router>
* }
* </pre>
* <p>
* A Message Router that sends Messages to a list of recipient channels. The

View File

@@ -25,7 +25,7 @@ import java.net.URL;
import java.util.Collection;
import java.util.Map;
import org.springframework.integration.json.JsonHeaders;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonParser;

View File

@@ -29,7 +29,7 @@ import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.JavaType;
import org.springframework.integration.json.JsonHeaders;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.util.Assert;
/**

View File

@@ -2169,6 +2169,15 @@
<xsd:complexContent>
<xsd:extension base="specialized-transformer-type">
<xsd:attributeGroup ref="inputOutputChannelGroup" />
<xsd:attribute name="flatten" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Specifies if the result Map of Maps should be transformed further to flat keys of
object's property paths.
Default is 'true'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

View File

@@ -15,4 +15,12 @@
<object-to-map-transformer input-channel="directInput" output-channel="output"/>
<channel id="nestedInput"/>
<channel id="nestedOutput">
<queue capacity="1"/>
</channel>
<object-to-map-transformer input-channel="nestedInput" output-channel="nestedOutput" flatten="false"/>
</beans:beans>

View File

@@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -40,10 +41,12 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
/**
* @author Oleg Zhurakousky
* @author Gunnar Hillert
* @author Mauro Franceschini
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@@ -57,6 +60,14 @@ public class ObjectToMapTransformerParserTests {
@Qualifier("output")
private PollableChannel output;
@Autowired
@Qualifier("nestedInput")
private MessageChannel nestedInput;
@Autowired
@Qualifier("nestedOutput")
private PollableChannel nestedOutput;
@SuppressWarnings("unchecked")
@Test
@@ -90,6 +101,23 @@ public class ObjectToMapTransformerParserTests {
directInput.send(message);
}
@Test
public void testObjectToNotFlattenedMapTransformer(){
Employee employee = this.buildEmployee();
Message<Employee> message = MessageBuilder.withPayload(employee).build();
nestedInput.send(message);
@SuppressWarnings("unchecked")
Message<Map<String, Object>> outputMessage = (Message<Map<String, Object>>) nestedOutput.receive(1000);
Map<String, Object> transformedMap = outputMessage.getPayload();
assertNotNull(outputMessage.getPayload());
assertEquals(employee.getCompanyName(), transformedMap.get("companyName"));
assertThat(transformedMap.get("companyAddress"), Matchers.instanceOf(Map.class));
assertThat(transformedMap.get("departments"), Matchers.instanceOf(List.class));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public Employee buildEmployee(){
Address companyAddress = new Address();

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.store.metadata;
package org.springframework.integration.metadata;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -27,7 +27,6 @@ import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.integration.store.metadata.PropertiesPersistingMetadataStore;
/**
* @author Oleg Zhurakousky

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2013 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.
@@ -23,6 +23,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.feed.inbound.FeedEntryMessageSource;
import org.springframework.util.StringUtils;
/**
@@ -31,20 +32,23 @@ import org.springframework.util.StringUtils;
* @author Josh Long
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Gunnar Hillert
* @author Artem Bilan
* @since 2.0
*/
public class FeedInboundChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser {
@Override
protected BeanMetadataElement parseSource(final Element element, final ParserContext parserContext) {
BeanDefinitionBuilder sourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.feed.inbound.FeedEntryMessageSource");
BeanDefinitionBuilder sourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(FeedEntryMessageSource.class);
sourceBuilder.addConstructorArgValue(element.getAttribute("url"));
sourceBuilder.addConstructorArgValue(element.getAttribute(ID_ATTRIBUTE));
String feedFetcherRef = element.getAttribute("feed-fetcher");
if (StringUtils.hasText(feedFetcherRef)) {
sourceBuilder.addConstructorArgReference(feedFetcherRef);
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(sourceBuilder, element, "metadata-store");
return sourceBuilder.getBeanDefinition();
}

View File

@@ -30,8 +30,8 @@ import org.springframework.messaging.MessagingException;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.store.metadata.MetadataStore;
import org.springframework.integration.store.metadata.SimpleMetadataStore;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -52,6 +52,7 @@ import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher;
* @author Josh Long
* @author Mario Gray
* @author Oleg Zhurakousky
* @author Artem Bilan
* @since 2.0
*/
public class FeedEntryMessageSource extends IntegrationObjectSupport implements MessageSource<SyndEntry> {
@@ -62,7 +63,7 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
private final Queue<SyndEntry> entries = new ConcurrentLinkedQueue<SyndEntry>();
private volatile String metadataKey;
private final String metadataKey;
private volatile MetadataStore metadataStore;
@@ -82,17 +83,19 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
* If the feed URL has a protocol other than http*, consider providing a custom implementation of the
* {@link FeedFetcher} via the alternate constructor.
*/
public FeedEntryMessageSource(URL feedUrl) {
this(feedUrl, new HttpURLFeedFetcher(HashMapFeedInfoCache.getInstance()));
public FeedEntryMessageSource(URL feedUrl, String metadataKey) {
this(feedUrl, metadataKey, new HttpURLFeedFetcher(HashMapFeedInfoCache.getInstance()));
}
/**
* Creates a FeedEntryMessageSource that will use the provided FeedFetcher to read from the given feed URL.
*/
public FeedEntryMessageSource(URL feedUrl, FeedFetcher feedFetcher) {
public FeedEntryMessageSource(URL feedUrl, String metadataKey, FeedFetcher feedFetcher) {
Assert.notNull(feedUrl, "feedUrl must not be null");
Assert.notNull(metadataKey, "metadataKey must not be null");
Assert.notNull(feedFetcher, "feedFetcher must not be null");
this.feedUrl = feedUrl;
this.metadataKey = metadataKey + "." + this.feedUrl;
this.feedFetcher = feedFetcher;
}
@@ -130,18 +133,7 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
this.metadataStore = new SimpleMetadataStore();
}
}
StringBuilder metadataKeyBuilder = new StringBuilder();
if (StringUtils.hasText(this.getComponentType())) {
metadataKeyBuilder.append(this.getComponentType() + ".");
}
if (StringUtils.hasText(this.getComponentName())) {
metadataKeyBuilder.append(this.getComponentName() + ".");
}
else if (logger.isWarnEnabled()) {
logger.warn("FeedEntryMessageSource has no name. MetadataStore key might not be unique.");
}
metadataKeyBuilder.append(this.feedUrl);
this.metadataKey = metadataKeyBuilder.toString();
String lastTimeValue = this.metadataStore.get(this.metadataKey);
if (StringUtils.hasText(lastTimeValue)) {
this.lastTime = Long.parseLong(lastTimeValue);
@@ -237,6 +229,7 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
}
return (date2 == null) ? 1 : 0;
}
}
@@ -258,6 +251,7 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
}
}
}
}
}

View File

@@ -22,7 +22,28 @@
<xsd:sequence>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
<xsd:attribute name="id" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
The bean id of this Polling Endpoint; the MessageSource is also registered with this id
plus a suffix '.source'; also used as the
MetaDataStore key with suffix '.' + feedUrl - The URL for an RSS or ATOM feed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies the channel attached to this adapter, to which messages will be sent.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
<xsd:attribute name="url" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
@@ -54,7 +75,7 @@
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.store.MetadataStore" />
<tool:expected-type type="org.springframework.integration.metadata.MetadataStore" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>

View File

@@ -21,6 +21,6 @@
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.inbound.FileUrlFeedFetcher"/>
<bean id="metadataStore" class="org.springframework.integration.store.metadata.PropertiesPersistingMetadataStore"/>
<bean id="metadataStore" class="org.springframework.integration.metadata.PropertiesPersistingMetadataStore"/>
</beans>

View File

@@ -1,21 +0,0 @@
<?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-feed="http://www.springframework.org/schema/integration/feed"
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/feed http://www.springframework.org/schema/integration/feed/spring-integration-feed.xsd">
<int-feed:inbound-channel-adapter channel="feedChannelUsage"
url="file:src/test/java/org/springframework/integration/feed/sample.rss"
feed-fetcher="fileUrlFeedFetcher">
<int:poller fixed-rate="10000" max-messages-per-poll="100"/>
</int-feed:inbound-channel-adapter>
<int:service-activator id="sampleActivator" input-channel="feedChannelUsage">
<bean class="org.springframework.integration.feed.config.FeedInboundChannelAdapterParserTests$SampleServiceNoHistory" />
</int:service-activator>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.inbound.FileUrlFeedFetcher"/>
</beans>

View File

@@ -33,6 +33,7 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.messaging.Message;
@@ -43,7 +44,7 @@ import org.springframework.messaging.MessageHandler;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.feed.inbound.FeedEntryMessageSource;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.store.metadata.MetadataStore;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.integration.test.util.TestUtils;
import com.sun.syndication.feed.synd.SyndEntry;
@@ -83,6 +84,7 @@ public class FeedInboundChannelAdapterParserTests {
context.destroy();
}
public void validateSuccessfulHttpConfigurationWithCustomMetadataStore() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"FeedInboundChannelAdapterParserTests-http-context.xml", this.getClass());
@@ -98,7 +100,7 @@ public class FeedInboundChannelAdapterParserTests {
@Test
public void validateSuccessfulNewsRetrievalWithFileUrlAndMessageHistory() throws Exception {
File persisterFile = new File(System.getProperty("java.io.tmpdir") + "/spring-integration/", "message-store.properties");
File persisterFile = new File(System.getProperty("java.io.tmpdir") + "/spring-integration/", "metadata-store.properties");
if (persisterFile.exists()) {
persisterFile.delete();
}
@@ -120,26 +122,6 @@ public class FeedInboundChannelAdapterParserTests {
context.destroy();
}
@Test
public void validateSuccessfulNewsRetrievalWithFileUrlNoPersistentIdentifier() throws Exception{
//Test file samples.rss has 3 news items
latch = spy(new CountDownLatch(3));
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"FeedInboundChannelAdapterParserTests-file-usage-noid-context.xml", this.getClass());
latch.await(5, TimeUnit.SECONDS);
verify(latch, times(3)).countDown();
context.destroy();
// since we are not deleting the persister file
// in this iteration no new feeds will be received and the latch will timeout
latch = spy(new CountDownLatch(3));
context = new ClassPathXmlApplicationContext(
"FeedInboundChannelAdapterParserTests-file-usage-noid-context.xml", this.getClass());
latch.await(5, TimeUnit.SECONDS);
verify(latch, times(3)).countDown();
context.destroy();
}
@Test
@Ignore // goes against the real feed
public void validateSuccessfulNewsRetrievalWithHttpUrl() throws Exception{
@@ -204,6 +186,11 @@ public class FeedInboundChannelAdapterParserTests {
public String get(String key) {
return null;
}
@Override
public String remove(String key) {
return null;
}
}
}

View File

@@ -25,8 +25,9 @@ import java.net.URL;
import org.junit.Before;
import org.junit.Test;
import org.springframework.integration.metadata.PropertiesPersistingMetadataStore;
import org.springframework.messaging.Message;
import org.springframework.integration.store.metadata.PropertiesPersistingMetadataStore;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.fetcher.FeedFetcher;
@@ -52,14 +53,14 @@ public class FeedEntryMessageSourceTests {
@Test(expected=IllegalArgumentException.class)
public void testFailureWhenNotInitialized() throws Exception {
URL url = new URL("file:src/test/java/org/springframework/integration/feed/sample.rss");
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url);
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, "foo");
feedEntrySource.receive();
}
@Test
public void testReceiveFeedWithNoEntries() throws Exception {
URL url = new URL("file:src/test/java/org/springframework/integration/feed/empty.rss");
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, this.feedFetcher);
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, "foo", this.feedFetcher);
feedEntrySource.setBeanName("feedReader");
feedEntrySource.afterPropertiesSet();
assertNull(feedEntrySource.receive());
@@ -68,7 +69,7 @@ public class FeedEntryMessageSourceTests {
@Test
public void testReceiveFeedWithEntriesSorted() throws Exception {
URL url = new URL("file:src/test/java/org/springframework/integration/feed/sample.rss");
FeedEntryMessageSource source = new FeedEntryMessageSource(url, this.feedFetcher);
FeedEntryMessageSource source = new FeedEntryMessageSource(url, "foo", this.feedFetcher);
source.setComponentName("feedReader");
source.afterPropertiesSet();
Message<SyndEntry> message1 = source.receive();
@@ -87,7 +88,7 @@ public class FeedEntryMessageSourceTests {
@Test
public void testReceiveFeedWithRealEntriesAndRepeatWithPersistentMetadataStore() throws Exception {
URL url = new URL("file:src/test/java/org/springframework/integration/feed/sample.rss");
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, this.feedFetcher);
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, "foo", this.feedFetcher);
feedEntrySource.setBeanName("feedReader");
PropertiesPersistingMetadataStore metadataStore = new PropertiesPersistingMetadataStore();
metadataStore.afterPropertiesSet();
@@ -111,7 +112,7 @@ public class FeedEntryMessageSourceTests {
metadataStore.afterPropertiesSet();
// now test that what's been read is no longer retrieved
feedEntrySource = new FeedEntryMessageSource(url, this.feedFetcher);
feedEntrySource = new FeedEntryMessageSource(url, "foo", this.feedFetcher);
feedEntrySource.setBeanName("feedReader");
metadataStore = new PropertiesPersistingMetadataStore();
metadataStore.afterPropertiesSet();
@@ -127,7 +128,7 @@ public class FeedEntryMessageSourceTests {
@Test
public void testReceiveFeedWithRealEntriesAndRepeatNoPersistentMetadataStore() throws Exception {
URL url = new URL("file:src/test/java/org/springframework/integration/feed/sample.rss");
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, this.feedFetcher);
FeedEntryMessageSource feedEntrySource = new FeedEntryMessageSource(url, "foo", this.feedFetcher);
feedEntrySource.setBeanName("feedReader");
feedEntrySource.afterPropertiesSet();
SyndEntry entry1 = feedEntrySource.receive().getPayload();
@@ -146,7 +147,7 @@ public class FeedEntryMessageSourceTests {
// UNLIKE the previous test
// now test that what's been read is read AGAIN
feedEntrySource = new FeedEntryMessageSource(url, this.feedFetcher);
feedEntrySource = new FeedEntryMessageSource(url, "foo", this.feedFetcher);
feedEntrySource.setBeanName("feedReader");
feedEntrySource.afterPropertiesSet();
entry1 = feedEntrySource.receive().getPayload();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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,9 +16,10 @@
package org.springframework.integration.file.config;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.ParserContext;
@@ -27,7 +28,6 @@ import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.file.locking.NioFileLocker;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
/**
* Parser for the &lt;inbound-channel-adapter&gt; element of the 'file' namespace.
@@ -53,9 +53,8 @@ public class FileInboundChannelAdapterParser extends AbstractPollingInboundChann
builder.addPropertyReference("locker", lockerBeanName);
}
builder.addPropertyReference("filter", filterBeanName);
String beanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
return new RuntimeBeanReference(beanName);
return builder.getBeanDefinition();
}
private String registerLocker(Element element, ParserContext parserContext) {

View File

@@ -60,9 +60,8 @@ public class FtpParserInboundTests {
fail("BeansException expected.");
}
catch (BeansException e) {
assertThat(e, Matchers.instanceOf(BeanCreationException.class));
Throwable cause = e.getCause();
assertThat(cause, Matchers.instanceOf(BeanCreationException.class));
cause = cause.getCause();
assertThat(cause, Matchers.instanceOf(MessagingException.class));
cause = cause.getCause();
assertThat(cause, Matchers.instanceOf(FileNotFoundException.class));

View File

@@ -38,7 +38,7 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
* {@link SqlParameterSourceFactory} abstraction, the default implementation of which wraps the message so that its bean
* properties can be referred to by name in the query string E.g.
*
* <pre>
* <pre class="code">
* INSERT INTO FOOS (MESSAGE_ID, PAYLOAD) VALUES (:headers[id], :payload)
* </pre>
*
@@ -101,6 +101,7 @@ public class JdbcMessageHandler extends AbstractMessageHandler {
/**
* Executes the update, passing the message into the {@link SqlParameterSourceFactory}.
*/
@Override
protected void handleMessageInternal(Message<?> message) throws MessageRejectedException, MessageHandlingException,
MessageDeliveryException {
List<? extends Map<String, Object>> keys = executeUpdateQuery(message, keysGenerated);

View File

@@ -76,12 +76,6 @@ import org.springframework.util.StringUtils;
* This message store shall be used for message channels only.
* </p>
* <p>
* <strong>NOTICE</strong>: This implementation may change for Spring Integration
* 3.0. It is provided for use-cases where the current {@link JdbcMessageStore}
* is not delivering the desired performance characteristics.
* </p>
*
* <p>
* As such, the {@link JdbcChannelMessageStore} uses database specific SQL queries.
* </p>
* <p>
@@ -89,8 +83,8 @@ import org.springframework.util.StringUtils;
* database table only. The SQL scripts to create the necessary table are packaged
* under <code>org/springframework/integration/jdbc/messagestore/channel/schema-*.sql</code>,
* where <code>*</code> denotes the target database type.
* </p
* >
* </p>
*
* @author Gunnar Hillert
* @author Artem Bilan
* @since 2.2
@@ -331,7 +325,7 @@ public class JdbcChannelMessageStore extends AbstractMessageGroupStore implement
* <p>For this to work, you must setup the corresponding
* {@link TransactionSynchronizationFactory}:</p>
*
* <pre>
* <pre class="code">
* {@code
* <int:transaction-synchronization-factory id="syncFactory">
* <int:after-commit expression="@jdbcChannelMessageStore.removeFromIdCache(headers.id.toString())" />
@@ -343,7 +337,7 @@ public class JdbcChannelMessageStore extends AbstractMessageGroupStore implement
* This {@link TransactionSynchronizationFactory} is then referenced in the
* transaction configuration of the poller:
*
* <pre>
* <pre class="code">
* {@code
* <int:poller fixed-delay="300" receive-timeout="500"
* max-messages-per-poll="1" task-executor="pool">

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2013 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.
@@ -19,18 +19,15 @@ package org.springframework.integration.jms.config;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.util.StringUtils;
/**
* Parser for the &lt;inbound-channel-adapter/&gt; element of the 'jms' namespace.
*
* Parser for the &lt;inbound-channel-adapter/&gt; element of the 'jms' namespace.
*
* @author Mark Fisher
*/
public class JmsInboundChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser {
@@ -87,9 +84,7 @@ public class JmsInboundChannelAdapterParser extends AbstractPollingInboundChanne
builder.addPropertyReference(JmsAdapterParserUtils.HEADER_MAPPER_PROPERTY, headerMapper);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "selector", "messageSelector");
BeanDefinition beanDefinition = builder.getBeanDefinition();
String beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, parserContext.getRegistry());
return new BeanComponentDefinition(beanDefinition, beanName);
return builder.getBeanDefinition();
}
}

View File

@@ -95,7 +95,8 @@ public class JmsMessageDrivenEndpointParser extends AbstractSingleBeanDefinition
String listenerBeanName = this.parseMessageListener(element, parserContext);
builder.addConstructorArgReference(containerBeanName);
builder.addConstructorArgReference(listenerBeanName);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-startup");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE);
}
private String parseMessageListenerContainer(Element element, ParserContext parserContext) {

View File

@@ -13,18 +13,18 @@
http://www.springframework.org/schema/integration/jmx/spring-integration-jmx.xsd">
<context:mbean-server id="mbs" />
<context:mbean-export server="mbs" default-domain="test.PollingAdapterMBean"/>
<int:channel id="testChannel" />
<int:inbound-channel-adapter channel="testChannel" method="get">
<int:inbound-channel-adapter id="adapter" channel="testChannel" method="get">
<int:poller fixed-rate="5000" max-messages-per-poll="1"/>
<bean class="org.springframework.integration.jmx.config.PollingAdapterMBeanTests$Source"/>
</int:inbound-channel-adapter>
<int:logging-channel-adapter channel="testChannel"/>
<jmx:mbean-export id="integrationMbeanExporter" server="mbs" default-domain="test.PollingAdapterMBean"/>
</beans>

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2002-2010 the original author or authors.
*
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
@@ -36,7 +36,7 @@ public class PollingAdapterMBeanTests {
@Autowired
private MBeanServer server;
@Test
public void testMessageSourceMBeanExists() throws Exception {
// System.err.println(server.queryNames(new ObjectName("*:type=MessageSource,*"), null));

View File

@@ -15,13 +15,11 @@
*/
package org.springframework.integration.mongodb.config;
import org.springframework.beans.factory.config.BeanDefinition;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
@@ -52,8 +50,7 @@ public class MongoDbInboundChannelAdapterParser extends AbstractPollingInboundCh
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "entity-class");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expect-single-result");
String beanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
return new RuntimeBeanReference(beanName);
return builder.getBeanDefinition();
}
}

View File

@@ -23,20 +23,21 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.redis.inbound.RedisInboundChannelAdapter;
import org.springframework.util.StringUtils;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Gary Russell
* @author Artem Bilan
* @since 2.1
*/
public class RedisInboundChannelAdapterParser extends AbstractChannelAdapterParser {
@Override
protected AbstractBeanDefinition doParse(Element element, ParserContext parserContext, String channelName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.redis.inbound.RedisInboundChannelAdapter");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RedisInboundChannelAdapter.class);
String connectionFactory = element.getAttribute("connection-factory");
if (!StringUtils.hasText(connectionFactory)) {
connectionFactory = "redisConnectionFactory";
@@ -46,7 +47,8 @@ public class RedisInboundChannelAdapterParser extends AbstractChannelAdapterPars
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "topics");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converter");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer", true);
return builder.getBeanDefinition();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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,6 +21,7 @@ import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHa
* Namespace handler for Spring Integration's 'redis' namespace.
*
* @author Oleg Zhurakousky
* @author Artem Bilan
* @since 2.1
*/
public class RedisNamespaceHandler extends AbstractIntegrationNamespaceHandler {
@@ -31,5 +32,7 @@ public class RedisNamespaceHandler extends AbstractIntegrationNamespaceHandler {
registerBeanDefinitionParser("store-inbound-channel-adapter", new RedisStoreInboundChannelAdapterParser());
registerBeanDefinitionParser("store-outbound-channel-adapter", new RedisStoreOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new RedisOutboundChannelAdapterParser());
registerBeanDefinitionParser("queue-inbound-channel-adapter", new RedisQueueInboundChannelAdapterParser());
registerBeanDefinitionParser("queue-outbound-channel-adapter", new RedisQueueOutboundChannelAdapterParser());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -18,32 +18,41 @@ package org.springframework.integration.redis.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
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.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler;
import org.springframework.util.StringUtils;
/**
* Parser for the {@code <outbound-channel-adapter/>} component.
*
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Artem Bilan
* @since 2.1
*/
public class RedisOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser {
@Override
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.redis.outbound.RedisPublishingMessageHandler");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RedisPublishingMessageHandler.class);
String connectionFactory = element.getAttribute("connection-factory");
if (!StringUtils.hasText(connectionFactory)) {
connectionFactory = "redisConnectionFactory";
}
builder.addConstructorArgReference(connectionFactory);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "topic", "defaultTopic");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converter");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer");
BeanDefinition topicExpression = IntegrationNamespaceUtils
.createExpressionDefinitionFromValueOrExpression("topic", "topic-expression", parserContext, element, true);
builder.addPropertyValue("topicExpression", topicExpression);
return builder.getBeanDefinition();
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2013 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.redis.config;
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.xml.AbstractChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint;
import org.springframework.util.StringUtils;
/**
* Parser for the &lt;queue-inbound-channel-adapter&gt; element of the 'redis' namespace.
*
* @author Artem Bilan
* @since 3.0
*/
public class RedisQueueInboundChannelAdapterParser extends AbstractChannelAdapterParser {
@Override
protected AbstractBeanDefinition doParse(Element element, ParserContext parserContext, String channelName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RedisQueueMessageDrivenEndpoint.class);
builder.addConstructorArgValue(element.getAttribute("queue"));
String connectionFactory = element.getAttribute("connection-factory");
if (!StringUtils.hasText(connectionFactory)) {
connectionFactory = "redisConnectionFactory";
}
builder.addConstructorArgReference(connectionFactory);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer", true);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "task-executor");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expect-message");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "receive-timeout");
builder.addPropertyReference("outputChannel", channelName);
return builder.getBeanDefinition();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013 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.redis.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
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.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter;
import org.springframework.util.StringUtils;
/**
* Parser for the &lt;int-redis:queue-outbound-channel-adapter&gt; element.
*
* @author Artem Bilan
* @since 3.0
*/
public class RedisQueueOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser {
@Override
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RedisQueueOutboundChannelAdapter.class);
BeanDefinition queueExpression = IntegrationNamespaceUtils
.createExpressionDefinitionFromValueOrExpression("queue", "queue-expression", parserContext, element, true);
builder.addConstructorArgValue(queueExpression);
String connectionFactory = element.getAttribute("connection-factory");
if (!StringUtils.hasText(connectionFactory)) {
connectionFactory = "redisConnectionFactory";
}
builder.addConstructorArgReference(connectionFactory);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-payload");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer");
return builder.getBeanDefinition();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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,9 +20,7 @@ import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
@@ -62,9 +60,8 @@ public class RedisStoreInboundChannelAdapterParser extends AbstractPollingInboun
parserContext, element, atLeastOneRequired);
builder.addConstructorArgValue(expressionDef);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "collection-type");
String beanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
return new RuntimeBeanReference(beanName);
return builder.getBeanDefinition();
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2013 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.redis.event;
/**
* @author Artem Bilan
* @since 3.0
*/
@SuppressWarnings("serial")
public class RedisExceptionEvent extends RedisIntegrationEvent {
public RedisExceptionEvent(Object source, Throwable cause) {
super(source, cause);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2013 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.redis.event;
import org.springframework.integration.event.IntegrationEvent;
/**
* @author Artem Bilan
* @since 3.0
*
*/
@SuppressWarnings("serial")
public abstract class RedisIntegrationEvent extends IntegrationEvent {
public RedisIntegrationEvent(Object source) {
super(source);
}
public RedisIntegrationEvent(Object source, Throwable cause) {
super(source, cause);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Events generated by the redis module
*/
package org.springframework.integration.redis.event;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2007-2012 the original author or authors
* Copyright 2007-2013 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.
@@ -34,9 +34,9 @@ import org.springframework.util.Assert;
/**
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.1
*/
@SuppressWarnings("rawtypes")
public class RedisInboundChannelAdapter extends MessageProducerSupport {
private final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
@@ -53,7 +53,6 @@ public class RedisInboundChannelAdapter extends MessageProducerSupport {
}
public void setSerializer(RedisSerializer<?> serializer) {
Assert.notNull(serializer, "'serializer' must not be null");
this.serializer = serializer;
}
@@ -100,17 +99,16 @@ public class RedisInboundChannelAdapter extends MessageProducerSupport {
this.container.stop();
}
@SuppressWarnings("unchecked")
private Message<?> convertMessage(String s) {
return this.messageConverter.toMessage(s, null);
private Message<?> convertMessage(Object object) {
return this.messageConverter.toMessage(object, null);
}
private class MessageListenerDelegate {
@SuppressWarnings("unused")
public void handleMessage(String s) {
sendMessage(convertMessage(s));
public void handleMessage(Object object) {
sendMessage(convertMessage(object));
}
}

View File

@@ -15,10 +15,12 @@
*/
package org.springframework.integration.redis.inbound;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
@@ -27,7 +29,9 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.redis.event.RedisExceptionEvent;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
import org.springframework.integration.util.ErrorHandlingTaskExecutor;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedOperation;
@@ -44,15 +48,19 @@ import org.springframework.util.Assert;
* @since 3.0
*/
@ManagedResource
public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport implements ApplicationEventPublisherAware {
public static final long DEFAULT_RECEIVE_TIMEOUT = 1000;
public static final long DEFAULT_RECOVERY_INTERVAL = 5000;
private final BoundListOperations<String, byte[]> boundListOperations;
private MessageChannel errorChannel;
private volatile ApplicationEventPublisher applicationEventPublisher;
private volatile TaskExecutor taskExecutor;
private volatile MessageChannel errorChannel;
private volatile Executor taskExecutor;
private volatile RedisSerializer<?> serializer = new JdkSerializationRedisSerializer();
@@ -60,6 +68,8 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
private volatile long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;
private volatile long recoveryInterval = DEFAULT_RECOVERY_INTERVAL;
private volatile boolean active;
private volatile boolean listening;
@@ -79,6 +89,11 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
this.boundListOperations = template.boundListOps(queueName);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void setSerializer(RedisSerializer<?> serializer) {
this.serializer = serializer;
}
@@ -117,7 +132,7 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
this.receiveTimeout = receiveTimeout;
}
public void setTaskExecutor(TaskExecutor taskExecutor) {
public void setTaskExecutor(Executor taskExecutor) {
this.taskExecutor = taskExecutor;
}
@@ -127,6 +142,10 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
this.errorChannel = errorChannel;
}
public void setRecoveryInterval(long recoveryInterval) {
this.recoveryInterval = recoveryInterval;
}
@Override
protected void onInit() {
super.onInit();
@@ -138,7 +157,8 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
this.taskExecutor = new SimpleAsyncTaskExecutor((beanName == null ? "" : beanName + "-") + this.getComponentType());
}
if (!(this.taskExecutor instanceof ErrorHandlingTaskExecutor)) {
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
MessagePublishingErrorHandler errorHandler =
new MessagePublishingErrorHandler(new BeanFactoryChannelResolver(this.getBeanFactory()));
errorHandler.setDefaultErrorChannel(this.errorChannel);
this.taskExecutor = new ErrorHandlingTaskExecutor(this.taskExecutor, errorHandler);
}
@@ -146,14 +166,24 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
@Override
public String getComponentType() {
return "int-redis:message-driven-channel-adapter";
return "redis:queue-inbound-channel-adapter";
}
@SuppressWarnings("unchecked")
private void popMessageAndSend() {
Message<Object> message = null;
byte[] value = this.boundListOperations.rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS);
byte[] value = null;
try {
value = this.boundListOperations.rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
logger.error("Failed to execute listening task. Will attempt to resubmit in " + this.recoveryInterval + " milliseconds.", e);
this.listening = false;
this.sleepBeforeRecoveryAttempt();
this.publishException(e);
return;
}
if (value != null) {
if (this.expectMessage) {
@@ -186,6 +216,32 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
}
}
/**
* Sleep according to the specified recovery interval.
* Called between recovery attempts.
*/
private void sleepBeforeRecoveryAttempt() {
if (this.recoveryInterval > 0) {
try {
Thread.sleep(this.recoveryInterval);
}
catch (InterruptedException e) {
logger.debug("Thread interrupted while sleeping the recovery interval");
}
}
}
private void publishException(Exception e) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new RedisExceptionEvent(this, e));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No application event publisher for exception: " + e.getMessage());
}
}
}
private void restart() {
this.taskExecutor.execute(new ListenerTask());
}

View File

@@ -11,13 +11,13 @@
* specific language governing permissions and limitations under the License.
*/
package org.springframework.integration.redis.store.metadata;
package org.springframework.integration.redis.metadata;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.integration.store.metadata.MetadataStore;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.util.Assert;
/**
@@ -65,4 +65,13 @@ public class RedisMetadataStore implements MetadataStore {
BoundValueOperations<String, String> ops = this.redisTemplate.boundValueOps(key);
return ops.get();
}
@Override
public String remove(String key) {
Assert.notNull(key, "'key' must not be null.");
String value = this.get(key);
this.redisTemplate.delete(key);
return value;
}
}

View File

@@ -0,0 +1,5 @@
/**
* Provides support for Redis-based
* {@link org.springframework.integration.metadata.MetadataStore}s.
*/
package org.springframework.integration.redis.metadata;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2007-2011 the original author or authors
* Copyright 2007-2013 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.
@@ -17,9 +17,13 @@
package org.springframework.integration.redis.outbound;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.expression.IntegrationEvaluationContextAware;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.support.converter.SimpleMessageConverter;
import org.springframework.messaging.Message;
@@ -28,22 +32,32 @@ import org.springframework.util.Assert;
/**
* @author Mark Fisher
* @author Artem Bilan
* @since 2.1
*/
@SuppressWarnings("rawtypes")
public class RedisPublishingMessageHandler extends AbstractMessageHandler {
public class RedisPublishingMessageHandler extends AbstractMessageHandler implements IntegrationEvaluationContextAware {
private final StringRedisTemplate template;
private final RedisTemplate<?, ?> template;
private volatile EvaluationContext evaluationContext;
private volatile MessageConverter messageConverter = new SimpleMessageConverter();
private volatile String defaultTopic;
private volatile RedisSerializer<?> serializer = new StringRedisSerializer();
private volatile Expression topicExpression;
public RedisPublishingMessageHandler(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "connectionFactory must not be null");
this.template = new StringRedisTemplate(connectionFactory);
this.template = new RedisTemplate<Object, Object>();
this.template.setConnectionFactory(connectionFactory);
this.template.setEnableDefaultSerializer(false);
this.template.afterPropertiesSet();
}
@Override
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
}
public void setSerializer(RedisSerializer<?> serializer) {
@@ -56,28 +70,42 @@ public class RedisPublishingMessageHandler extends AbstractMessageHandler {
this.messageConverter = messageConverter;
}
/**
* @deprecated in favor of {@link #setTopicExpression(Expression)} or {@link #setTopic(String)}
*/
@Deprecated
public void setDefaultTopic(String defaultTopic) {
this.defaultTopic = defaultTopic;
Assert.hasText(defaultTopic, "'defaultTopic' must not be an empty string.");
this.setTopicExpression(new LiteralExpression(defaultTopic));
}
private String determineTopic(Message<?> message) {
// TODO: add support for determining topic by evaluating SpEL against the Message
Assert.hasText(this.defaultTopic, "Failed to determine Redis topic " +
"from Message, and no defaultTopic has been provided.");
return this.defaultTopic;
public void setTopic(String topic) {
Assert.hasText(topic, "'topic' must not be an empty string.");
this.setTopicExpression(new LiteralExpression(topic));
}
@SuppressWarnings("unchecked")
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
String topic = this.determineTopic(message);
Object value = this.messageConverter.fromMessage(message, Object.class);
this.template.convertAndSend(topic, value.toString());
public void setTopicExpression(Expression topicExpression) {
Assert.notNull(topicExpression, "'topicExpression' must not be null.");
this.topicExpression = topicExpression;
}
@Override
protected void onInit() throws Exception {
this.template.setValueSerializer(this.serializer);
this.template.afterPropertiesSet();
Assert.notNull(topicExpression, "'topicExpression' must not be null.");
}
@Override
@SuppressWarnings("unchecked")
protected void handleMessageInternal(Message<?> message) throws Exception {
String topic = this.topicExpression.getValue(this.evaluationContext, message, String.class);
Object value = this.messageConverter.fromMessage(message, null);
if (value instanceof byte[]) {
this.template.convertAndSend(topic, value);
}
else {
this.template.convertAndSend(topic, ((RedisSerializer<Object>) this.serializer).serialize(value));
}
}
}

View File

@@ -43,7 +43,7 @@ public class RedisQueueOutboundChannelAdapter extends AbstractMessageHandler imp
private final Expression queueNameExpression;
private EvaluationContext evaluationContext;
private volatile EvaluationContext evaluationContext;
private volatile boolean extractPayload = true;
@@ -84,7 +84,7 @@ public class RedisQueueOutboundChannelAdapter extends AbstractMessageHandler imp
@Override
public String getComponentType() {
return "int-redis:outbound-channel-adapter";
return "redis:outbound-channel-adapter";
}
@Override

View File

@@ -1,5 +0,0 @@
/**
* Provides support for Redis-based
* {@link org.springframework.integration.store.metadata.MetadataStore}s.
*/
package org.springframework.integration.redis.store.metadata;

View File

@@ -82,10 +82,7 @@
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a Spring TaskExecutor (or standard JDK 1.5+ Executor) for executing
JMS listener invokers. Default is a SimpleAsyncTaskExecutor in case of a
DefaultMessageListenerContainer, using internally managed threads. For a
SimpleMessageListenerContainer, listeners will always get invoked within the
JMS provider's receive thread by default.
Redis listener invokers. Default is a SimpleAsyncTaskExecutor.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
@@ -177,7 +174,9 @@
<xsd:annotation>
<xsd:appinfo>
<xsd:documentation>
Reference to an instance of org.springframework.data.redis.serializer.RedisSerializer
Reference to an instance of org.springframework.data.redis.serializer.RedisSerializer.
This attribute can be an empty string, which results in 'null' being used by the underlying adapter,
meaning no serializer is used and the raw byte[] will be the message payload.
</xsd:documentation>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.data.redis.serializer.RedisSerializer"/>
@@ -200,7 +199,22 @@
<xsd:sequence>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="topic" type="xsd:string"/>
<xsd:attribute name="topic" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specifies the Redis topic.
This attribute is mutually exclusive with the 'topic-expression' attribute.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="topic-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specifies the SpEL expression to determine the Redis topic using the Message at runtime.
This attribute is mutually exclusive with the 'topic' attribute.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="message-converter" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
@@ -329,6 +343,146 @@
</xsd:complexType>
</xsd:element>
<xsd:element name="queue-inbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Defines a Message Driven Endpoint for listening a Redis queue.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="redisAdapterType">
<xsd:attribute name="queue" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
Redis queue name.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="error-channel" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Identifies the channel to which error messages will be sent if a failure occurs in this
Endpoint's process. If no "error-channel" reference is provided, this Endpoint will
propagate Exceptions to the caller. To completely suppress Exceptions, provide a
reference to the "nullChannel" here.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="serializer" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<xsd:documentation>
Reference to an instance of org.springframework.data.redis.serializer.RedisSerializer.
It can be specified as an empty String value, which means the Endpoint's 'serializer' property is
set to 'null', in which case the Message will contain the raw byte[] payload.
</xsd:documentation>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.data.redis.serializer.RedisSerializer"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="receive-timeout" type="xsd:string" default="1000">
<xsd:annotation>
<xsd:documentation>
Specify the timeout in milliseconds to wait for the result of the
'rightPop' operation on Redis queue.
Default is 1 second.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="expect-message" type="xsd:string" default="false">
<xsd:annotation>
<xsd:documentation>
When true, specifies that the 'byte[]' from a Redis message should be deserialized
as an entire Spring Integration Message. Otherwise the data becomes just the
payload of the message (deserialized or not).
If this attribute is 'true', the 'serializer' must not be an empty String.
Default is 'false'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="task-executor" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a Spring TaskExecutor (or standard JDK 1.5+ Executor) for executing
the listening task on the Redis queue. Default is a SimpleAsyncTaskExecutor.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java.util.concurrent.Executor"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="queue-outbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Defines an outbound Redis Queue Message-sending Channel Adapter.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="redisAdapterType">
<xsd:sequence>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="queue" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specifies the name of the Redis queue.
This attribute is mutually exclusive with 'queue-expression' attribute.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="queue-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specifies the expression to determine the name of the Redis queue
against the Message at runtime.
This attribute is mutually exclusive with 'queue' attribute.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specifies the order for invocation when this adapter is connected as a
subscriber to a SubscribableChannel.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="serializer" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<xsd:documentation>
Reference to an instance of org.springframework.data.redis.serializer.RedisSerializer
</xsd:documentation>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.data.redis.serializer.RedisSerializer"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-payload" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Specifies if the Message payload or the entire (serialized) Message will be send to the Redis queue.
Default is 'true'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="redisAdapterType">
<xsd:annotation>
<xsd:documentation>

View File

@@ -30,7 +30,6 @@ import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@@ -53,7 +52,7 @@ public class SubscribableRedisChannelTests extends RedisAvailableTests {
@Test
@RedisAvailable
public void pubSubChannelTest() throws Exception{
public void pubSubChannelTest() throws Exception {
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
SubscribableRedisChannel channel = new SubscribableRedisChannel(connectionFactory, "si.test.channel");
@@ -61,14 +60,7 @@ public class SubscribableRedisChannelTests extends RedisAvailableTests {
channel.afterPropertiesSet();
channel.start();
RedisConnection connection = TestUtils.getPropertyValue(channel, "container.subscriptionTask.connection",
RedisConnection.class);
int n = 0;
while (n++ < 100 && !connection.isSubscribed()) {
Thread.sleep(100);
}
assertTrue(n < 100);
this.awaitContainerSubscribed(TestUtils.getPropertyValue(channel, "container", RedisMessageListenerContainer.class));
final CountDownLatch latch = new CountDownLatch(3);
MessageHandler handler = new MessageHandler() {

View File

@@ -32,4 +32,7 @@
<int:bridge input-channel="autoChannel" output-channel="nullChannel"/>
<int-redis:inbound-channel-adapter id="withoutSerializer" topics="foo" serializer=""/>
</beans>

View File

@@ -17,6 +17,8 @@
package org.springframework.integration.redis.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import org.junit.Test;
@@ -68,6 +70,10 @@ public class RedisInboundChannelAdapterParserTests extends RedisAvailableTests {
Object converterBean = context.getBean("testConverter");
assertEquals(converterBean, accessor.getPropertyValue("messageConverter"));
assertEquals(context.getBean("serializer"), accessor.getPropertyValue("serializer"));
Object bean = context.getBean("withoutSerializer.adapter");
assertNotNull(bean);
assertNull(TestUtils.getPropertyValue(bean, "serializer"));
}
@Test

View File

@@ -0,0 +1,44 @@
<?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-redis="http://www.springframework.org/schema/integration/redis"
xmlns:task="http://www.springframework.org/schema/task"
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/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<bean id="redisConnectionFactory"
class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<property name="port" value="#{T(org.springframework.integration.redis.rules.RedisAvailableRule).REDIS_PORT}"/>
</bean>
<bean id="customRedisConnectionFactory" parent="redisConnectionFactory"/>
<int-redis:queue-inbound-channel-adapter id="defaultAdapter" queue="si.test.Int3017.Inbound1"/>
<int:channel id="sendChannel"/>
<int-redis:queue-inbound-channel-adapter id="customAdapter"
queue="si.test.Int3017.Inbound2"
channel="sendChannel"
connection-factory="customRedisConnectionFactory"
expect-message="true"
serializer="serializer"
error-channel="errorChannel"
receive-timeout="2000"
task-executor="executor"
auto-startup="false"
phase="100"/>
<bean id="executor" class="org.springframework.integration.util.ErrorHandlingTaskExecutor">
<constructor-arg ref="threadPoolTaskExecutor"/>
<constructor-arg value="#{T(org.springframework.scheduling.support.TaskUtils).LOG_AND_SUPPRESS_ERROR_HANDLER}"/>
</bean>
<task:executor id="threadPoolTaskExecutor" pool-size="5"/>
<bean id="serializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</beans>

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2013 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.redis.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
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.core.task.TaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.util.ErrorHandlingTaskExecutor;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Artem Bilan
* @since 3.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class RedisMessageDrivenEndpointParserTests {
@Autowired
@Qualifier("redisConnectionFactory")
private RedisConnectionFactory connectionFactory;
@Autowired
@Qualifier("customRedisConnectionFactory")
private RedisConnectionFactory customRedisConnectionFactory;
@Autowired
@Qualifier("defaultAdapter.adapter")
private RedisQueueMessageDrivenEndpoint defaultAdapter;
@Autowired
@Qualifier("defaultAdapter")
private MessageChannel defaultAdapterChannel;
@Autowired
@Qualifier("customAdapter")
private RedisQueueMessageDrivenEndpoint customAdapter;
@Autowired
@Qualifier("errorChannel")
private MessageChannel errorChannel;
@Autowired
@Qualifier("sendChannel")
private MessageChannel sendChannel;
@Autowired
@Qualifier("executor")
private TaskExecutor taskExecutor;
@Autowired
private RedisSerializer<?> serializer;
@Test
public void testInt3017DefaultConfig() {
assertSame(this.connectionFactory, TestUtils.getPropertyValue(this.defaultAdapter, "boundListOperations.ops.template.connectionFactory"));
assertEquals("si.test.Int3017.Inbound1", TestUtils.getPropertyValue(this.defaultAdapter, "boundListOperations.key"));
assertFalse(TestUtils.getPropertyValue(this.defaultAdapter, "expectMessage", Boolean.class));
assertEquals(new Long(1000), TestUtils.getPropertyValue(this.defaultAdapter, "receiveTimeout", Long.class));
assertNull(TestUtils.getPropertyValue(this.defaultAdapter, "errorChannel"));
assertThat(TestUtils.getPropertyValue(this.defaultAdapter, "taskExecutor"), Matchers.instanceOf(ErrorHandlingTaskExecutor.class));
assertThat(TestUtils.getPropertyValue(this.defaultAdapter, "serializer"), Matchers.instanceOf(JdkSerializationRedisSerializer.class));
assertTrue(TestUtils.getPropertyValue(this.defaultAdapter, "autoStartup", Boolean.class));
assertSame(this.defaultAdapterChannel, TestUtils.getPropertyValue(this.defaultAdapter, "outputChannel"));
}
@Test
public void testInt3017CustomConfig() {
assertSame(this.customRedisConnectionFactory, TestUtils.getPropertyValue(this.customAdapter, "boundListOperations.ops.template.connectionFactory"));
assertEquals("si.test.Int3017.Inbound2", TestUtils.getPropertyValue(this.customAdapter, "boundListOperations.key"));
assertTrue(TestUtils.getPropertyValue(this.customAdapter, "expectMessage", Boolean.class));
assertEquals(new Long(2000), TestUtils.getPropertyValue(this.customAdapter, "receiveTimeout", Long.class));
assertSame(this.errorChannel, TestUtils.getPropertyValue(this.customAdapter, "errorChannel"));
assertSame(this.taskExecutor, TestUtils.getPropertyValue(this.customAdapter, "taskExecutor"));
assertSame(this.serializer, TestUtils.getPropertyValue(this.customAdapter, "serializer"));
assertFalse(TestUtils.getPropertyValue(this.customAdapter, "autoStartup", Boolean.class));
assertEquals(new Integer(100), TestUtils.getPropertyValue(this.customAdapter, "phase", Integer.class));
assertSame(this.sendChannel, TestUtils.getPropertyValue(this.customAdapter, "outputChannel"));
}
}

View File

@@ -11,7 +11,7 @@
<int-redis:outbound-channel-adapter id="outboundAdapter"
channel="sendChannel"
topic="foo"
topic-expression="headers['topic'] ?: 'foo'"
message-converter="testConverter"
serializer="serializer"/>
@@ -21,6 +21,12 @@
<int:queue/>
</int:channel>
<int-redis:inbound-channel-adapter channel="barChannel" topics="bar"/>
<int:channel id="barChannel">
<int:queue/>
</int:channel>
<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<property name="port" value="#{T(org.springframework.integration.redis.rules.RedisAvailableRule).REDIS_PORT}"/>
</bean>

View File

@@ -25,15 +25,17 @@ import org.junit.runner.RunWith;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.expression.Expression;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.converter.SimpleMessageConverter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -59,7 +61,9 @@ public class RedisOutboundChannelAdapterParserTests extends RedisAvailableTests{
new DirectFieldAccessor(adapter).getPropertyValue("handler");
assertEquals("outboundAdapter", adapter.getComponentName());
DirectFieldAccessor accessor = new DirectFieldAccessor(handler);
assertEquals("foo", accessor.getPropertyValue("defaultTopic"));
Object topicExpression = accessor.getPropertyValue("topicExpression");
assertNotNull(topicExpression);
assertEquals("headers['topic'] ?: 'foo'", ((Expression) topicExpression).getExpressionString());
Object converterBean = context.getBean("testConverter");
assertEquals(converterBean, accessor.getPropertyValue("messageConverter"));
assertEquals(context.getBean("serializer"), accessor.getPropertyValue("serializer"));
@@ -74,6 +78,13 @@ public class RedisOutboundChannelAdapterParserTests extends RedisAvailableTests{
Message<?> message = receiveChannel.receive(5000);
assertNotNull(message);
assertEquals("Hello Redis", message.getPayload());
sendChannel = context.getBean("sendChannel", MessageChannel.class);
sendChannel.send(MessageBuilder.withPayload("Hello Redis").setHeader("topic", "bar").build());
receiveChannel = context.getBean("barChannel", QueueChannel.class);
message = receiveChannel.receive(5000);
assertNotNull(message);
assertEquals("Hello Redis", message.getPayload());
}
@Test //INT-2275

View File

@@ -0,0 +1,29 @@
<?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-redis="http://www.springframework.org/schema/integration/redis"
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/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd">
<bean id="redisConnectionFactory"
class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<property name="port" value="#{T(org.springframework.integration.redis.rules.RedisAvailableRule).REDIS_PORT}"/>
</bean>
<bean id="customRedisConnectionFactory" parent="redisConnectionFactory"/>
<int:channel id="sendChannel"/>
<int-redis:queue-outbound-channel-adapter id="defaultAdapter" channel="sendChannel" queue="foo"/>
<int-redis:queue-outbound-channel-adapter id="customAdapter" channel="sendChannel"
queue-expression="headers['redis_queue']"
extract-payload="false"
serializer="serializer"
connection-factory="customRedisConnectionFactory"/>
<bean id="serializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</beans>

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013 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.redis.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
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.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.expression.Expression;
import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Artem Bilan
* @since 3.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class RedisQueueOutboundChannelAdapterParserTests {
@Autowired
@Qualifier("redisConnectionFactory")
private RedisConnectionFactory connectionFactory;
@Autowired
@Qualifier("customRedisConnectionFactory")
private RedisConnectionFactory customRedisConnectionFactory;
@Autowired
@Qualifier("defaultAdapter.handler")
private RedisQueueOutboundChannelAdapter defaultAdapter;
@Autowired
@Qualifier("customAdapter.handler")
private RedisQueueOutboundChannelAdapter customAdapter;
@Autowired
private RedisSerializer<?> serializer;
@Test
public void testInt3017DefaultConfig() {
assertSame(this.connectionFactory, TestUtils.getPropertyValue(this.defaultAdapter, "template.connectionFactory"));
assertEquals("foo", TestUtils.getPropertyValue(this.defaultAdapter, "queueNameExpression", Expression.class).getExpressionString());
assertTrue(TestUtils.getPropertyValue(this.defaultAdapter, "extractPayload", Boolean.class));
assertFalse(TestUtils.getPropertyValue(this.defaultAdapter, "serializerExplicitlySet", Boolean.class));
}
@Test
public void testInt3017CustomConfig() {
assertSame(this.customRedisConnectionFactory, TestUtils.getPropertyValue(this.customAdapter, "template.connectionFactory"));
assertEquals("headers['redis_queue']", TestUtils.getPropertyValue(this.customAdapter, "queueNameExpression", Expression.class).getExpressionString());
assertFalse(TestUtils.getPropertyValue(this.customAdapter, "extractPayload", Boolean.class));
assertTrue(TestUtils.getPropertyValue(this.customAdapter, "serializerExplicitlySet", Boolean.class));
assertSame(this.serializer, TestUtils.getPropertyValue(this.customAdapter, "serializer"));
}
}

View File

@@ -18,15 +18,14 @@ package org.springframework.integration.redis.inbound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.messaging.Message;
@@ -37,12 +36,11 @@ import org.springframework.integration.test.util.TestUtils;
/**
* @author Mark Fisher
* @author Artem Bilan
* @since 2.1
*/
public class RedisInboundChannelAdapterTests extends RedisAvailableTests{
private final Log logger = LogFactory.getLog(this.getClass());
@Test
@RedisAvailable
public void testRedisInboundChannelAdapter() throws Exception {
@@ -59,19 +57,18 @@ public class RedisInboundChannelAdapterTests extends RedisAvailableTests{
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
RedisInboundChannelAdapter adapter = new RedisInboundChannelAdapter(connectionFactory);
adapter.setTopics("testRedisInboundChannelAdapterChannel");
adapter.setTopics(redisChannelName);
adapter.setOutputChannel(channel);
adapter.afterPropertiesSet();
adapter.start();
RedisMessageListenerContainer container = waitUntilSubscribed(adapter);
this.awaitContainerSubscribed(TestUtils.getPropertyValue(adapter, "container", RedisMessageListenerContainer.class));
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
redisTemplate.afterPropertiesSet();
for (int i = 0; i < numToTest; i++) {
String message = "test-" + i + " iteration " + iteration;
redisTemplate.convertAndSend(redisChannelName, message);
logger.debug("Sent " + message);
}
int counter = 0;
for (int i = 0; i < numToTest; i++) {
@@ -85,34 +82,42 @@ public class RedisInboundChannelAdapterTests extends RedisAvailableTests{
}
assertEquals(numToTest, counter);
adapter.stop();
container.stop();
}
/**
* Wait until the container has subscribed to the queue and return a
* reference to it, so we can stop it at the end of the test.
*/
protected RedisMessageListenerContainer waitUntilSubscribed(
RedisInboundChannelAdapter adapter) throws Exception {
RedisMessageListenerContainer container = (RedisMessageListenerContainer) TestUtils
.getPropertyValue(adapter, "container");
Object subscriptionTask = TestUtils.getPropertyValue(container, "subscriptionTask");
RedisConnection connection = (RedisConnection) TestUtils
.getPropertyValue(subscriptionTask, "connection");
int n = 0;
while (true) {
if (n++ > 50) {
fail("RMLC Failed to Subscribe");
}
if (connection.isSubscribed()) {
logger.debug("Subscribed OK");
break;
}
logger.debug("Waiting...");
Thread.sleep(100);
redisChannelName = "testRedisBytesInboundChannelAdapterChannel";
adapter.setTopics(redisChannelName);
adapter.setSerializer(null);
adapter.afterPropertiesSet();
adapter.start();
this.awaitContainerSubscribed(TestUtils.getPropertyValue(adapter, "container", RedisMessageListenerContainer.class));
RedisTemplate<?, ?> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(connectionFactory);
template.setEnableDefaultSerializer(false);
template.afterPropertiesSet();
for (int i = 0; i < numToTest; i++) {
String message = "test-" + i + " iteration " + iteration;
template.convertAndSend(redisChannelName, message.getBytes());
}
Thread.sleep(100); // Wait a little longer due to race condition in connection.isSubscribed()
return container;
counter = 0;
for (int i = 0; i < numToTest; i++) {
Message<?> message = channel.receive(5000);
if (message == null){
throw new RuntimeException("Failed to receive message # " + i + " iteration " + iteration);
}
assertNotNull(message);
Object payload = message.getPayload();
assertThat(payload, Matchers.instanceOf(byte[].class));
assertTrue(new String((byte[]) payload).startsWith("test-"));
counter++;
}
assertEquals(numToTest, counter);
adapter.stop();
}
}

View File

@@ -0,0 +1,44 @@
<?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-redis="http://www.springframework.org/schema/integration/redis"
xmlns:task="http://www.springframework.org/schema/task"
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/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<bean id="redisConnectionFactory"
class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<property name="port" value="#{T(org.springframework.integration.redis.rules.RedisAvailableRule).REDIS_PORT}"/>
</bean>
<int:channel id="fromChannel">
<int:queue/>
</int:channel>
<int-redis:queue-inbound-channel-adapter queue="si.test.Int3017IntegrationInbound"
channel="fromChannel"
expect-message="true"
serializer="testSerializer"/>
<bean id="testSerializer" class="org.springframework.integration.redis.util.CustomJsonSerializer"/>
<int:chain input-channel="symmetricalInputChannel">
<int:payload-serializing-transformer/>
<int-redis:queue-outbound-channel-adapter queue-expression="headers.redis_queue"/>
</int:chain>
<int-redis:queue-inbound-channel-adapter queue="si.test.Int3017IntegrationSymmetrical"
channel="symmetricalRedisChannel"
serializer=""/>
<int:payload-deserializing-transformer input-channel="symmetricalRedisChannel" output-channel="symmetricalOutputChannel"/>
<int:channel id="symmetricalOutputChannel">
<int:queue/>
</int:channel>
</beans>

View File

@@ -18,33 +18,68 @@ package org.springframework.integration.redis.inbound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.event.IntegrationEvent;
import org.springframework.integration.redis.event.RedisExceptionEvent;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Gunnar Hillert
* @author Artem Bilan
* @since 3.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
@Autowired
private RedisConnectionFactory connectionFactory;
@Autowired
private PollableChannel fromChannel;
@Autowired
private MessageChannel symmetricalInputChannel;
@Autowired
private PollableChannel symmetricalOutputChannel;
@Test
@RedisAvailable
@SuppressWarnings("unchecked")
@@ -52,10 +87,8 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
String queueName = "si.test.redisQueueInboundChannelAdapterTests";
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.setEnableDefaultSerializer(false);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
@@ -71,7 +104,8 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
PollableChannel channel = new QueueChannel();
RedisQueueMessageDrivenEndpoint endpoint = new RedisQueueMessageDrivenEndpoint(queueName, connectionFactory);
RedisQueueMessageDrivenEndpoint endpoint = new RedisQueueMessageDrivenEndpoint(queueName, this.connectionFactory);
endpoint.setBeanFactory(Mockito.mock(BeanFactory.class));
endpoint.setOutputChannel(channel);
endpoint.setReceiveTimeout(1000);
endpoint.afterPropertiesSet();
@@ -86,7 +120,6 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
assertEquals(payload2, receive.getPayload());
endpoint.stop();
this.waitUntilListening(endpoint);
}
@Test
@@ -96,10 +129,8 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
final String queueName = "si.test.redisQueueInboundChannelAdapterTests2";
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.setEnableDefaultSerializer(false);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
@@ -115,7 +146,8 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
PollableChannel errorChannel = new QueueChannel();
RedisQueueMessageDrivenEndpoint endpoint = new RedisQueueMessageDrivenEndpoint(queueName, connectionFactory);
RedisQueueMessageDrivenEndpoint endpoint = new RedisQueueMessageDrivenEndpoint(queueName, this.connectionFactory);
endpoint.setBeanFactory(Mockito.mock(BeanFactory.class));
endpoint.setExpectMessage(true);
endpoint.setOutputChannel(channel);
endpoint.setErrorChannel(errorChannel);
@@ -137,21 +169,95 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests {
assertThat(((Exception) receive.getPayload()).getCause().getMessage(),
Matchers.containsString("java.lang.String cannot be cast to org.springframework.messaging.Message"));
endpoint.stop();
this.waitUntilListening(endpoint);
}
@Test
@RedisAvailable
public void testInt3017IntegrationInbound() throws Exception {
public void waitUntilListening(RedisQueueMessageDrivenEndpoint endpoint) throws Exception {
int n = 0;
while (endpoint.isListening()) {
Thread.sleep(100);
if (n++ > 100) {
throw new Exception("RedisQueueMessageDrivenEndpoint failed to stop.");
String payload = new Date().toString();
RedisTemplate<String, String> redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.afterPropertiesSet();
redisTemplate.boundListOps("si.test.Int3017IntegrationInbound").leftPush("{\"payload\":\"" + payload + "\",\"headers\":{}}");
Message<?> receive = this.fromChannel.receive(2000);
assertNotNull(receive);
assertEquals(payload, receive.getPayload());
}
@Test
@RedisAvailable
public void testInt3017IntegrationSymmetrical() throws Exception {
UUID payload = UUID.randomUUID();
Message<UUID> message = MessageBuilder.withPayload(payload)
.setHeader("redis_queue", "si.test.Int3017IntegrationSymmetrical")
.build();
this.symmetricalInputChannel.send(message);
Message<?> receive = this.symmetricalOutputChannel.receive(2000);
assertNotNull(receive);
assertEquals(payload, receive.getPayload());
}
@Test
@RedisAvailable
@SuppressWarnings("unchecked")
public void testInt3196Recovery() throws Exception {
String queueName = "test.si.Int3196Recovery";
QueueChannel channel = new QueueChannel();
final List<ApplicationEvent> exceptionEvents = new ArrayList<ApplicationEvent>();
RedisQueueMessageDrivenEndpoint endpoint = new RedisQueueMessageDrivenEndpoint(queueName, this.connectionFactory);
endpoint.setBeanFactory(Mockito.mock(BeanFactory.class));
endpoint.setApplicationEventPublisher(new ApplicationEventPublisher() {
@Override
public void publishEvent(ApplicationEvent event) {
exceptionEvents.add(event);
}
});
endpoint.setOutputChannel(channel);
endpoint.setReceiveTimeout(100);
endpoint.setRecoveryInterval(200);
endpoint.afterPropertiesSet();
endpoint.start();
((DisposableBean) this.connectionFactory).destroy();
Thread.sleep(300);
assertThat(exceptionEvents.size(), Matchers.greaterThan(0));
for (ApplicationEvent exceptionEvent : exceptionEvents) {
assertThat(exceptionEvent, Matchers.instanceOf(RedisExceptionEvent.class));
assertSame(endpoint, exceptionEvent.getSource());
assertThat(((IntegrationEvent) exceptionEvent).getCause().getClass(),
Matchers.isIn(Arrays.<Class<? extends Throwable>> asList(RedisSystemException.class, RedisConnectionFailureException.class)));
}
((InitializingBean) this.connectionFactory).afterPropertiesSet();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(this.getConnectionFactoryForTest());
redisTemplate.setEnableDefaultSerializer(false);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.afterPropertiesSet();
String payload = "testing";
redisTemplate.boundListOps(queueName).leftPush(payload);
Message<?> receive = channel.receive(1000);
assertNotNull(receive);
assertEquals(payload, receive.getPayload());
endpoint.stop();
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.redis.store.metadata;
package org.springframework.integration.redis.metadata;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -29,6 +29,7 @@ import org.springframework.integration.redis.rules.RedisAvailableTests;
/**
* @author Gunnar Hillert
* @author Artem Bilan
* @since 3.0
*
*/
@@ -143,4 +144,20 @@ public class RedisMetadataStoreTests extends RedisAvailableTests {
fail("Expected an IllegalArgumentException to be thrown.");
}
@Test
@RedisAvailable
public void testRemoveFromMetadataStore(){
RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
RedisMetadataStore metadataStore = new RedisMetadataStore(jcf);
String testKey = "RedisMetadataStoreTests-Remove";
String testValue = "Integration";
metadataStore.put(testKey, testValue);
assertEquals(testValue, metadataStore.remove(testKey));
assertNull(metadataStore.remove(testKey));
}
}

View File

@@ -30,12 +30,14 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.support.MessageBuilder;
/**
* @author Mark Fisher
* @author Artem Bilan
* @since 2.1
*/
public class RedisPublishingMessageHandlerTests extends RedisAvailableTests {
@@ -45,7 +47,7 @@ public class RedisPublishingMessageHandlerTests extends RedisAvailableTests {
public void testRedisPublishingMessageHandler() throws Exception {
int numToTest = 10;
String topic = "si.test.channel";
final CountDownLatch latch = new CountDownLatch(numToTest);
final CountDownLatch latch = new CountDownLatch(numToTest * 2);
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
@@ -59,14 +61,20 @@ public class RedisPublishingMessageHandlerTests extends RedisAvailableTests {
container.afterPropertiesSet();
container.addMessageListener(listener, Collections.<Topic>singletonList(new ChannelTopic(topic)));
container.start();
Thread.sleep(1000);
this.awaitContainerSubscribed(container);
final RedisPublishingMessageHandler handler = new RedisPublishingMessageHandler(connectionFactory);
handler.setDefaultTopic(topic);
handler.setTopicExpression(new LiteralExpression(topic));
for (int i = 0; i < numToTest; i++) {
handler.handleMessage(MessageBuilder.withPayload("test-" + i).build());
}
assertTrue(latch.await(3, TimeUnit.SECONDS));
for (int i = 0; i < numToTest; i++) {
handler.handleMessage(MessageBuilder.withPayload(("test-" + i).getBytes()).build());
}
assertTrue(latch.await(10, TimeUnit.SECONDS));
container.stop();
}
@@ -83,6 +91,7 @@ public class RedisPublishingMessageHandlerTests extends RedisAvailableTests {
public void handleMessage(String s) {
this.latch.countDown();
}
}
}

View File

@@ -0,0 +1,23 @@
<?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-redis="http://www.springframework.org/schema/integration/redis"
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/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd">
<bean id="redisConnectionFactory"
class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<property name="port" value="#{T(org.springframework.integration.redis.rules.RedisAvailableRule).REDIS_PORT}"/>
</bean>
<int:chain input-channel="toRedisQueueChannel">
<int-redis:queue-outbound-channel-adapter queue-expression="payload"
extract-payload="false"
serializer="testSerializer"/>
</int:chain>
<bean id="testSerializer" class="org.springframework.integration.redis.util.CustomJsonSerializer"/>
</beans>

View File

@@ -24,41 +24,58 @@ import java.util.Date;
import java.util.concurrent.TimeUnit;
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.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.json.Jackson2JsonMessageParser;
import org.springframework.integration.support.json.JsonInboundMessageMapper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Gunnar Hillert
* @author Artem Bilan
* @since 3.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class RedisQueueOutboundChannelAdapterTests extends RedisAvailableTests {
@Autowired
private RedisConnectionFactory connectionFactory;
@Autowired
@Qualifier("toRedisQueueChannel")
private MessageChannel sendChannel;
@Test
@RedisAvailable
public void testInt3015Default() throws Exception {
final String queueName = "si.test.testRedisQueueOutboundChannelAdapter";
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
final RedisQueueOutboundChannelAdapter handler = new RedisQueueOutboundChannelAdapter(queueName, connectionFactory);
final RedisQueueOutboundChannelAdapter handler = new RedisQueueOutboundChannelAdapter(queueName, this.connectionFactory);
String payload = "testing";
handler.handleMessage(MessageBuilder.withPayload(payload).build());
RedisTemplate<String, ?> redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.afterPropertiesSet();
Object result = redisTemplate.boundListOps(queueName).rightPop(5000, TimeUnit.MILLISECONDS);
@@ -70,7 +87,7 @@ public class RedisQueueOutboundChannelAdapterTests extends RedisAvailableTests {
handler.handleMessage(MessageBuilder.withPayload(payload2).build());
RedisTemplate<String, ?> redisTemplate2 = new RedisTemplate<String, Object>();
redisTemplate2.setConnectionFactory(connectionFactory);
redisTemplate2.setConnectionFactory(this.connectionFactory);
redisTemplate2.setEnableDefaultSerializer(false);
redisTemplate2.setKeySerializer(new StringRedisSerializer());
redisTemplate2.setValueSerializer(new JdkSerializationRedisSerializer());
@@ -88,16 +105,14 @@ public class RedisQueueOutboundChannelAdapterTests extends RedisAvailableTests {
final String queueName = "si.test.testRedisQueueOutboundChannelAdapter2";
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
final RedisQueueOutboundChannelAdapter handler = new RedisQueueOutboundChannelAdapter(queueName, connectionFactory);
final RedisQueueOutboundChannelAdapter handler = new RedisQueueOutboundChannelAdapter(queueName, this.connectionFactory);
handler.setExtractPayload(false);
Message<String> message = MessageBuilder.withPayload("testing").build();
handler.handleMessage(message);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.setEnableDefaultSerializer(false);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
@@ -116,13 +131,11 @@ public class RedisQueueOutboundChannelAdapterTests extends RedisAvailableTests {
final String queueName = "si.test.testRedisQueueOutboundChannelAdapter2";
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
final RedisQueueOutboundChannelAdapter handler = new RedisQueueOutboundChannelAdapter(queueName, connectionFactory);
final RedisQueueOutboundChannelAdapter handler = new RedisQueueOutboundChannelAdapter(queueName, this.connectionFactory);
handler.setSerializer(new JacksonJsonRedisSerializer<Object>(Object.class));
RedisTemplate<String, ?> redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.afterPropertiesSet();
handler.handleMessage(new GenericMessage<Object>(Arrays.asList("foo", "bar", "baz")));
@@ -140,4 +153,24 @@ public class RedisQueueOutboundChannelAdapterTests extends RedisAvailableTests {
assertEquals("\"test\"", result);
}
@Test
@RedisAvailable
public void testInt3017IntegrationOutbound() throws Exception {
final String queueName = "si.test.Int3017IntegrationOutbound";
GenericMessage<Object> message = new GenericMessage<Object>(queueName);
this.sendChannel.send(message);
RedisTemplate<String, String> redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(this.connectionFactory);
redisTemplate.afterPropertiesSet();
String result = redisTemplate.boundListOps(queueName).rightPop(5000, TimeUnit.MILLISECONDS);
assertNotNull(result);
InboundMessageMapper<String> mapper = new JsonInboundMessageMapper(String.class, new Jackson2JsonMessageParser());
Message<?> resultMessage = mapper.toMessage(result);
assertEquals(message.getPayload(), resultMessage.getPayload());
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.integration.redis.rules;
import static org.junit.Assert.assertTrue;
import java.util.UUID;
import org.junit.Rule;
@@ -28,6 +30,8 @@ import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.integration.test.util.TestUtils;
/**
* @author Oleg Zhurakousky
@@ -40,7 +44,7 @@ public class RedisAvailableTests {
@Rule
public RedisAvailableRule redisAvailableRule = new RedisAvailableRule();
public RedisConnectionFactory getConnectionFactoryForTest(){
protected RedisConnectionFactory getConnectionFactoryForTest(){
LettuceConnectionFactory connectionFactory = RedisAvailableRule.connectionFactoryResource.get();
RedisTemplate<UUID, Object> rt = new RedisTemplate<UUID, Object>();
rt.setConnectionFactory(connectionFactory);
@@ -56,6 +60,17 @@ public class RedisAvailableTests {
return connectionFactory;
}
protected void awaitContainerSubscribed(RedisMessageListenerContainer container) throws Exception {
RedisConnection connection = TestUtils.getPropertyValue(container, "subscriptionTask.connection",
RedisConnection.class);
int n = 0;
while (n++ < 100 && !connection.isSubscribed()) {
Thread.sleep(100);
}
assertTrue("RedisMessageListenerContainer Failed to Subscribe", n < 100);
}
protected void prepareList(RedisConnectionFactory connectionFactory){
StringRedisTemplate redisTemplate = new StringRedisTemplate();

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2013 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.redis.util;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.integration.Message;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.support.json.Jackson2JsonMessageParser;
import org.springframework.integration.support.json.JsonInboundMessageMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Artem Bilan
* @since 3.0
*/
public class CustomJsonSerializer implements RedisSerializer<Message<?>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final InboundMessageMapper<String> mapper =
new JsonInboundMessageMapper(String.class, new Jackson2JsonMessageParser());
@Override
public byte[] serialize(Message<?> message) throws SerializationException {
try {
return this.objectMapper.writeValueAsBytes(message);
}
catch (JsonProcessingException e) {
throw new SerializationException("Fail to serialize 'message' to json.", e);
}
}
@Override
public Message<?> deserialize(byte[] bytes) throws SerializationException {
try {
return mapper.toMessage(new String(bytes));
}
catch (Exception e) {
throw new SerializationException("Fail to deserialize 'message' from json.", e);
}
}
}

View File

@@ -24,6 +24,7 @@ import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.junit.Assert;
import org.springframework.integration.EiMessageHeaderAccessor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
@@ -36,30 +37,34 @@ import org.springframework.messaging.MessageHeaders;
* entry:
* <p>
*
* <pre>
* ANY_HEADER_KEY = &quot;foo&quot;;
* ANY_HEADER_VALUE = &quot;bar&quot;;
* <pre class="code">
* {@code
* ANY_HEADER_KEY = "foo";
* ANY_HEADER_VALUE = "bar";
* assertThat(message, hasEntry(ANY_HEADER_KEY, ANY_HEADER_VALUE));
* assertThat(message, hasEntry(ANY_HEADER_KEY, is(String.class)));
* assertThat(message, hasEntry(ANY_HEADER_KEY, notNullValue()));
* assertThat(message, hasEntry(ANY_HEADER_KEY, is(ANY_HEADER_VALUE)));
* }
* </pre>
* <p>
* For multiple entries to match all:
* <p>
* <pre>
* Map&lt;String, Object&gt; expectedInHeaderMap = new HashMap&lt;String, Object&gt;();
* <pre class="code">
* {@code
* Map<String, Object> expectedInHeaderMap = new HashMap<String, Object>();
* expectedInHeaderMap.put(ANY_HEADER_KEY, ANY_HEADER_VALUE);
* expectedInHeaderMap.put(OTHER_HEADER_KEY, is(OTHER_HEADER_VALUE));
* assertThat(message, HeaderMatcher.hasAllEntries(expectedInHeaderMap));
* }
* </pre>
*
* <p>
* For a single key:
* <p>
*
* <pre>
* ANY_HEADER_KEY = &quot;foo&quot;;
* <pre class="code">
* ANY_HEADER_KEY = "foo";
* assertThat(message, HeaderMatcher.hasKey(ANY_HEADER_KEY));
* </pre>
*

View File

@@ -33,7 +33,7 @@ import org.hamcrest.core.IsEqual;
* It is possible to match a single entry by value or matcher like this:
* </p>
*
* <pre>
* <pre class="code">
* assertThat(map, hasEntry(SOME_KEY, is(SOME_VALUE)));
* assertThat(map, hasEntry(SOME_KEY, is(String.class)));
* assertThat(map, hasEntry(SOME_KEY, notNullValue()));
@@ -43,16 +43,18 @@ import org.hamcrest.core.IsEqual;
* It's also possible to match multiple entries in a map:
* </p>
*
* <pre>
* Map&lt;String, Object&gt; expectedInMap = new HashMap&lt;String, Object&gt;();
* <pre class="code">
* {@code
* Map<String, Object> expectedInMap = new HashMap<String, Object>();
* expectedInMap.put(SOME_KEY, SOME_VALUE);
* expectedInMap.put(OTHER_KEY, is(OTHER_VALUE));
* assertThat(map, hasAllEntries(expectedInMap));
* }
* </pre>
*
* <p>If you only need to verify the existence of a key:</p>
*
* <pre>
* <pre class="code">
* assertThat(map, hasKey(SOME_KEY));
* </pre>
*

View File

@@ -23,6 +23,7 @@ import java.util.Map;
import org.hamcrest.Matcher;
import org.mockito.Mockito;
import org.springframework.messaging.Message;
/**
@@ -38,22 +39,26 @@ import org.springframework.messaging.Message;
* With {@link Mockito#verify(Object)}:
* </p>
*
* <pre>
* <pre class="code">
* {@code
* &#064;Mock
* MessageHandler handler;
* ...
* handler.handleMessage(message);
* verify(handler).handleMessage(messageWithPayload(SOME_PAYLOAD));
* verify(handler).handleMessage(messageWithPayload(is(SOME_CLASS)));
* }
* </pre>
* <p>
* With {@link Mockito#when(Object)}:
* </p>
*
* <pre>
* <pre class="code">
* {@code
* ...
* when(channel.send(messageWithPayload(SOME_PAYLOAD))).thenReturn(true);
* assertThat(channel.send(message), is(true));
* }
* </pre>
*
* @author Alex Peters
@@ -64,12 +69,12 @@ public class MockitoMessageMatchers {
@SuppressWarnings("unchecked")
public static <T> Message<T> messageWithPayload(Matcher<T> payloadMatcher) {
return (Message<T>) argThat(hasPayload(payloadMatcher));
return argThat(hasPayload(payloadMatcher));
}
@SuppressWarnings("unchecked")
public static <T> Message<T> messageWithPayload(T payload) {
return (Message<T>) argThat(hasPayload(payload));
return argThat(hasPayload(payload));
}
public static Message<?> messageWithHeaderEntry(String key, Object value) {

View File

@@ -20,6 +20,7 @@ import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.springframework.messaging.Message;
/**
@@ -29,17 +30,20 @@ import org.springframework.messaging.Message;
* A Junit example using {@link Assert#assertThat(Object, Matcher)} could look
* like this to test a payload value:
* <p>
* <pre>
* ANY_PAYLOAD = new BigDecimal(&quot;1.123&quot;);
* Message&lt;BigDecimal message = MessageBuilder.withPayload(ANY_PAYLOAD).build();
* <pre class="code">
* {@code
* ANY_PAYLOAD = new BigDecimal("1.123");
* Message<BigDecimal message = MessageBuilder.withPayload(ANY_PAYLOAD).build();
* assertThat(message, hasPayload(ANY_PAYLOjAD));
* }
* </pre>
*
* <p>
* An example using {@link Assert#assertThat(Object, Matcher)} delegating to
* another {@link Matcher}.
* <p>
* <pre>
* ANY_PAYLOAD = new BigDecimal(&quot;1.123&quot;);
* <pre class="code">
* ANY_PAYLOAD = new BigDecimal("1.123");
* assertThat(message, PayloadMatcher.hasPayload(is(BigDecimal.class)));
* assertThat(message, PayloadMatcher.hasPayload(notNullValue()));
* assertThat(message, not((PayloadMatcher.hasPayload(is(String.class))))); *

View File

@@ -47,7 +47,7 @@ public final class SocketUtils {
* the need to use the methods of this class multiple times from within your
* Spring Application Context XML file using SpEL. Of course you can do:
*
* <pre>
* <pre class="code">
* {@code
* ...port="#{T(org.springframework.integration.test.util.SocketUtils).findAvailableServerSocket(12000)}"
* }
@@ -57,7 +57,7 @@ public final class SocketUtils {
* This will be acceptable for single use, but if you need to invoke the
* methods numerous time, you may instead want to do this:
*
* <pre>
* <pre class="code">
* {@code
* <bean id="tcpIpUtils" class="org.springframework.integration.test.util.SocketUtils" />
*

View File

@@ -32,7 +32,7 @@ import org.springframework.util.StringUtils;
/**
* Parser for inbound Twitter Channel Adapters.
*
*
* @author Oleg Zhurakousky
* @since 2.0
*/
@@ -50,7 +50,10 @@ public class TwitterInboundChannelAdapterParser extends AbstractPollingInboundCh
BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(TwitterTemplate.class);
builder.addConstructorArgValue(templateBuilder.getBeanDefinition());
}
builder.addConstructorArgValue(element.getAttribute(ID_ATTRIBUTE));
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "query");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "metadata-store");
return builder.getBeanDefinition();
}

View File

@@ -28,9 +28,11 @@ import org.springframework.messaging.MessagingException;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.store.metadata.MetadataStore;
import org.springframework.integration.store.metadata.SimpleMetadataStore;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.social.twitter.api.DirectMessage;
import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.Twitter;
@@ -44,24 +46,29 @@ import org.springframework.util.StringUtils;
* messages when using the Twitter API. This class also handles keeping track of
* the latest inbound message it has received and avoiding, where possible,
* redelivery of duplicate messages. This functionality is enabled using the
* {@link org.springframework.integration.store.MetadataStore} strategy.
* {@link org.springframework.integration.metadata.MetadataStore} strategy.
*
* @author Josh Long
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Gunnar Hillert
* @author Artem Bilan
*
* @since 2.0
*/
@SuppressWarnings("rawtypes")
abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport implements MessageSource {
private volatile long lastPollForTweet;
private final Twitter twitter;
private final TweetComparator tweetComparator = new TweetComparator();
private final Object lastEnqueuedIdMonitor = new Object();
private final String metadataKey;
private volatile MetadataStore metadataStore;
private volatile String metadataKey;
private final Queue<T> tweets = new LinkedBlockingQueue<T>();
private volatile int prefetchThreshold = 0;
@@ -70,25 +77,36 @@ abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport
private volatile long lastProcessedId = -1;
private final Twitter twitter;
private final TweetComparator tweetComparator = new TweetComparator();
private final Object lastEnqueuedIdMonitor = new Object();
public AbstractTwitterMessageSource(Twitter twitter) {
public AbstractTwitterMessageSource(Twitter twitter, String metadataKey) {
Assert.notNull(twitter, "twitter must not be null");
Assert.notNull(metadataKey, "metadataKey must not be null");
this.twitter = twitter;
if (this.twitter.isAuthorized()){
UserOperations userOperations = this.twitter.userOperations();
String profileId = String.valueOf(userOperations.getProfileId());
if (profileId != null) {
metadataKey += "." + profileId;
}
}
this.metadataKey = metadataKey;
}
public void setMetadataStore(MetadataStore metadataStore) {
this.metadataStore = metadataStore;
}
public void setPrefetchThreshold(int prefetchThreshold) {
this.prefetchThreshold = prefetchThreshold;
}
protected Twitter getTwitter() {
return this.twitter;
}
@Override
protected void onInit() throws Exception{
protected void onInit() throws Exception {
super.onInit();
if (this.metadataStore == null) {
// first try to look for a 'metadataStore' in the context
@@ -100,26 +118,7 @@ abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport
this.metadataStore = new SimpleMetadataStore();
}
}
StringBuilder metadataKeyBuilder = new StringBuilder();
if (StringUtils.hasText(this.getComponentType())) {
metadataKeyBuilder.append(this.getComponentType());
}
if (StringUtils.hasText(this.getComponentName())) {
metadataKeyBuilder.append("." + this.getComponentName());
}
else if (logger.isWarnEnabled()) {
logger.warn(this.getClass().getSimpleName() + " has no name. MetadataStore key might not be unique.");
}
if (this.twitter.isAuthorized()){
UserOperations userOperations = this.twitter.userOperations();
String profileId = String.valueOf(userOperations.getProfileId());
if (profileId != null) {
metadataKeyBuilder.append("." + profileId);
}
}
this.metadataKey = metadataKeyBuilder.toString();
String lastId = this.metadataStore.get(this.metadataKey);
// initialize the last status ID from the metadataStore
if (StringUtils.hasText(lastId)) {
@@ -132,16 +131,10 @@ abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport
public Message<?> receive() {
T tweet = this.tweets.poll();
if (tweet == null) {
long currentTime = System.currentTimeMillis();
long elapsedTime = currentTime - this.lastPollForTweet;
if (elapsedTime < 15000) {
// need to wait longer
return null;
}
this.refreshTweetQueueIfNecessary();
tweet = this.tweets.poll();
this.lastPollForTweet = currentTime;
}
if (tweet != null) {
this.lastProcessedId = this.getIdForTweet(tweet);
this.metadataStore.put(this.metadataKey, String.valueOf(this.lastProcessedId));
@@ -204,6 +197,27 @@ abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport
}
/**
* Remove the metadata key and the corresponding value from the Metadata Store.
*/
@ManagedOperation(description="Remove the metadata key and the corresponding value from the Metadata Store.")
void resetMetadataStore() {
synchronized(this) {
this.metadataStore.remove(this.metadataKey);
this.lastProcessedId = -1L;
this.lastEnqueuedId = -1L;
}
}
/**
*
* @return {@code -1} if lastProcessedId is not set, yet.
*/
@ManagedAttribute
public long getLastProcessedId() {
return this.lastProcessedId;
}
private class TweetComparator implements Comparator<T> {
public int compare(T tweet1, T tweet2) {
@@ -230,6 +244,7 @@ abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport
throw new IllegalArgumentException("Uncomparable Twitter objects: " + tweet1 + " and " + tweet2);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -31,14 +31,13 @@ import org.springframework.social.twitter.api.Twitter;
*/
public class DirectMessageReceivingMessageSource extends AbstractTwitterMessageSource<DirectMessage> {
public DirectMessageReceivingMessageSource(Twitter twitter) {
super(twitter);
public DirectMessageReceivingMessageSource(Twitter twitter, String metadataKey) {
super(twitter, metadataKey);
}
@Override
public String getComponentType() {
return "twitter:dm-inbound-channel-adapter";
return "twitter:dm-inbound-channel-adapter";
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -30,11 +30,10 @@ import org.springframework.social.twitter.api.Twitter;
*/
public class MentionsReceivingMessageSource extends AbstractTwitterMessageSource<Tweet> {
public MentionsReceivingMessageSource(Twitter twitter) {
super(twitter);
public MentionsReceivingMessageSource(Twitter twitter, String metadataKey) {
super(twitter, metadataKey);
}
@Override
public String getComponentType() {
return "twitter:mentions-inbound-channel-adapter";

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -35,11 +35,10 @@ public class SearchReceivingMessageSource extends AbstractTwitterMessageSource<T
private volatile String query;
public SearchReceivingMessageSource(Twitter twitter) {
super(twitter);
public SearchReceivingMessageSource(Twitter twitter, String metadataKey) {
super(twitter, metadataKey);
}
public void setQuery(String query) {
Assert.hasText(query, "'query' must not be null");
this.query = query;
@@ -47,7 +46,7 @@ public class SearchReceivingMessageSource extends AbstractTwitterMessageSource<T
@Override
public String getComponentType() {
return "twitter:search-inbound-channel-adapter";
return "twitter:search-inbound-channel-adapter";
}
@Override

View File

@@ -22,7 +22,7 @@ import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.Twitter;
/**
* This {@link org.springframework.integration.core.MessageSource} lets Spring Integration consume
* This {@link org.springframework.integration.core.MessageSource} lets Spring Integration consume
* given account's timeline as messages. It has support for dynamic throttling of API requests.
*
* @author Josh Long
@@ -31,14 +31,13 @@ import org.springframework.social.twitter.api.Twitter;
*/
public class TimelineReceivingMessageSource extends AbstractTwitterMessageSource<Tweet> {
public TimelineReceivingMessageSource(Twitter twitter) {
super(twitter);
public TimelineReceivingMessageSource(Twitter twitter, String metadataKey) {
super(twitter, metadataKey);
}
@Override
public String getComponentType() {
return "twitter:inbound-channel-adapter";
return "twitter:inbound-channel-adapter";
}
@Override

View File

@@ -123,7 +123,28 @@
<xsd:sequence>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
<xsd:attribute name="id" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
The bean id of this Polling Endpoint; the MessageSource is also registered with this id
plus a suffix '.source'; also used as the
MetaDataStore key with suffix '.' + the profileId from the authorized Twitter user.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies the channel the attached to this adapter, to which messages will be sent.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
<xsd:attribute name="twitter-template" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
@@ -136,6 +157,21 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="metadata-store" use="optional" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Reference to a MetadataStore instance for storing metadata associated with
the retrieved feeds. If the implementation is persistent, it can help to
prevent duplicates between restarts. If shared, it can help coordinate multiple
instances of an adapter across different processes.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.metadata.MetadataStore" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="outbound-twitter-type">

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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,7 +16,10 @@
package org.springframework.integration.twitter.config;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
@@ -25,16 +28,13 @@ import org.springframework.integration.twitter.inbound.DirectMessageReceivingMes
import org.springframework.integration.twitter.inbound.MentionsReceivingMessageSource;
import org.springframework.integration.twitter.inbound.TimelineReceivingMessageSource;
import static org.junit.Assert.assertNotNull;
/**
* @author Oleg Zhurakousky
* @author Gunnar Hillert
*/
public class TestReceivingMessageSourceParserTests {
@Test
public void testReceivingAdapterConfigurationAutoStartup(){
ApplicationContext ac = new ClassPathXmlApplicationContext("TestReceivingMessageSourceParser-context.xml", this.getClass());
@@ -47,10 +47,24 @@ public class TestReceivingMessageSourceParserTests {
assertNotNull(dms);
spca = ac.getBean("updateAdapter", SourcePollingChannelAdapter.class);
spca = ac.getBean("updateAdapter", SourcePollingChannelAdapter.class);
TimelineReceivingMessageSource tms = TestUtils.getPropertyValue(spca, "source", TimelineReceivingMessageSource.class);
assertNotNull(tms);
}
@Test
public void testThatMessageSourcesAreRegisteredAsBeans(){
ApplicationContext ac = new ClassPathXmlApplicationContext("TestReceivingMessageSourceParser-context.xml", this.getClass());
MentionsReceivingMessageSource ms = ac.getBean("mentionAdapter.source", MentionsReceivingMessageSource.class);
assertNotNull(ms);
DirectMessageReceivingMessageSource dms = ac.getBean("dmAdapter.source", DirectMessageReceivingMessageSource.class);
assertNotNull(dms);
TimelineReceivingMessageSource tms = ac.getBean("updateAdapter.source", TimelineReceivingMessageSource.class);
assertNotNull(tms);
}
}

View File

@@ -31,7 +31,7 @@ import org.springframework.social.twitter.api.impl.TwitterTemplate;
*/
public class DirectMessageReceivingMessageSourceTests {
@SuppressWarnings("unchecked")
@Test @Ignore
public void demoReceiveDm() throws Exception{
@@ -40,11 +40,11 @@ public class DirectMessageReceivingMessageSourceTests {
pf.afterPropertiesSet();
Properties prop = pf.getObject();
System.out.println(prop);
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"));
DirectMessageReceivingMessageSource tSource = new DirectMessageReceivingMessageSource(template);
DirectMessageReceivingMessageSource tSource = new DirectMessageReceivingMessageSource(template, "foo");
tSource.afterPropertiesSet();
for (int i = 0; i < 50; i++) {
Message<DirectMessage> message = (Message<DirectMessage>) tSource.receive();

View File

@@ -34,9 +34,9 @@ import org.junit.Test;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.messaging.Message;
import org.springframework.integration.store.metadata.SimpleMetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.social.twitter.api.SearchMetadata;
import org.springframework.social.twitter.api.SearchOperations;
import org.springframework.social.twitter.api.SearchResults;
@@ -66,7 +66,7 @@ public class SearchReceivingMessageSourceTests {
prop.getProperty("z_oleg.oauth.consumerSecret"),
prop.getProperty("z_oleg.oauth.accessToken"),
prop.getProperty("z_oleg.oauth.accessTokenSecret"));
SearchReceivingMessageSource tSource = new SearchReceivingMessageSource(template);
SearchReceivingMessageSource tSource = new SearchReceivingMessageSource(template, "foo");
tSource.setQuery(SEARCH_QUERY);
tSource.afterPropertiesSet();
for (int i = 0; i < 50; i++) {
@@ -84,14 +84,14 @@ public class SearchReceivingMessageSourceTests {
@Test
public void testSearchReceivingMessageSourceInit() {
final SearchReceivingMessageSource messageSource = new SearchReceivingMessageSource(new TwitterTemplate("test"));
final SearchReceivingMessageSource messageSource = new SearchReceivingMessageSource(new TwitterTemplate("test"), "foo");
messageSource.setComponentName("twitterSearchMessageSource");
final Object metadataStore = TestUtils.getPropertyValue(messageSource, "metadataStore");
final Object metadataKey = TestUtils.getPropertyValue(messageSource, "metadataKey");
assertNull(metadataStore);
assertNull(metadataKey);
assertNotNull(metadataKey);
messageSource.afterPropertiesSet();
@@ -101,7 +101,7 @@ public class SearchReceivingMessageSourceTests {
assertNotNull(metadataStoreInitialized);
assertTrue(metadataStoreInitialized instanceof SimpleMetadataStore);
assertNotNull(metadataKeyInitialized);
assertEquals("twitter:search-inbound-channel-adapter.twitterSearchMessageSource", metadataKeyInitialized);
assertEquals("foo", metadataKeyInitialized);
final Twitter twitter = TestUtils.getPropertyValue(messageSource, "twitter", Twitter.class);
@@ -123,7 +123,7 @@ public class SearchReceivingMessageSourceTests {
when(twitterTemplate.searchOperations()).thenReturn(so);
when(twitterTemplate.searchOperations().search(SEARCH_QUERY, 20, 0, 0)).thenReturn(null);
final SearchReceivingMessageSource messageSource = new SearchReceivingMessageSource(twitterTemplate);
final SearchReceivingMessageSource messageSource = new SearchReceivingMessageSource(twitterTemplate, "foo");
messageSource.setQuery(SEARCH_QUERY);
final String setQuery = TestUtils.getPropertyValue(messageSource, "query", String.class);
@@ -165,7 +165,7 @@ public class SearchReceivingMessageSourceTests {
SearchParameters params = new SearchParameters(SEARCH_QUERY).count(20).sinceId(0);
when(twitterTemplate.searchOperations().search(params)).thenReturn(results);
final SearchReceivingMessageSource messageSource = new SearchReceivingMessageSource(twitterTemplate);
final SearchReceivingMessageSource messageSource = new SearchReceivingMessageSource(twitterTemplate, "foo");
messageSource.setQuery(SEARCH_QUERY);

View File

@@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/integration/twitter http://www.springframework.org/schema/integration/twitter/spring-integration-twitter.xsd">
<int:channel id="inbound_twitter"/>
<int:channel id="inbound_twitter">
<int:queue/>
</int:channel>
<int-twitter:search-inbound-channel-adapter id="twitterSearchAdapter"
query="springintegration"
twitter-template="twitterTemplate"
channel="inbound_twitter"
metadata-store="redisMetadataStore"
auto-startup="false">
<int:poller fixed-rate="5000" max-messages-per-poll="3"/>
<int:poller fixed-rate="100" max-messages-per-poll="3"/>
</int-twitter:search-inbound-channel-adapter>
<bean id="metadataStore" class="org.springframework.integration.redis.store.metadata.RedisMetadataStore">
<bean id="redisMetadataStore" class="org.springframework.integration.redis.metadata.RedisMetadataStore">
<constructor-arg name="connectionFactory" ref="redisConnectionFactory"/>
</bean>

View File

@@ -19,6 +19,7 @@ package org.springframework.integration.twitter.inbound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
@@ -35,43 +36,56 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.integration.redis.metadata.RedisMetadataStore;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.redis.store.metadata.RedisMetadataStore;
import org.springframework.integration.store.metadata.MetadataStore;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.PollableChannel;
import org.springframework.social.twitter.api.SearchMetadata;
import org.springframework.social.twitter.api.SearchOperations;
import org.springframework.social.twitter.api.SearchResults;
import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.UserOperations;
import org.springframework.social.twitter.api.impl.SearchParameters;
import org.springframework.social.twitter.api.impl.TwitterTemplate;
/**
* @author Gunnar Hillert
* @author Artem Bilan
* @since 3.0
*/
public class SearchReceivingMessageSourceWithRedisTests extends RedisAvailableTests {
private SourcePollingChannelAdapter twitterSearchAdapter;
private RedisConnectionFactory redisConnectionFactory;
private StringRedisTemplate redisTemplate;
private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
private AbstractTwitterMessageSource<?> twitterMessageSource;
private MetadataStore metadataStore;
private String metadataKey;
private PollableChannel tweets;
@Before
public void setup() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(SearchReceivingMessageSourceWithRedisTestsConfig.class);
context.registerShutdownHook();
context.refresh();
this.redisConnectionFactory = context.getBean(RedisConnectionFactory.class);
this.twitterSearchAdapter = context.getBean(SourcePollingChannelAdapter.class);
this.redisTemplate = new StringRedisTemplate(redisConnectionFactory);
this.twitterMessageSource = context.getBean(AbstractTwitterMessageSource.class);
this.metadataStore = context.getBean(MetadataStore.class);
this.tweets = context.getBean("inbound_twitter", PollableChannel.class);
this.metadataKey = TestUtils.getPropertyValue(twitterSearchAdapter, "source.metadataKey", String.class);
// There is need to set a value, not 'remove' and re-init 'twitterMessageSource'
this.metadataStore.put(metadataKey, "-1");
this.twitterMessageSource.afterPropertiesSet();
}
/**
@@ -81,51 +95,43 @@ public class SearchReceivingMessageSourceWithRedisTests extends RedisAvailableTe
@Test
@RedisAvailable
public void testPollForTweetsThreeResultsWithRedisMetadataStore() throws Exception {
final MetadataStore metadataStore = TestUtils.getPropertyValue(twitterSearchAdapter, "source.metadataStore", MetadataStore.class);
MetadataStore metadataStore = TestUtils.getPropertyValue(this.twitterSearchAdapter, "source.metadataStore", MetadataStore.class);
assertTrue("Exptected metadataStore to be an instance of RedisMetadataStore", metadataStore instanceof RedisMetadataStore);
assertSame(this.metadataStore, metadataStore);
/*
* The metadataKey is automatically generated. To ensure that we use the
* the correct key, we retrieve it from the adapter.
*/
final String metadataKey = TestUtils.getPropertyValue(twitterSearchAdapter, "source.metadataKey", String.class);
assertEquals("twitterSearchAdapter.74", metadataKey);
/*
* As we had to retrieve the metadataKey from the adapter. The metdataStore
* was already invoked and the id retrieved from Redis before we had a chance
* to reset possibly pre-existing values.
*
* Rather than deleting the value, we have to set a value, because "null" values
* returned from the MetadataStore are ignored by the onInit() method in
* the AbstractTwitterMessageSource. */
redisTemplate.opsForValue().set(metadataKey, "-1");
assertEquals("-1", redisTemplate.opsForValue().get(metadataKey));
this.twitterSearchAdapter.start();
final SearchReceivingMessageSource source = TestUtils.getPropertyValue(twitterSearchAdapter, "source", SearchReceivingMessageSource.class);
/* We need to call onInit() in order to update the id from the metadataStore. */
source.onInit();
final Message<?> message1 = source.receive();
final Message<?> message2 = source.receive();
final Message<?> message3 = source.receive();
assertNotNull(this.tweets.receive(10000));
assertNotNull(this.tweets.receive(1000));
assertNotNull(this.tweets.receive(1000));
/* We received 3 messages so far. When invoking receive() again the search
* will return again the 3 test Tweets but as we already processed them
* no message (null) is returned. */
final Message<?> message4 = source.receive();
assertNull(this.tweets.receive(0));
assertNotNull(message1);
assertNotNull(message2);
assertNotNull(message3);
assertNull(message4);
final String persistedMetadataStoreValue = redisTemplate.opsForValue().get(metadataKey);
String persistedMetadataStoreValue = this.metadataStore.get(metadataKey);
assertNotNull(persistedMetadataStoreValue);
assertEquals("3", redisTemplate.opsForValue().get(metadataKey));
assertEquals("3", persistedMetadataStoreValue);
redisTemplate.delete(metadataKey);
this.twitterSearchAdapter.stop();
this.metadataStore.put(metadataKey, "1");
this.twitterMessageSource.afterPropertiesSet();
this.twitterSearchAdapter.start();
assertNotNull(this.tweets.receive(1000));
assertNotNull(this.tweets.receive(1000));
assertNull(this.tweets.receive(0));
persistedMetadataStoreValue = this.metadataStore.get(metadataKey);
assertNotNull(persistedMetadataStoreValue);
assertEquals("3", persistedMetadataStoreValue);
}
@Configuration
@@ -153,7 +159,15 @@ public class SearchReceivingMessageSourceWithRedisTests extends RedisAvailableTe
when(twitterTemplate.searchOperations()).thenReturn(so);
when(twitterTemplate.searchOperations().search(any(SearchParameters.class))).thenReturn(results);
when(twitterTemplate.isAuthorized()).thenReturn(true);
final UserOperations userOperations = mock(UserOperations.class);
when(twitterTemplate.userOperations()).thenReturn(userOperations);
when(userOperations.getProfileId()).thenReturn(74L);
return twitterTemplate;
}
}
}

View File

@@ -32,7 +32,7 @@ import org.springframework.social.twitter.api.impl.TwitterTemplate;
*/
public class TimelineReceivingMessageSourceTests {
@SuppressWarnings("unchecked")
@Test @Ignore
public void demoReceiveTimeline() throws Exception{
@@ -41,11 +41,11 @@ public class TimelineReceivingMessageSourceTests {
pf.afterPropertiesSet();
Properties prop = pf.getObject();
System.out.println(prop);
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"));
TimelineReceivingMessageSource tSource = new TimelineReceivingMessageSource(template);
TimelineReceivingMessageSource tSource = new TimelineReceivingMessageSource(template, "foo");
tSource.afterPropertiesSet();
for (int i = 0; i < 50; i++) {
Message<Tweet> message = (Message<Tweet>) tSource.receive();

View File

@@ -62,14 +62,14 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/feed
Spring Integration provides a convenient mechanism to eliminate the need to worry about duplicate entries.
Each feed entry will have a <emphasis>published date</emphasis> field. Every time a new Message is generated and sent,
Spring Integration will store the value of the latest <emphasis>published date</emphasis> in an instance of the
<classname>org.springframework.integration.store.MetadataStore</classname> strategy. The MetadataStore interface is
<classname>org.springframework.integration.metadata.MetadataStore</classname> strategy. The MetadataStore interface is
designed to store various types of generic meta-data (e.g., published date of the last feed entry that has been processed)
to help components such as this Feed adapter deal with duplicates.
</para>
<para>
The default rule for locating this metadata store is as follows:
<emphasis>Spring Integration</emphasis> will look for a bean of type
<classname>org.springframework.integration.store.MetadataStore</classname> in
<classname>org.springframework.integration.metadata.MetadataStore</classname> in
the ApplicationContext. If one is found then it will be used, otherwise
it will create a new instance of <classname>SimpleMetadataStore</classname>
which is an in-memory implementation that will only persist metadata within
@@ -107,5 +107,10 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/feed
<interfacename>MetadataStore</interfacename> interface (e.g. JdbcMetadataStore)
and configure it as bean in the Application Context.
</para>
<note>
The key used to persist the latest <emphasis>published date</emphasis> is the value of the (required)
<code>id</code> attribute of the Feed Inbound Channel Adapter component plus the <code>feedUrl</code>
from the adapter's configuration.
</note>
</section>
</chapter>

View File

@@ -121,10 +121,11 @@
<para>
When a message transitions through an application, each time it is
mutated (e.g. by a transformer) a new message id is assigned. The message id is
a <code>UUID</code>. Beginning with Spring Integration 3.0, the default strategy
used for id generation is to use the <code>com.eaio.uuid</code> package to
generate Type 1 UUIDs. This is much more efficient than the previous
<code>java.util.UUID.randomUUID()</code> implementation.
a <code>UUID</code>. Beginning with <emphasis>Spring Integration 3.0</emphasis>,
the default strategy used for id generation is more efficient than the previous
<code>java.util.UUID.randomUUID()</code> implementation. It uses simple random
numbers based on a secure random seed, instead of creating a secure random
number each time.
</para>
<para>
A different UUID generation strategy can be selected by declaring a bean that implements
@@ -147,11 +148,6 @@
can be used in cases where a UUID is not really needed and a simple incrementing
value is sufficient.
</para>
<important>
The default strategy of creating Type 1 UUIDs may present security concerns for some users
because the UUID contains the MAC address of a network interface on the platform. For these
users, an alternate strategy should be selected.
</important>
</section>
</section>

View File

@@ -152,6 +152,12 @@ rt.setConnectionFactory(redisConnectionFactory);]]></programlisting>
<para>Inbound adapters can subscribe to multiple topic names hence the comma-delimited set of values in the
<code>topics</code> attribute.</para>
<para>
Inbound adapters can use a <classname>RedisSerializer</classname> to deserialize the body of Redis Messages.
The <code>serializer</code> attribute of the <code>&lt;int-redis:inbound-channel-adapter&gt;</code> can be set to an
empty string, which results in a <code>null</code> value for the <classname>RedisSerializer</classname> property.
In this case the raw <code>byte[]</code> bodies of Redis Messages are provided as the message payloads.
</para>
</section>
<section id="redis-outbound-channel-adapter">
@@ -178,7 +184,166 @@ rt.setConnectionFactory(redisConnectionFactory);]]></programlisting>
a <classname>RedisConnectionFactory</classname> which was defined with '<code>redisConnectionFactory</code>' as its bean name.
This example also includes the optional, custom <classname>MessageConverter</classname> (the '<code>testConverter</code>' bean).
</para>
<para>
Since <emphasis>Spring Integration 3.0</emphasis>, the <code>&lt;int-redis:outbound-channel-adapter&gt;</code>,
as an alternative to the <code>topic</code> attribute, has the <code>topic-expression</code> attribute to determine
the Redis topic against the Message at runtime. These attributes are mutually exclusive.
</para>
</section>
<section id="redis-queue-inbound-channel-adapter">
<title>Redis Queue Inbound Channel Adapter</title>
<para>
Since <emphasis>Spring Integration 3.0</emphasis>, a Queue Inbound Channel Adapter
is available to 'right pop' messages from a Redis List.
The adapter is message-driven using an internal listener thread and does not use a poller.
<programlisting language="xml"><![CDATA[<int-redis:queue-inbound-channel-adapter id="" ]]><co id="redis-m-d-c-a-id"/><![CDATA[
channel="" ]]><co id="redis-m-d-c-a-channel"/><![CDATA[
auto-startup="" ]]><co id="redis-m-d-c-a-autoStartup"/><![CDATA[
phase="" ]]><co id="redis-m-d-c-a-phase"/><![CDATA[
connection-factory="" ]]><co id="redis-m-d-c-a-connectionFactory"/><![CDATA[
queue="" ]]><co id="redis-m-d-c-a-queue"/><![CDATA[
error-channel="" ]]><co id="redis-m-d-c-a-errorChannel"/><![CDATA[
serializer="" ]]><co id="redis-m-d-c-a-serializer"/><![CDATA[
receive-timeout="" ]]><co id="redis-m-d-c-a-receiveTimeout"/><![CDATA[
expect-message="" ]]><co id="redis-m-d-c-a-expectMessage"/><![CDATA[
task-executor=""/> ]]><co id="redis-m-d-c-a-task-executor"/>
</programlisting>
<calloutlist>
<callout arearefs="redis-m-d-c-a-id">
<para>
The component bean name. If the <code>channel</code> attribute isn't provided a <classname>DirectChannel</classname>
is created and registered with application context with this <code>id</code> attribute as the bean name.
In this case, the endpoint itself is registered with the bean name <code>id + '.adapter'</code>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-channel">
<para>
The <interfacename>MessageChannel</interfacename> to which to send <interfacename>Message</interfacename>s from this Endpoint.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-autoStartup">
<para>
A <interfacename>SmartLifecycle</interfacename> attribute to specify whether this Endpoint should start automatically after
the application context start or not. Default is <code>true</code>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-phase">
<para>
A <interfacename>SmartLifecycle</interfacename> attribute to specify the <emphasis>phase</emphasis> in which
this Endpoint will be started. Default is <code>0</code>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-connectionFactory">
<para>
A reference to a <interfacename>RedisConnectionFactory</interfacename> bean. Defaults to
<code>redisConnectionFactory</code>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-queue">
<para>
The name of the Redis List on which the queue-based 'right pop' operation is performed to get Redis messages.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-errorChannel">
<para>
The <interfacename>MessageChannel</interfacename> to which to send <interfacename>ErrorMessage</interfacename>s with
<interfacename>Exception</interfacename>s from the listening task of the Endpoint.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-serializer">
<para>
The <interfacename>RedisSerializer</interfacename> bean reference. Can be an empty string, which means 'no serializer'.
In this case the raw <code>byte[]</code> from the inbound Redis message is sent to the <code>channel</code> as the
<interfacename>Message</interfacename> payload. By default it is a <classname>JdkSerializationRedisSerializer</classname>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-receiveTimeout">
<para>
The timeout in milliseconds for 'right pop' operation to wait for a Redis message from the queue. Default is 1 second.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-expectMessage">
<para>
Specify if this Endpoint expects data from the Redis queue to contain entire <interfacename>Message</interfacename>s.
If this attribute is set to <code>true</code>, the <code>serializer</code> can't be an empty string because messages
require some form of deserialization (JDK serialization by default).
Default is <code>false</code>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-task-executor">
<para>
A reference to a Spring <interfacename>TaskExecutor</interfacename> (or standard JDK 1.5+ <interfacename>Executor</interfacename>)
bean. It is used for the underlying listening task. By default a <classname>SimpleAsyncTaskExecutor</classname>
is used.
</para>
</callout>
</calloutlist>
</para>
</section>
<section id="redis-queue-outbound-channel-adapter">
<title>Redis Queue Outbound Channel Adapter</title>
<para>
Since <emphasis>Spring Integration 3.0</emphasis>, a Queue Outbound Channel Adapter
is available to 'left push' to a Redis List from Spring Integration messages:
<programlisting language="xml"><![CDATA[<int-redis:queue-outbound-channel-adapter id="" ]]><co id="redis-q-u-c-a-id"/><![CDATA[
channel="" ]]><co id="redis-q-u-c-a-channel"/><![CDATA[
connection-factory="" ]]><co id="redis-q-u-c-a-connectionFactory"/><![CDATA[
queue="" ]]><co id="redis-q-u-c-a-queue"/><![CDATA[
queue-expression="" ]]><co id="redis-q-u-c-a-queueExpression"/><![CDATA[
serializer="" ]]><co id="redis-q-u-c-a-serializer"/><![CDATA[
extract-payload="" />]]><co id="redis-q-u-c-a-extractPayload"/>
</programlisting>
<calloutlist>
<callout arearefs="redis-q-u-c-a-id">
<para>
The component bean name. If the <code>channel</code> attribute isn't provided, a <classname>DirectChannel</classname>
is created and registered with the application context with this <code>id</code> attribute as the bean name.
In this case, the endpoint is registered with the bean name <code>id + '.adapter'</code>.
</para>
</callout>
<callout arearefs="redis-q-u-c-a-channel">
<para>
The <interfacename>MessageChannel</interfacename> from which this Endpoint receives <interfacename>Message</interfacename>s.
</para>
</callout>
<callout arearefs="redis-q-u-c-a-connectionFactory">
<para>
A reference to a <interfacename>RedisConnectionFactory</interfacename> bean. Defaults to
<code>redisConnectionFactory</code>.
</para>
</callout>
<callout arearefs="redis-q-u-c-a-queue">
<para>
The name of the Redis List on which the queue-based 'left push' operation is performed to send Redis messages.
This attribute is mutually exclusive with <code>queue-expression</code>.
</para>
</callout>
<callout arearefs="redis-q-u-c-a-queueExpression">
<para>
A SpEL <interfacename>Expression</interfacename> to determine the name of the Redis List
using the incoming <interfacename>Message</interfacename> at runtime as the <code>#root</code> variable.
This attribute is mutually exclusive with <code>queue</code>.
</para>
</callout>
<callout arearefs="redis-m-d-c-a-serializer">
<para>
A <interfacename>RedisSerializer</interfacename> bean reference.
By default it is a <classname>JdkSerializationRedisSerializer</classname>.
However, for <classname>String</classname> payloads, a <classname>StringRedisSerializer</classname>
is used, if a <code>serializer</code> reference isn't provided.
</para>
</callout>
<callout arearefs="redis-q-u-c-a-extractPayload">
<para>
Specify if this Endpoint should send just the <emphasis>payload</emphasis> to the Redis queue,
or the entire <interfacename>Message</interfacename>.
Default is <code>true
</code>.
</para>
</callout>
</calloutlist>
</para>
</section>
</section>
<section id="redis-message-store">
@@ -423,4 +588,4 @@ the serialization of values, you may want to consider providing your own
</para>
</section>
</chapter>
</chapter>

View File

@@ -184,10 +184,33 @@ public class Kid {
   // setters and getters are omitted
}]]></programlisting>
</para>
<para>
If you need to create a "structured" map, you can provide the 'flatten' attribute. The default value for this
attribute is 'true' meaning the default behavior; if you provide a 'false' value, then the structure will be a
map of maps.
</para>
<para>
For example:
<programlisting language="java"><![CDATA[public class Parent {
private Child child;
private String name;
// setters and getters are omitted
}
public class Child {
private String name;
private List<String> nickNames;
// setters and getters are omitted
}]]></programlisting>
... will be transformed to a Map which looks like this:
<code>{name=George, child={name=Jenna, nickNames=[Bimbo, ...]}}</code>
</para>
<para>
To configure these transformers, Spring Integration provides namespace support
Object-to-Map:
<programlisting language="xml"><![CDATA[<int:object-to-map-transformer input-channel="directInput" output-channel="output"/>]]></programlisting>
or
<programlisting language="xml"><![CDATA[<int:object-to-map-transformer input-channel="directInput" output-channel="output" flatten="false"/>]]></programlisting>
Map-to-Object
<programlisting language="xml"><![CDATA[<int:map-to-object-transformer input-channel="input" 
                       output-channel="output" 

View File

@@ -17,11 +17,11 @@
subscribers who are known as followers.
</para>
<para>
<important>
Previous versions of Spring Integration were dependent upon the <link linkend="http://twitter4j.org/en/index.html">Twitter4J API</link>,
but with the release of <link linkend="http://www.springsource.org/spring-social">Spring Social 1.0 GA</link>,
Spring Integration, as of version 2.1, now builds directly upon Spring Social's Twitter support, instead of Twitter4J.
</important>
<important>
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.
</important>
</para>
<para>
@@ -39,9 +39,9 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter
<para>
The Twitter API allows for both authenticated and anonymous operations. 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 <link linkend="http://oauth.net/">http://oauth.net/</link> or
in this article <link linkend="http://hueniverse.com/oauth/">http://hueniverse.com/oauth/</link> from Hueniverse.
Please also see <link linkend="http://dev.twitter.com/pages/oauth_faq">OAuth FAQ</link> for more information about OAuth and Twitter.
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.
Please also see <ulink url="http://dev.twitter.com/pages/oauth_faq">OAuth FAQ</ulink> for more information about OAuth and Twitter.
</para>
<para>
In order to use OAuth authentication/authorization with Twitter you must create a new Application on the Twitter Developers site.
@@ -50,7 +50,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter
<para>
<itemizedlist>
<listitem>
<para>Go to <link linkend="http://dev.twitter.com/">http://dev.twitter.com/</link></para>
<para>Go to <ulink url="http://dev.twitter.com">http://dev.twitter.com</ulink></para>
</listitem>
<listitem>
<para>Click on the <code>Register an app</code> link and fill out all required fields on the form provided;
@@ -121,31 +121,39 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]></programlis
<title>Twitter Inbound Adapters</title>
<para>
Twitter inbound adapters allow you to receive Twitter Messages. There are several types of
<link linkend="http://support.twitter.com/groups/31-twitter-basics/topics/109-tweets-messages/articles/119138-types-of-tweets-and-where-they-appear">twitter messages, or tweets</link>
<ulink url="http://support.twitter.com/articles/119138-types-of-tweets-and-where-they-appear">twitter messages, or tweets</ulink>
</para>
<para>
<emphasis>Spring Integration version 2.0 and above</emphasis> provides support for receiving tweets as <emphasis>Timeline Updates</emphasis>,
<emphasis>Direct Messages</emphasis>, <emphasis>Mention Messages</emphasis> as well as Search Results.
</para>
<para>
Every Inbound Twitter Channel Adapter is a <emphasis>Polling Consumer</emphasis> which means you have to provide a poller
configuration. However, there is one important thing you must understand about Twitter since its inner-workings are slightly
different than other polling consumers. Twitter defines a concept of Rate Limiting. You can read more about
it here: <link linkend="http://dev.twitter.com/pages/rate-limiting">Rate Limiting</link>. In a nutshell, Rate Limiting
is the way Twitter manages how often an application can poll for updates. You should consider this when setting your
poller intervals, but we are also doing a few things to limit excessively aggressive polling within our adapters.
</para>
<para>
<important>
<para>
Every Inbound Twitter Channel Adapter is a <emphasis>Polling Consumer</emphasis> which means you have to provide a poller
configuration.
Twitter defines a concept of Rate Limiting. You can read more
about it here: <ulink url="https://dev.twitter.com/docs/rate-limiting/1.1">Rate Limiting</ulink>. In a nutshell,
Rate Limiting is a mechanism that Twitter uses to manage how often an application can poll for updates. You should consider this when
setting your poller intervals so that the adapter polls in compliance with the Twitter policies.
</para>
<para>
With Spring Integration prior to <emphasis>version 3.0</emphasis>, a hard-coded limit within the adapters was used to ensure
the polling interval could not be less than 15 seconds. This is no longer the case and the poller configuration is
applied directly.
</para>
</important>
<para>
Another issue that we need to worry about is handling duplicate Tweets. The same adapter (e.g., Search or Timeline Update)
while polling on Twitter may receive the same values more than once. For example if you keep searching on Twitter with the same search
criteria you'll end up with the same set of tweets unless some other new tweet that matches your search criteria was posted
in between your searches. In that situation you'll get all the tweets you had before plus the new one. But what you really
want is only the new tweet(s). Spring Integration provides an elegant mechanism for handling these situations.
The latest Tweet timestamp will be stored in an instance of the <classname>org.springframework.integration.store.MetadataStore</classname> which is a
The latest Tweet id will be stored in an instance of the <classname>org.springframework.integration.metadata.MetadataStore</classname> which is a
strategy interface designed for storing various types of metadata (e.g., last retrieved tweet in this case). That strategy helps components such as
these Twitter adapters avoid duplicates. By default, Spring Integration will look for a bean of type
<classname>org.springframework.integration.store.MetadataStore</classname> in the ApplicationContext.
If one is found then it will be used, otherwise it will create a new instance of <classname>SimpleMetadataStore</classname>
<classname>org.springframework.integration.metadata.MetadataStore</classname> in the ApplicationContext. Alternatively,
you can configure an explicit <classname>MetadataStore</classname> on the adapter.
If there is no explicit or default store, the adapter will create a new instance of <classname>SimpleMetadataStore</classname>
which is a simple in-memory implementation that will only persist metadata within the lifecycle of the currently running application context.
That means upon restart you may end up with duplicate entries. If you need to persist metadata between Application Context
restarts, you may use the <classname>PropertiesPersistingMetadataStore</classname> (which is backed by a properties file, and a persister
@@ -165,8 +173,16 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]></programlis
</warning>
<programlisting language="xml"><![CDATA[<bean id="metadataStore" class="o.s.i.store.PropertiesPersistingMetadataStore"/>
]]></programlisting>
The Poller that is configured as part of any Inbound Twitter Adapter (see below) will simply poll from this MetadataStore to determine the latest tweet
received.
<para>
If the <classname>MetadataStore</classname> is persistent, during initialization, any Inbound Twitter Adapter (see below)
will retrieve the latest tweet id that has already been sent by the adapter.
</para>
<note>
The key used to persist the latest <emphasis>twitter id</emphasis> is the value of the (required)
<code>id</code> attribute of the Twitter Inbound Channel Adapter component plus the <code>profileId</code>
of the Twitter user.
</note>
<section id="inbound-twitter-update">
<title>Inbound Message Channel Adapter</title>
<para>

View File

@@ -143,12 +143,12 @@
For more information see <xref linkend="http-namespace"/>.
</para>
</section>
<section id="3.0-redis-meta-data-store">
<title>Redis Metadata Store</title>
<section id="3.0-redis-new-components">
<title>Redis: New Components</title>
<para>
A new Redis-based
<interfacename><ulink url="http://docs.spring.io/spring-integration/docs/latest-ga/api/org/springframework/integration/store/MetadataStore.html">MetadataStore</ulink></interfacename>
implementation was added. The <classname>RedisMetadataStore</classname> can
implementation has been added. The <classname>RedisMetadataStore</classname> can
be used to maintain state of a <interfacename>MetadataStore</interfacename>
across application restarts. This new <interfacename>MetadataStore</interfacename>
implementation can be used with adapters such as:
@@ -158,7 +158,12 @@
<listitem>Feed Inbound Channel Adapter</listitem>
</itemizedlist>
<para>
For more information see <xref linkend="redis-metadata-store" />.
New queue-based components have been added. The <code>&lt;int-redis:queue-inbound-channel-adapter/&gt;</code>
and the <code>&lt;int-redis:queue-outbound-channel-adapter/&gt;</code> components are provided
to perform 'right pop' and 'left push' operations on a Redis List, respectively.
</para>
<para>
For more information see <xref linkend="redis" />.
</para>
</section>
</section>
@@ -462,8 +467,8 @@
<title>Message ID Generation</title>
<para>
Previously, message ids were generated using the JDK <code>UUID.randomUUID()</code> method. With this
release, the default mechanism has been changed to use the <code>com.eaio.uuid</code> package which
generates Type 1 UUIDs, and is significantly faster. In addition, the ability to change
release, the default mechanism has been changed to use a more efficient algorithm which
is significantly faster. In addition, the ability to change
the strategy used to generate message ids has been added.
For more information see <xref linkend="message-id-generation"/>.
</para>
@@ -567,5 +572,23 @@
result set respectively. For more information see <xref linkend="jpa"/>.
</para>
</section>
<section id="3.0-redis">
<title>Redis Adapters Changers</title>
<para>
<itemizedlist>
<listitem>
The Redis Inbound Channel Adapter can now use a <code>null</code> value for <code>serializer</code>
property, with the raw data being the message payload.
</listitem>
<listitem>
The Redis Outbound Channel Adapter now has the <code>topic-expression</code> property to determine
the Redis topic against the Message at runtime.
</listitem>
</itemizedlist>
</para>
<para>
For more information, see <xref linkend="redis"/>.
</para>
</section>
</section>
</chapter>