From cd3f13ac91f433ff3d899b3807bb095c5a6a2eee Mon Sep 17 00:00:00 2001 From: Mauro Franceschini Date: Mon, 4 Nov 2013 10:45:22 +0200 Subject: [PATCH 01/10] INT-3193: Add ObjectToMapTransformer flatten attr JIRA: https://jira.springsource.org/browse/INT-3193 --- .../xml/ObjectToMapTransformerParser.java | 4 ++- .../config/xml/spring-integration-3.0.xsd | 9 ++++++ ...ectToMapTransformerParserTests-context.xml | 8 ++++++ .../ObjectToMapTransformerParserTests.java | 28 +++++++++++++++++++ src/reference/docbook/transformer.xml | 23 +++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java index 6226f63cfa..501ffaf4d4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java @@ -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"); } } diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd index 4c6867c967..a7b47c6edb 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd @@ -2169,6 +2169,15 @@ + + + + Specifies if the result Map of Maps should be transformed further to flat keys of + object's property paths. + Default is 'true'. + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests-context.xml index c0dfd12e5d..11ef5d780d 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests-context.xml @@ -15,4 +15,12 @@ + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests.java index 057ecb086b..75eb4ecd6f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ObjectToMapTransformerParserTests.java @@ -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 message = MessageBuilder.withPayload(employee).build(); + nestedInput.send(message); + + @SuppressWarnings("unchecked") + Message> outputMessage = (Message>) nestedOutput.receive(1000); + Map 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(); diff --git a/src/reference/docbook/transformer.xml b/src/reference/docbook/transformer.xml index ee1a6d87e4..495664b267 100644 --- a/src/reference/docbook/transformer.xml +++ b/src/reference/docbook/transformer.xml @@ -184,10 +184,33 @@ public class Kid {    // setters and getters are omitted }]]> + + 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. + + + For example: + nickNames; + // setters and getters are omitted +}]]> + ... will be transformed to a Map which looks like this: + {name=George, child={name=Jenna, nickNames=[Bimbo, ...]}} + To configure these transformers, Spring Integration provides namespace support Object-to-Map: ]]> + or +]]> Map-to-Object Date: Sat, 2 Nov 2013 11:38:19 -0400 Subject: [PATCH 02/10] INT-3190 Fix Package Tangle (JsonHeaders) https://jira.springsource.org/browse/INT-3190 --- .../integration/amqp/support/DefaultAmqpHeaderMapper.java | 2 +- .../integration/json/JsonToObjectTransformer.java | 1 + .../integration/mapping/AbstractHeaderMapper.java | 2 +- .../integration/{json => mapping/support}/JsonHeaders.java | 2 +- .../integration/mapping/support/package-info.java | 4 ++++ .../integration/support/json/Jackson2JsonObjectMapper.java | 2 +- .../integration/support/json/JacksonJsonObjectMapper.java | 2 +- 7 files changed, 10 insertions(+), 5 deletions(-) rename spring-integration-core/src/main/java/org/springframework/integration/{json => mapping/support}/JsonHeaders.java (95%) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/mapping/support/package-info.java diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java index ad6dc4c846..1779152dad 100644 --- a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java @@ -26,8 +26,8 @@ import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; import org.springframework.integration.MessageHeaders; 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; /** diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java index 92d895ef5a..a65757aa6b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java @@ -18,6 +18,7 @@ package org.springframework.integration.json; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.integration.Message; +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; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java index 954f7cb6d3..cb83f71e35 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java @@ -28,7 +28,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.integration.MessageHeaders; -import org.springframework.integration.json.JsonHeaders; +import org.springframework.integration.mapping.support.JsonHeaders; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/support/JsonHeaders.java similarity index 95% rename from spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java rename to spring-integration-core/src/main/java/org/springframework/integration/mapping/support/JsonHeaders.java index 5381bdf472..fa2c88eef2 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/support/JsonHeaders.java @@ -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; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/mapping/support/package-info.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/support/package-info.java new file mode 100644 index 0000000000..0fa87a6375 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Support classes for mapping. + */ +package org.springframework.integration.mapping.support; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java index 0d802e5bc4..30a6b28a89 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java @@ -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; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java index 5297a2bb33..92342dd621 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java @@ -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; /** From 5be8ef3fd8f258584220b5feb12f73e64f3b0244 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 3 Oct 2013 11:27:36 -0400 Subject: [PATCH 03/10] INT-3147: (3167,3173,1941) Improve MetadataStore Previously, `MetadataStore` couldn't be configured for Twitter Adapters - only a global one could be used. The `metadataKey` was generated automatically with a 'difficult' value. * Register all `MessageSource` for `SourcePollingChannelAdapter` as beans with id based on adapter id and prefix '.source' (INT-3147) * Polishing parser to get rid of explicit `MessageSource` beans. (INT-3147) * Make Feed and Twitter adapters `id` attribute as required - now it presents a `metadataKey` for `MetadataStore` (INT-3147) * Add to Twitter adapters a reference attribute for `MetadataStore` (INT-3173) * Add Twitter adapters `poll-skip-period` attribute (INT-3167) * Add and implement `MetadataStore#remove` (INT-1941) * Make `MetadataStore` as `@ManagedResource` (INT-1941) * Polishing tests JIRAs: https://jira.springsource.org/browse/INT-3147 https://jira.springsource.org/browse/INT-3167 https://jira.springsource.org/browse/INT-3173 https://jira.springsource.org/browse/INT-1941 INT-3147: Polishing and fixes * add domain suffix to `metadataKey` * change contract of `MetadataStore.remove` * remove timeout window from `AbstractTwitterMessageSource` * polishing and fix `SearchReceivingMessageSourceWithRedisTests` INT-3147: Rebasing and polishing INT-3147: fix 'metadata' package tangle INT-3147 Doc Polishing --- ...actPollingInboundChannelAdapterParser.java | 10 +- .../context/IntegrationContextUtils.java | 2 +- .../{store => }/metadata/MetadataStore.java | 15 ++- .../PropertiesPersistingMetadataStore.java | 8 +- .../metadata/SimpleMetadataStore.java | 7 +- .../{store => }/metadata/package-info.java | 2 +- ...ropertiesPersistingMetadataStoreTests.java | 3 +- .../FeedInboundChannelAdapterParser.java | 10 +- .../feed/inbound/FeedEntryMessageSource.java | 30 ++--- .../config/spring-integration-feed-3.0.xsd | 25 ++++- ...lAdapterParserTests-file-usage-context.xml | 2 +- ...terParserTests-file-usage-noid-context.xml | 21 ---- .../FeedInboundChannelAdapterParserTests.java | 31 ++---- .../inbound/FeedEntryMessageSourceTests.java | 16 +-- .../FileInboundChannelAdapterParser.java | 11 +- .../ftp/FtpParserInboundTests.java | 3 +- .../JmsInboundChannelAdapterParser.java | 13 +-- .../PollingAdapterMBeanTests-context.xml | 10 +- .../jmx/config/PollingAdapterMBeanTests.java | 8 +- .../MongoDbInboundChannelAdapterParser.java | 9 +- ...RedisStoreInboundChannelAdapterParser.java | 9 +- .../metadata/RedisMetadataStore.java | 13 ++- .../redis/metadata/package-info.java | 5 + .../redis/store/metadata/package-info.java | 5 - .../metadata/RedisMetadataStoreTests.java | 19 +++- .../TwitterInboundChannelAdapterParser.java | 5 +- .../inbound/AbstractTwitterMessageSource.java | 95 +++++++++------- .../DirectMessageReceivingMessageSource.java | 9 +- .../MentionsReceivingMessageSource.java | 7 +- .../inbound/SearchReceivingMessageSource.java | 9 +- .../TimelineReceivingMessageSource.java | 9 +- .../config/spring-integration-twitter-3.0.xsd | 38 ++++++- ...TestReceivingMessageSourceParserTests.java | 26 ++++- ...ectMessageReceivingMessageSourceTests.java | 10 +- .../SearchReceivingMessageSourceTests.java | 14 +-- ...ingMessageSourceWithRedisTests-context.xml | 14 +-- ...hReceivingMessageSourceWithRedisTests.java | 103 ++++++++++-------- .../TimelineReceivingMessageSourceTests.java | 10 +- src/reference/docbook/feed.xml | 9 +- src/reference/docbook/twitter.xml | 19 +++- 40 files changed, 393 insertions(+), 271 deletions(-) rename spring-integration-core/src/main/java/org/springframework/integration/{store => }/metadata/MetadataStore.java (69%) rename spring-integration-core/src/main/java/org/springframework/integration/{store => }/metadata/PropertiesPersistingMetadataStore.java (96%) rename spring-integration-core/src/main/java/org/springframework/integration/{store => }/metadata/SimpleMetadataStore.java (89%) rename spring-integration-core/src/main/java/org/springframework/integration/{store => }/metadata/package-info.java (50%) rename spring-integration-core/src/test/java/org/springframework/integration/{store => }/metadata/PropertiesPersistingMetadataStoreTests.java (94%) delete mode 100644 spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-noid-context.xml rename spring-integration-redis/src/main/java/org/springframework/integration/redis/{store => }/metadata/RedisMetadataStore.java (88%) create mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/package-info.java delete mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/package-info.java rename spring-integration-redis/src/test/java/org/springframework/integration/redis/{store => }/metadata/RedisMetadataStoreTests.java (89%) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractPollingInboundChannelAdapterParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractPollingInboundChannelAdapterParser.java index 7a672c9297..6da6aec529 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractPollingInboundChannelAdapterParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractPollingInboundChannelAdapterParser.java @@ -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"); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java index 184513990f..a3d6f70a0a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java @@ -20,7 +20,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.core.convert.ConversionService; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.MessageChannel; -import org.springframework.integration.store.metadata.MetadataStore; +import org.springframework.integration.metadata.MetadataStore; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/MetadataStore.java b/spring-integration-core/src/main/java/org/springframework/integration/metadata/MetadataStore.java similarity index 69% rename from spring-integration-core/src/main/java/org/springframework/integration/store/metadata/MetadataStore.java rename to spring-integration-core/src/main/java/org/springframework/integration/metadata/MetadataStore.java index de4a8376aa..06d48c9cab 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/MetadataStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/metadata/MetadataStore.java @@ -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 key, or + * null if there was no mapping for key. + */ + @ManagedAttribute + String remove(String key); + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/PropertiesPersistingMetadataStore.java b/spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java similarity index 96% rename from spring-integration-core/src/main/java/org/springframework/integration/store/metadata/PropertiesPersistingMetadataStore.java rename to spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java index 3393468cfd..26c35231ec 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/PropertiesPersistingMetadataStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java @@ -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(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/SimpleMetadataStore.java b/spring-integration-core/src/main/java/org/springframework/integration/metadata/SimpleMetadataStore.java similarity index 89% rename from spring-integration-core/src/main/java/org/springframework/integration/store/metadata/SimpleMetadataStore.java rename to spring-integration-core/src/main/java/org/springframework/integration/metadata/SimpleMetadataStore.java index 5a04477715..0bf5b50fdd 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/SimpleMetadataStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/metadata/SimpleMetadataStore.java @@ -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); + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/package-info.java b/spring-integration-core/src/main/java/org/springframework/integration/metadata/package-info.java similarity index 50% rename from spring-integration-core/src/main/java/org/springframework/integration/store/metadata/package-info.java rename to spring-integration-core/src/main/java/org/springframework/integration/metadata/package-info.java index 18bee7d3e5..d27403d529 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/metadata/package-info.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/metadata/package-info.java @@ -1,4 +1,4 @@ /** * Provides classes supporting metadata stores. */ -package org.springframework.integration.store.metadata; \ No newline at end of file +package org.springframework.integration.metadata; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/store/metadata/PropertiesPersistingMetadataStoreTests.java b/spring-integration-core/src/test/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStoreTests.java similarity index 94% rename from spring-integration-core/src/test/java/org/springframework/integration/store/metadata/PropertiesPersistingMetadataStoreTests.java rename to spring-integration-core/src/test/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStoreTests.java index f3ac8a7748..8ed8cc371c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/store/metadata/PropertiesPersistingMetadataStoreTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStoreTests.java @@ -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 diff --git a/spring-integration-feed/src/main/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParser.java b/spring-integration-feed/src/main/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParser.java index c71a959827..f7f355cd18 100644 --- a/spring-integration-feed/src/main/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParser.java +++ b/spring-integration-feed/src/main/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParser.java @@ -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(); } diff --git a/spring-integration-feed/src/main/java/org/springframework/integration/feed/inbound/FeedEntryMessageSource.java b/spring-integration-feed/src/main/java/org/springframework/integration/feed/inbound/FeedEntryMessageSource.java index 38ca53ef82..9e39d23718 100644 --- a/spring-integration-feed/src/main/java/org/springframework/integration/feed/inbound/FeedEntryMessageSource.java +++ b/spring-integration-feed/src/main/java/org/springframework/integration/feed/inbound/FeedEntryMessageSource.java @@ -30,8 +30,8 @@ import org.springframework.integration.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 { @@ -62,7 +63,7 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements private final Queue entries = new ConcurrentLinkedQueue(); - 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 } } } + } } diff --git a/spring-integration-feed/src/main/resources/org/springframework/integration/feed/config/spring-integration-feed-3.0.xsd b/spring-integration-feed/src/main/resources/org/springframework/integration/feed/config/spring-integration-feed-3.0.xsd index c3182eaf84..954fe1c681 100644 --- a/spring-integration-feed/src/main/resources/org/springframework/integration/feed/config/spring-integration-feed-3.0.xsd +++ b/spring-integration-feed/src/main/resources/org/springframework/integration/feed/config/spring-integration-feed-3.0.xsd @@ -22,7 +22,28 @@ - + + + + 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. + + + + + + + + + + + + Identifies the channel attached to this adapter, to which messages will be sent. + + + + @@ -54,7 +75,7 @@ - + diff --git a/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-context.xml b/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-context.xml index 216a0ee5a9..05b8997914 100644 --- a/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-context.xml +++ b/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-context.xml @@ -21,6 +21,6 @@ - + diff --git a/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-noid-context.xml b/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-noid-context.xml deleted file mode 100644 index 544aa96b6f..0000000000 --- a/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests-file-usage-noid-context.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests.java b/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests.java index 9d1d973610..0e6ee90862 100644 --- a/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests.java +++ b/spring-integration-feed/src/test/java/org/springframework/integration/feed/config/FeedInboundChannelAdapterParserTests.java @@ -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.integration.Message; @@ -43,7 +44,7 @@ import org.springframework.integration.core.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; + } } } diff --git a/spring-integration-feed/src/test/java/org/springframework/integration/feed/inbound/FeedEntryMessageSourceTests.java b/spring-integration-feed/src/test/java/org/springframework/integration/feed/inbound/FeedEntryMessageSourceTests.java index 1244cd3e48..d7aad3f25c 100644 --- a/spring-integration-feed/src/test/java/org/springframework/integration/feed/inbound/FeedEntryMessageSourceTests.java +++ b/spring-integration-feed/src/test/java/org/springframework/integration/feed/inbound/FeedEntryMessageSourceTests.java @@ -26,7 +26,7 @@ import java.net.URL; import org.junit.Before; import org.junit.Test; import org.springframework.integration.Message; -import org.springframework.integration.store.metadata.PropertiesPersistingMetadataStore; +import org.springframework.integration.metadata.PropertiesPersistingMetadataStore; import com.sun.syndication.feed.synd.SyndEntry; import com.sun.syndication.fetcher.FeedFetcher; @@ -52,14 +52,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 +68,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 message1 = source.receive(); @@ -87,7 +87,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 +111,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 +127,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 +146,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(); diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileInboundChannelAdapterParser.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileInboundChannelAdapterParser.java index d6261d48de..404a58dfa8 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileInboundChannelAdapterParser.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileInboundChannelAdapterParser.java @@ -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 <inbound-channel-adapter> 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) { diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpParserInboundTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpParserInboundTests.java index 4ff9043190..68d5193db1 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpParserInboundTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpParserInboundTests.java @@ -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)); diff --git a/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsInboundChannelAdapterParser.java b/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsInboundChannelAdapterParser.java index 9c7157b896..a9decccdd7 100644 --- a/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsInboundChannelAdapterParser.java +++ b/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsInboundChannelAdapterParser.java @@ -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 <inbound-channel-adapter/> element of the 'jms' namespace. - * + * Parser for the <inbound-channel-adapter/> 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(); } } diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests-context.xml index d19f5e6bf4..0ab4f1ab17 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests-context.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests-context.xml @@ -13,18 +13,18 @@ http://www.springframework.org/schema/integration/jmx/spring-integration-jmx.xsd"> - + - - + + - + - + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests.java b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests.java index 25faf51a4d..535f8b12ec 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests.java +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/PollingAdapterMBeanTests.java @@ -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)); diff --git a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/config/MongoDbInboundChannelAdapterParser.java b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/config/MongoDbInboundChannelAdapterParser.java index 216f642ec6..a236ce1b26 100644 --- a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/config/MongoDbInboundChannelAdapterParser.java +++ b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/config/MongoDbInboundChannelAdapterParser.java @@ -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(); } + } diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisStoreInboundChannelAdapterParser.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisStoreInboundChannelAdapterParser.java index 1920f4ad97..0e73cd3cd7 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisStoreInboundChannelAdapterParser.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisStoreInboundChannelAdapterParser.java @@ -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(); } } diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/RedisMetadataStore.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/RedisMetadataStore.java similarity index 88% rename from spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/RedisMetadataStore.java rename to spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/RedisMetadataStore.java index b837625b41..e8aa0609c7 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/RedisMetadataStore.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/RedisMetadataStore.java @@ -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 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; + } + } diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/package-info.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/package-info.java new file mode 100644 index 0000000000..65840e9612 --- /dev/null +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/metadata/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides support for Redis-based + * {@link org.springframework.integration.metadata.MetadataStore}s. + */ +package org.springframework.integration.redis.metadata; diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/package-info.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/package-info.java deleted file mode 100644 index 8e0a51cc48..0000000000 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/metadata/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Provides support for Redis-based - * {@link org.springframework.integration.store.metadata.MetadataStore}s. - */ -package org.springframework.integration.redis.store.metadata; diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/metadata/RedisMetadataStoreTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/metadata/RedisMetadataStoreTests.java similarity index 89% rename from spring-integration-redis/src/test/java/org/springframework/integration/redis/store/metadata/RedisMetadataStoreTests.java rename to spring-integration-redis/src/test/java/org/springframework/integration/redis/metadata/RedisMetadataStoreTests.java index 2e57b9c1b2..c9a48610c8 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/metadata/RedisMetadataStoreTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/metadata/RedisMetadataStoreTests.java @@ -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)); + } + } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java index 675c52fe9f..9538f12796 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/config/TwitterInboundChannelAdapterParser.java @@ -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(); } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java index 536c5ab910..3240276365 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/AbstractTwitterMessageSource.java @@ -28,9 +28,11 @@ import org.springframework.integration.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 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 tweets = new LinkedBlockingQueue(); private volatile int prefetchThreshold = 0; @@ -70,25 +77,36 @@ abstract class AbstractTwitterMessageSource 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 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 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 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 { public int compare(T tweet1, T tweet2) { @@ -230,6 +244,7 @@ abstract class AbstractTwitterMessageSource extends IntegrationObjectSupport throw new IllegalArgumentException("Uncomparable Twitter objects: " + tweet1 + " and " + tweet2); } } + } } diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java index deed9b2be7..758302aa6e 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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 { - 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 diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java index ad9175e84a..5551f3de1a 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/MentionsReceivingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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 { - 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"; diff --git a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java index d84ce95b3f..ae8bf4a377 100644 --- a/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java +++ b/spring-integration-twitter/src/main/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSource.java @@ -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 { - 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 diff --git a/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-3.0.xsd b/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-3.0.xsd index 3a72f4b8a9..c98713f305 100644 --- a/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-3.0.xsd +++ b/spring-integration-twitter/src/main/resources/org/springframework/integration/twitter/config/spring-integration-twitter-3.0.xsd @@ -123,7 +123,28 @@ - + + + + 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. + + + + + + + + + + + + Identifies the channel the attached to this adapter, to which messages will be sent. + + + + @@ -136,6 +157,21 @@ + + + + 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. + + + + + + + + diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java index 73946ecd36..ec97511362 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/config/TestReceivingMessageSourceParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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); + } + } diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSourceTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSourceTests.java index 743039925c..4c2caec1ea 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSourceTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/DirectMessageReceivingMessageSourceTests.java @@ -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 message = (Message) tSource.receive(); diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceTests.java index 7fb524243c..1eb70124ee 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceTests.java @@ -35,7 +35,7 @@ import org.junit.Test; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.core.io.ClassPathResource; import org.springframework.integration.Message; -import org.springframework.integration.store.metadata.SimpleMetadataStore; +import org.springframework.integration.metadata.SimpleMetadataStore; import org.springframework.integration.test.util.TestUtils; import org.springframework.social.twitter.api.SearchMetadata; import org.springframework.social.twitter.api.SearchOperations; @@ -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); diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests-context.xml b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests-context.xml index 1dcc01831e..b750aaf2f8 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests-context.xml +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests-context.xml @@ -1,27 +1,27 @@ - - + + + - + - + diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests.java index 4985dfea89..c29762180f 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/SearchReceivingMessageSourceWithRedisTests.java @@ -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; @@ -30,47 +31,61 @@ import java.util.List; import org.junit.Before; import org.junit.Test; + 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.Message; +import org.springframework.integration.core.PollableChannel; import org.springframework.integration.endpoint.SourcePollingChannelAdapter; 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.redis.metadata.RedisMetadataStore; +import org.springframework.integration.metadata.MetadataStore; import org.springframework.integration.test.util.TestUtils; 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 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(); } /** @@ -80,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 @@ -152,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; } + } + } diff --git a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSourceTests.java b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSourceTests.java index 6782c65222..f568f63c55 100644 --- a/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSourceTests.java +++ b/spring-integration-twitter/src/test/java/org/springframework/integration/twitter/inbound/TimelineReceivingMessageSourceTests.java @@ -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 message = (Message) tSource.receive(); diff --git a/src/reference/docbook/feed.xml b/src/reference/docbook/feed.xml index 5215161a09..cb02137b39 100644 --- a/src/reference/docbook/feed.xml +++ b/src/reference/docbook/feed.xml @@ -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 published date field. Every time a new Message is generated and sent, Spring Integration will store the value of the latest published date in an instance of the - org.springframework.integration.store.MetadataStore strategy. The MetadataStore interface is + org.springframework.integration.metadata.MetadataStore 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. The default rule for locating this metadata store is as follows: Spring Integration will look for a bean of type - org.springframework.integration.store.MetadataStore in + org.springframework.integration.metadata.MetadataStore in the ApplicationContext. If one is found then it will be used, otherwise it will create a new instance of SimpleMetadataStore 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 MetadataStore interface (e.g. JdbcMetadataStore) and configure it as bean in the Application Context. + + The key used to persist the latest published date is the value of the (required) + id attribute of the Feed Inbound Channel Adapter component plus the feedUrl + from the adapter's configuration. + diff --git a/src/reference/docbook/twitter.xml b/src/reference/docbook/twitter.xml index 40c4d7bb97..c78a2e7cc9 100644 --- a/src/reference/docbook/twitter.xml +++ b/src/reference/docbook/twitter.xml @@ -141,11 +141,12 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]>org.springframework.integration.store.MetadataStore which is a + The latest Tweet id will be stored in an instance of the org.springframework.integration.metadata.MetadataStore 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 - org.springframework.integration.store.MetadataStore in the ApplicationContext. - If one is found then it will be used, otherwise it will create a new instance of SimpleMetadataStore + org.springframework.integration.metadata.MetadataStore in the ApplicationContext. Alternatively, + you can configure an explicit MetadataStore on the adapter. + If there is no explicit or default store, the adapter will create a new instance of SimpleMetadataStore 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 PropertiesPersistingMetadataStore (which is backed by a properties file, and a persister @@ -165,8 +166,16 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]> ]]> -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. + + If the MetadataStore is persistent, during initialization, any Inbound Twitter Adapter (see below) + will retrieve the latest tweet id that has already been sent by the adapter. + + + The key used to persist the latest twitter id is the value of the (required) + id attribute of the Twitter Inbound Channel Adapter component plus the profileId + of the Twitter user. + +
Inbound Message Channel Adapter From ce1f46779320197f7e7f17fb21515cbe71996f9a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 4 Nov 2013 20:42:56 +0200 Subject: [PATCH 04/10] INT-3017: Redis Queue Adapters: Namespace Support JIRA: https://jira.springsource.org/browse/INT-3017 * Add parsers tests and integration tests * Polishing Redis XSD * Fix `JmsMessageDrivenEndpointParser` `LifeCycle` attributes * Catch `RedisSystemException` after `this.boundListOperations.rightPop`. It maybe an exception about 'connection closed'. If the `RedisQueueMessageDrivenEndpoint` is 'active' this exception is rethrown, otherwise just logged under error category. * Add Redis queue components docs JIRA: https://jira.springsource.org/browse/INT-3019 INT-3017 Doc Polishing --- .../config/xml/IntegrationNamespaceUtils.java | 23 ++- .../JmsMessageDrivenEndpointParser.java | 3 +- .../redis/config/RedisNamespaceHandler.java | 7 +- ...RedisQueueInboundChannelAdapterParser.java | 83 ++++++++++ ...edisQueueOutboundChannelAdapterParser.java | 57 +++++++ .../RedisQueueMessageDrivenEndpoint.java | 26 ++- .../RedisQueueOutboundChannelAdapter.java | 2 +- .../config/spring-integration-redis-3.0.xsd | 145 +++++++++++++++- ...ssageDrivenEndpointParserTests-context.xml | 44 +++++ ...RedisMessageDrivenEndpointParserTests.java | 114 +++++++++++++ ...boundChannelAdapterParserTests-context.xml | 29 ++++ ...ueueOutboundChannelAdapterParserTests.java | 83 ++++++++++ ...ueueMessageDrivenEndpointTests-context.xml | 44 +++++ .../RedisQueueMessageDrivenEndpointTests.java | 76 +++++++-- ...eueOutboundChannelAdapterTests-context.xml | 23 +++ ...RedisQueueOutboundChannelAdapterTests.java | 59 +++++-- .../redis/util/CustomJsonSerializer.java | 60 +++++++ src/reference/docbook/redis.xml | 156 +++++++++++++++++- src/reference/docbook/whats-new.xml | 13 +- 19 files changed, 993 insertions(+), 54 deletions(-) create mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java create mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParser.java create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests-context.xml create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests.java create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests-context.xml create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests.java create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests-context.xml create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests-context.xml create mode 100644 spring-integration-redis/src/test/java/org/springframework/integration/redis/util/CustomJsonSerializer.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java index 150a3dc054..df6d2f0012 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java @@ -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); } /** diff --git a/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointParser.java b/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointParser.java index 0e9a66f7de..3276096978 100644 --- a/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointParser.java +++ b/spring-integration-jms/src/main/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointParser.java @@ -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) { diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisNamespaceHandler.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisNamespaceHandler.java index 6877708810..c13c32bc06 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisNamespaceHandler.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisNamespaceHandler.java @@ -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()); } -} \ No newline at end of file +} diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java new file mode 100644 index 0000000000..ab0cf50cc3 --- /dev/null +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java @@ -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 org.w3c.dom.Element; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; +import org.springframework.util.StringUtils; + +/** + * Parser for the <queue-inbound-channel-adapter> element of the 'redis' namespace. + * + * @author Artem Bilan + * @since 3.0 + */ +public class RedisQueueInboundChannelAdapterParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return RedisQueueMessageDrivenEndpoint.class; + } + + @Override + protected final String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) + throws BeanDefinitionStoreException { + String id = element.getAttribute("id"); + if (!element.hasAttribute("channel")) { + // the created channel will get the 'id', so the adapter's bean name includes a suffix + id = id + ".adapter"; + } + else if (!StringUtils.hasText(id)) { + id = parserContext.getReaderContext().generateBeanName(definition); + } + return id; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + 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"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE); + + String channelName = element.getAttribute("channel"); + if (!StringUtils.hasText(channelName)) { + channelName = IntegrationNamespaceUtils.createDirectChannel(element, parserContext); + } + builder.addPropertyReference("outputChannel", channelName); + + } + +} diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParser.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParser.java new file mode 100644 index 0000000000..9e0653e814 --- /dev/null +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParser.java @@ -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 <int-redis:queue-outbound-channel-adapter> 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(); + } + +} diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java index 587d2277e3..1c33335f27 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java @@ -15,10 +15,11 @@ */ package org.springframework.integration.redis.inbound; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; +import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.BoundListOperations; import org.springframework.data.redis.core.RedisTemplate; @@ -31,6 +32,7 @@ import org.springframework.integration.MessagingException; import org.springframework.integration.channel.MessagePublishingErrorHandler; import org.springframework.integration.endpoint.MessageProducerSupport; 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; @@ -52,7 +54,7 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport { private MessageChannel errorChannel; - private volatile TaskExecutor taskExecutor; + private volatile Executor taskExecutor; private volatile RedisSerializer serializer = new JdkSerializationRedisSerializer(); @@ -117,7 +119,7 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport { this.receiveTimeout = receiveTimeout; } - public void setTaskExecutor(TaskExecutor taskExecutor) { + public void setTaskExecutor(Executor taskExecutor) { this.taskExecutor = taskExecutor; } @@ -138,7 +140,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 +149,25 @@ 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 message = null; - byte[] value = this.boundListOperations.rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS); + byte[] value = null; + try { + value = this.boundListOperations.rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS); + } + catch (RedisSystemException e) { + if (this.active) { + throw e; + } + else { + logger.error(e); + } + } if (value != null) { if (this.expectMessage) { diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java index bff2a74832..2ff459ec78 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java @@ -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 diff --git a/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd b/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd index 0a13868fab..274879d1d4 100644 --- a/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd +++ b/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd @@ -82,10 +82,7 @@ @@ -329,6 +326,146 @@ + + + + Defines a Message Driven Endpoint for listening a Redis queue. + + + + + + + + + Redis queue name. + + + + + + + + + + + + + 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. + + + + + + + + + + + Specify the timeout in milliseconds to wait for the result of the + 'rightPop' operation on Redis queue. + Default is 1 second. + + + + + + + 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'. + + + + + + + + + + + + + + + + + + + + + + Defines an outbound Redis Queue Message-sending Channel Adapter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Reference to an instance of org.springframework.data.redis.serializer.RedisSerializer + + + + + + + + + + + Specifies if the Message payload or the entire (serialized) Message will be send to the Redis queue. + Default is 'true'. + + + + + + + + diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests-context.xml b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests-context.xml new file mode 100644 index 0000000000..6c7558adf3 --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests-context.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests.java new file mode 100644 index 0000000000..56ba70bba6 --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisMessageDrivenEndpointParserTests.java @@ -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")); + } + +} diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests-context.xml b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests-context.xml new file mode 100644 index 0000000000..6fc343aeed --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests.java new file mode 100644 index 0000000000..14dcd4ccff --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisQueueOutboundChannelAdapterParserTests.java @@ -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")); + } + +} diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests-context.xml b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests-context.xml new file mode 100644 index 0000000000..3214554326 --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests-context.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java index 9184e0eff5..5a1c33c8a5 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java @@ -21,15 +21,22 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import java.util.Date; +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.annotation.Autowired; 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.Message; +import org.springframework.integration.MessageChannel; import org.springframework.integration.MessagingException; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.core.PollableChannel; @@ -37,14 +44,31 @@ import org.springframework.integration.message.ErrorMessage; import org.springframework.integration.redis.rules.RedisAvailable; import org.springframework.integration.redis.rules.RedisAvailableTests; import org.springframework.integration.support.MessageBuilder; +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 +76,8 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { String queueName = "si.test.redisQueueInboundChannelAdapterTests"; - RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest(); - RedisTemplate redisTemplate = new RedisTemplate(); - redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setConnectionFactory(this.connectionFactory); redisTemplate.setEnableDefaultSerializer(false); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); @@ -71,7 +93,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 +109,6 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { assertEquals(payload2, receive.getPayload()); endpoint.stop(); - this.waitUntilListening(endpoint); } @Test @@ -96,10 +118,8 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { final String queueName = "si.test.redisQueueInboundChannelAdapterTests2"; - RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest(); - RedisTemplate redisTemplate = new RedisTemplate(); - redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setConnectionFactory(this.connectionFactory); redisTemplate.setEnableDefaultSerializer(false); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); @@ -115,7 +135,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); @@ -139,19 +160,38 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { 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 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 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()); } } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests-context.xml b/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests-context.xml new file mode 100644 index 0000000000..87a2b1c3ae --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests-context.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests.java index 47a0c8c35c..e751911c95 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapterTests.java @@ -24,7 +24,10 @@ 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; @@ -32,33 +35,47 @@ 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.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.mapping.InboundMessageMapper; import org.springframework.integration.message.GenericMessage; 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.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 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 redisTemplate2 = new RedisTemplate(); - 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 message = MessageBuilder.withPayload("testing").build(); handler.handleMessage(message); RedisTemplate redisTemplate = new RedisTemplate(); - 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.class)); RedisTemplate redisTemplate = new StringRedisTemplate(); - redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setConnectionFactory(this.connectionFactory); redisTemplate.afterPropertiesSet(); handler.handleMessage(new GenericMessage(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 message = new GenericMessage(queueName); + this.sendChannel.send(message); + + RedisTemplate redisTemplate = new StringRedisTemplate(); + redisTemplate.setConnectionFactory(this.connectionFactory); + redisTemplate.afterPropertiesSet(); + + String result = redisTemplate.boundListOps(queueName).rightPop(5000, TimeUnit.MILLISECONDS); + assertNotNull(result); + InboundMessageMapper mapper = new JsonInboundMessageMapper(String.class, new Jackson2JsonMessageParser()); + Message resultMessage = mapper.toMessage(result); + assertEquals(message.getPayload(), resultMessage.getPayload()); + } + } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/CustomJsonSerializer.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/CustomJsonSerializer.java new file mode 100644 index 0000000000..d76f24a637 --- /dev/null +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/CustomJsonSerializer.java @@ -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> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final InboundMessageMapper 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); + } + } + +} diff --git a/src/reference/docbook/redis.xml b/src/reference/docbook/redis.xml index 42dcefeadd..367ab41d38 100644 --- a/src/reference/docbook/redis.xml +++ b/src/reference/docbook/redis.xml @@ -179,6 +179,160 @@ rt.setConnectionFactory(redisConnectionFactory);]]> This example also includes the optional, custom MessageConverter (the 'testConverter' bean). +
+ Redis Queue Inbound Channel Adapter + + Since Spring Integration 3.0, 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. + ]]> + + + + + The component bean name. If the channel attribute isn't provided a DirectChannel + is created and registered with application context with this id attribute as the bean name. + In this case, the endpoint itself is registered with the bean name id + '.adapter'. + + + + + The MessageChannel to which to send Messages from this Endpoint. + + + + + A SmartLifecycle attribute to specify whether this Endpoint should start automatically after + the application context start or not. Default is true. + + + + + A SmartLifecycle attribute to specify the phase in which + this Endpoint will be started. Default is 0. + + + + + A reference to a RedisConnectionFactory bean. Defaults to + redisConnectionFactory. + + + + + The name of the Redis List on which the queue-based 'right pop' operation is performed to get Redis messages. + + + + + The MessageChannel to which to send ErrorMessages with + Exceptions from the listening task of the Endpoint. + + + + + The RedisSerializer bean reference. Can be an empty string, which means 'no serializer'. + In this case the raw byte[] from the inbound Redis message is sent to the channel as the + Message payload. By default it is a JdkSerializationRedisSerializer. + + + + + The timeout in milliseconds for 'right pop' operation to wait for a Redis message from the queue. Default is 1 second. + + + + + Specify if this Endpoint expects data from the Redis queue to contain entire Messages. + If this attribute is set to true, the serializer can't be an empty string because messages + require some form of deserialization (JDK serialization by default). + Default is false. + + + + + A reference to a Spring TaskExecutor (or standard JDK 1.5+ Executor) + bean. It is used for the underlying listening task. By default a SimpleAsyncTaskExecutor + is used. + + + + +
+
+ Redis Queue Outbound Channel Adapter + + Since Spring Integration 3.0, a Queue Outbound Channel Adapter + is available to 'left push' to a Redis List from Spring Integration messages: + ]]> + + + + + The component bean name. If the channel attribute isn't provided, a DirectChannel + is created and registered with the application context with this id attribute as the bean name. + In this case, the endpoint is registered with the bean name id + '.adapter'. + + + + + The MessageChannel from which this Endpoint receives Messages. + + + + + A reference to a RedisConnectionFactory bean. Defaults to + redisConnectionFactory. + + + + + 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 queue-expression. + + + + + A SpEL Expression to determine the name of the Redis List + using the incoming Message at runtime as the #root variable. + This attribute is mutually exclusive with queue. + + + + + A RedisSerializer bean reference. + By default it is a JdkSerializationRedisSerializer. + However, for String payloads, a StringRedisSerializer + is used, if a serializer reference isn't provided. + + + + + Specify if this Endpoint should send just the payload to the Redis queue, + or the entire Message. + Default is true + . + + + + +
@@ -423,4 +577,4 @@ the serialization of values, you may want to consider providing your own
- \ No newline at end of file + diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 8bd71c43ee..614e17e06c 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -143,12 +143,12 @@ For more information see . -
- Redis Metadata Store +
+ Redis: New Components A new Redis-based MetadataStore - implementation was added. The RedisMetadataStore can + implementation has been added. The RedisMetadataStore can be used to maintain state of a MetadataStore across application restarts. This new MetadataStore implementation can be used with adapters such as: @@ -158,7 +158,12 @@ Feed Inbound Channel Adapter - For more information see . + New queue-based components has been added. The <int-redis:queue-inbound-channel-adapter/> + and the <int-redis:queue-outbound-channel-adapter/> components are provided + to perform 'right pop' and 'left push' operations on a Redis List, respectively. + + + For more information see .
From 68f399061199d8107f698f4e1c43586e921805e9 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 4 Nov 2013 17:08:53 -0500 Subject: [PATCH 05/10] INT-3017 Fix Typo --- src/reference/docbook/whats-new.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 614e17e06c..53b20bc8bd 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -158,7 +158,7 @@ Feed Inbound Channel Adapter - New queue-based components has been added. The <int-redis:queue-inbound-channel-adapter/> + New queue-based components have been added. The <int-redis:queue-inbound-channel-adapter/> and the <int-redis:queue-outbound-channel-adapter/> components are provided to perform 'right pop' and 'left push' operations on a Redis List, respectively. From 87988b50322c97beeb3f7a39e1555cfb14326815 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 4 Nov 2013 18:40:36 -0500 Subject: [PATCH 06/10] INT-3064 Message ID Generation Use the SPR 4.0.0 Message ID Generation algorithm. JIRA: https://jira.springsource.org/browse/INT-3064 --- build.gradle | 2 - .../integration/MessageHeaders.java | 49 +++++++++++++++---- src/reference/docbook/message.xml | 14 ++---- src/reference/docbook/whats-new.xml | 4 +- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 1af9cd8564..dd03d4936e 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,6 @@ subprojects { subproject -> junitVersion = '4.11' log4jVersion = '1.2.12' mockitoVersion = '1.9.5' - eaioUUIDVersion = '3.2' ftpServerVersion = '1.0.6' @@ -194,7 +193,6 @@ project('spring-integration-core') { compile "org.springframework:spring-context:$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) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java b/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java index c71a1ee1c9..da00aa3269 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java @@ -20,12 +20,15 @@ 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; @@ -33,8 +36,6 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.eaio.uuid.UUIDGen; - /** * The headers for a {@link Message}.
* IMPORTANT: MessageHeaders are immutable. Any mutating operation (e.g., put(..), putAll(..) etc.) @@ -55,6 +56,7 @@ import com.eaio.uuid.UUIDGen; * @author Mark Fisher * @author Oleg Zhurakousky * @author Gary Russell + * @author Rossen Stoyanchev */ public final class MessageHeaders implements Map, Serializable { @@ -64,6 +66,8 @@ public final class MessageHeaders implements Map, Serializable { 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 except in the @@ -100,13 +104,8 @@ public final class MessageHeaders implements Map, Serializable { public MessageHeaders(Map headers) { this.headers = (headers != null) ? new HashMap(headers) : new HashMap(); - if (MessageHeaders.idGenerator == null) { - UUID uuid = new UUID(UUIDGen.newTime(), UUIDGen.getClockSeqAndNode()); - this.headers.put(ID, uuid); - } - else { - this.headers.put(ID, MessageHeaders.idGenerator.generateId()); - } + IdGenerator generatorToUse = (idGenerator != null) ? idGenerator : defaultIdGenerator; + this.headers.put(ID, generatorToUse.generateId()); this.headers.put(TIMESTAMP, new Long(System.currentTimeMillis())); } @@ -285,6 +284,38 @@ public final class MessageHeaders implements Map, Serializable { } + /** + * 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(); diff --git a/src/reference/docbook/message.xml b/src/reference/docbook/message.xml index 891786eea8..88fd49ae56 100644 --- a/src/reference/docbook/message.xml +++ b/src/reference/docbook/message.xml @@ -121,10 +121,11 @@ 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 UUID. Beginning with Spring Integration 3.0, the default strategy - used for id generation is to use the com.eaio.uuid package to - generate Type 1 UUIDs. This is much more efficient than the previous - java.util.UUID.randomUUID() implementation. + a UUID. Beginning with Spring Integration 3.0, + the default strategy used for id generation is more efficient than the previous + java.util.UUID.randomUUID() implementation. It uses simple random + numbers based on a secure random seed, instead of creating a secure random + number each time. 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. - - 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. - diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 53b20bc8bd..0a4e18d4d4 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -467,8 +467,8 @@ Message ID Generation Previously, message ids were generated using the JDK UUID.randomUUID() method. With this - release, the default mechanism has been changed to use the com.eaio.uuid 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 . From 970852741d8ad6778101fa73ecf6bda34c1f7f37 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 4 Nov 2013 18:11:01 -0500 Subject: [PATCH 07/10] INT-3154/INT-3137 JavaDoc Formatting INT-3154 Use SPR Java7 stylesheet to put a box around `
`
code.

Update `
` tags to include class="code".

INT-3137 Remove NOTICE from `JdbcChannelMessageStore`.

JIRA: https://jira.springsource.org/browse/INT-3137
      https://jira.springsource.org/browse/INT-3154
---
 build.gradle                                  |  1 +
 .../integration/MessageHeaders.java           |  4 +--
 .../handler/MessageHandlerChain.java          | 16 +++++----
 .../router/RecipientListRouter.java           | 12 ++++---
 .../integration/jdbc/JdbcMessageHandler.java  |  3 +-
 .../jdbc/store/JdbcChannelMessageStore.java   | 14 +++-----
 .../test/matcher/HeaderMatcher.java           | 33 +++++++++++--------
 .../test/matcher/MapContentMatchers.java      | 10 +++---
 .../test/matcher/MockitoMessageMatchers.java  | 13 +++++---
 .../test/matcher/PayloadMatcher.java          | 14 +++++---
 .../integration/test/util/SocketUtils.java    |  4 +--
 11 files changed, 70 insertions(+), 54 deletions(-)

diff --git a/build.gradle b/build.gradle
index dd03d4936e..5cb5176d47 100644
--- a/build.gradle
+++ b/build.gradle
@@ -641,6 +641,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
 	}
diff --git a/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java b/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java
index da00aa3269..823250c98a 100644
--- a/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java
+++ b/spring-integration-core/src/main/java/org/springframework/integration/MessageHeaders.java
@@ -41,11 +41,11 @@ import org.apache.commons.logging.LogFactory;
  * 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
- * 
+ * 
  * MessageBuilder.withPayload("foo").setHeader("key1", "value1").setHeader("key2", "value2");
  * 
* or create an instance of GenericMessage passing payload as {@link Object} and headers as a regular {@link Map} - *
+ * 
  * Map headers = new HashMap();
  * headers.put("key1", "value1");
  * headers.put("key2", "value2");
diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/MessageHandlerChain.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/MessageHandlerChain.java
index 3d891ceb2b..56cbe5383f 100644
--- a/spring-integration-core/src/main/java/org/springframework/integration/handler/MessageHandlerChain.java
+++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/MessageHandlerChain.java
@@ -52,13 +52,15 @@ import org.springframework.util.Assert;
  * the configuration by removing channels that can be created implicitly.
  * 

* - *

- * <chain>
- *     <filter ref="someFilter"/>
- *     <bean class="SomeMessageHandlerImplementation"/>
- *     <transformer ref="someTransformer"/>
- *     <aggregator ... />
- * </chain>
+ * 
+ * {@code
+ * 
+ *     
+ *     
+ *     
+ *     
+ * 
+ * }
  * 
* * @author Mark Fisher diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java b/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java index 3e89c93921..601d8fd51b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java @@ -27,11 +27,13 @@ import org.springframework.integration.core.MessageSelector; import org.springframework.util.Assert; /** - *
- * <recipient-list-router id="simpleRouter" input-channel="routingChannelA">
- *     <recipient channel="channel1"/>
- *     <recipient channel="channel2"/>
- * </recipient-list-router>
+ * 
+ * {@code
+ * 
+ *     
+ *     
+ * 
+ * }
  * 
*

* A Message Router that sends Messages to a list of recipient channels. The diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java index 5e7988df45..c6dafcfcf4 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java @@ -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. * - *

+ * 
  * INSERT INTO FOOS (MESSAGE_ID, PAYLOAD) VALUES (:headers[id], :payload)
  * 
* @@ -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> keys = executeUpdateQuery(message, keysGenerated); diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java index 51cd0cdb20..3a07934db5 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java @@ -76,12 +76,6 @@ import org.springframework.util.StringUtils; * This message store shall be used for message channels only. *

*

- * NOTICE: 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. - *

- * - *

* As such, the {@link JdbcChannelMessageStore} uses database specific SQL queries. *

*

@@ -89,8 +83,8 @@ import org.springframework.util.StringUtils; * database table only. The SQL scripts to create the necessary table are packaged * under org/springframework/integration/jdbc/messagestore/channel/schema-*.sql, * where * denotes the target database type. - *

+ *

+ * * @author Gunnar Hillert * @author Artem Bilan * @since 2.2 @@ -331,7 +325,7 @@ public class JdbcChannelMessageStore extends AbstractMessageGroupStore implement *

For this to work, you must setup the corresponding * {@link TransactionSynchronizationFactory}:

* - *
+	 * 
 	 * {@code
 	 * 
 	 *     
@@ -343,7 +337,7 @@ public class JdbcChannelMessageStore extends AbstractMessageGroupStore implement
 	 * This {@link TransactionSynchronizationFactory} is then referenced in the
 	 * transaction configuration of the poller:
 	 *
-	 * 
+	 * 
 	 * {@code
 	 * 
diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java
index bd40145834..20bc3fb590 100644
--- a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java
+++ b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java
@@ -15,17 +15,18 @@
  */
 package org.springframework.integration.test.matcher;
 
-import org.hamcrest.Description;
-import org.hamcrest.Factory;
-import org.hamcrest.Matcher;
-import org.junit.Assert;
-import org.springframework.integration.Message;
-import org.springframework.integration.MessageHeaders;
+import static org.hamcrest.CoreMatchers.is;
 
 import java.util.Date;
 import java.util.Map;
 
-import static org.hamcrest.CoreMatchers.is;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.junit.Assert;
+
+import org.springframework.integration.Message;
+import org.springframework.integration.MessageHeaders;
 
 /**
  * Are the {@link MessageHeaders} of a {@link Message} containing any entry
@@ -35,30 +36,34 @@ import static org.hamcrest.CoreMatchers.is;
  * entry:
  * 

* - *

- * ANY_HEADER_KEY = "foo";
- * ANY_HEADER_VALUE = "bar";
+ * 
+ * {@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)));
+ * }
  * 
*

* For multiple entries to match all: *

- *

- * Map<String, Object> expectedInHeaderMap = new HashMap<String, Object>();
+ * 
+ * {@code
+ * Map expectedInHeaderMap = new HashMap();
  * expectedInHeaderMap.put(ANY_HEADER_KEY, ANY_HEADER_VALUE);
  * expectedInHeaderMap.put(OTHER_HEADER_KEY, is(OTHER_HEADER_VALUE));
  * assertThat(message, HeaderMatcher.hasAllEntries(expectedInHeaderMap));
+ * }
  * 
* *

* For a single key: *

* - *

- * ANY_HEADER_KEY = "foo";
+ * 
+ * ANY_HEADER_KEY = "foo";
  * assertThat(message, HeaderMatcher.hasKey(ANY_HEADER_KEY));
  * 
* diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java index 98e29142e6..4997dbca7a 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java @@ -33,7 +33,7 @@ import org.hamcrest.core.IsEqual; * It is possible to match a single entry by value or matcher like this: *

* - *
+ * 
  * 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:
  * 

* - *
- * Map<String, Object> expectedInMap = new HashMap<String, Object>();
+ * 
+ * {@code
+ * Map expectedInMap = new HashMap();
  * expectedInMap.put(SOME_KEY, SOME_VALUE);
  * expectedInMap.put(OTHER_KEY, is(OTHER_VALUE));
  * assertThat(map, hasAllEntries(expectedInMap));
+ * }
  * 
* *

If you only need to verify the existence of a key:

* - *
+ * 
  * assertThat(map, hasKey(SOME_KEY));
  * 
* diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java index cf188b34a5..5a5c4e5a44 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java @@ -23,6 +23,7 @@ import java.util.Map; import org.hamcrest.Matcher; import org.mockito.Mockito; + import org.springframework.integration.Message; /** @@ -38,22 +39,26 @@ import org.springframework.integration.Message; * With {@link Mockito#verify(Object)}: *

* - *
+ * 
+ * {@code
  * @Mock
  * MessageHandler handler;
  * ...
  * handler.handleMessage(message);
  * verify(handler).handleMessage(messageWithPayload(SOME_PAYLOAD));
  * verify(handler).handleMessage(messageWithPayload(is(SOME_CLASS)));
+ * }
  * 
*

* With {@link Mockito#when(Object)}: *

* - *
+ * 
+ * {@code
  * ...
  * when(channel.send(messageWithPayload(SOME_PAYLOAD))).thenReturn(true);
  * assertThat(channel.send(message), is(true));
+ * }
  * 
* * @author Alex Peters @@ -64,12 +69,12 @@ public class MockitoMessageMatchers { @SuppressWarnings("unchecked") public static Message messageWithPayload(Matcher payloadMatcher) { - return (Message) argThat(hasPayload(payloadMatcher)); + return argThat(hasPayload(payloadMatcher)); } @SuppressWarnings("unchecked") public static Message messageWithPayload(T payload) { - return (Message) argThat(hasPayload(payload)); + return argThat(hasPayload(payload)); } public static Message messageWithHeaderEntry(String key, Object value) { diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java index 0f85d89637..383254ee32 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java @@ -20,6 +20,7 @@ import org.hamcrest.Factory; import org.hamcrest.Matcher; import org.hamcrest.core.IsEqual; import org.junit.Assert; + import org.springframework.integration.Message; /** @@ -29,17 +30,20 @@ import org.springframework.integration.Message; * A Junit example using {@link Assert#assertThat(Object, Matcher)} could look * like this to test a payload value: *

- *

- * ANY_PAYLOAD = new BigDecimal("1.123");
- * Message<BigDecimal message = MessageBuilder.withPayload(ANY_PAYLOAD).build();
+ * 
+ * {@code
+ * ANY_PAYLOAD = new BigDecimal("1.123");
+ * Message
+ *
  * 

* An example using {@link Assert#assertThat(Object, Matcher)} delegating to * another {@link Matcher}. *

- *

- * ANY_PAYLOAD = new BigDecimal("1.123");
+ * 
+ * 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))))); *
diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/util/SocketUtils.java b/spring-integration-test/src/main/java/org/springframework/integration/test/util/SocketUtils.java
index e54f27cbc4..b57d959ca7 100644
--- a/spring-integration-test/src/main/java/org/springframework/integration/test/util/SocketUtils.java
+++ b/spring-integration-test/src/main/java/org/springframework/integration/test/util/SocketUtils.java
@@ -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:
 	 *
-	 * 
+	 * 
 	 * {@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:
 	 *
-	 * 
+	 * 
 	 * {@code
 	 * 
 	 *

From 156eeeb73876e90424c8ac9fccdb392450615dac Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Tue, 5 Nov 2013 14:02:06 +0200
Subject: [PATCH 08/10] INT-3167: Polishing Twitter Documentation

JIRA: https://jira.springsource.org/browse/INT-3167

Doc Polishing
---
 src/reference/docbook/twitter.xml | 45 ++++++++++++++++++-------------
 1 file changed, 26 insertions(+), 19 deletions(-)

diff --git a/src/reference/docbook/twitter.xml b/src/reference/docbook/twitter.xml
index c78a2e7cc9..f99e602207 100644
--- a/src/reference/docbook/twitter.xml
+++ b/src/reference/docbook/twitter.xml
@@ -17,11 +17,11 @@
       subscribers who are known as followers.
     
     
-    	
-    	Previous versions of Spring Integration were dependent upon the Twitter4J API,
-    	but with the release of Spring Social 1.0 GA,
-    	Spring Integration, as of version 2.1, now builds directly upon Spring Social's Twitter support, instead of Twitter4J.
-    	
+		
+		Versions of Spring Integration prior to 2.1 were dependent upon the Twitter4J API,
+		but with the release of Spring Social 1.0 GA,
+		Spring Integration, as of version 2.1, now builds directly upon Spring Social's Twitter support, instead of Twitter4J.
+		
     
 
     
@@ -39,9 +39,9 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter
   	
   	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 http://oauth.net/ or
-  	in this article http://hueniverse.com/oauth/ from Hueniverse.
-  	Please also see OAuth FAQ for more information about OAuth and Twitter.
+	sharing their password. More information can be found at http://oauth.net or
+	in this article http://hueniverse.com/oauth from Hueniverse.
+	Please also see OAuth FAQ for more information about OAuth and Twitter.
   	
   	
   	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
   	
   	
           
-            Go to http://dev.twitter.com/
+            Go to http://dev.twitter.com
           
           
             Click on the Register an app link and fill out all required fields on the form provided;
@@ -121,21 +121,28 @@ twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtxFK8w5ZMtMj20KFhB6o]]>Twitter Inbound Adapters
   	
   	Twitter inbound adapters allow you to receive Twitter Messages. There are several types of
-  	twitter messages, or tweets
+	twitter messages, or tweets
   	
   	
     Spring Integration version 2.0 and above provides support for receiving tweets as Timeline Updates,
   	Direct Messages, Mention Messages as well as Search Results.
   	
-  	
-  	Every Inbound Twitter Channel Adapter is a Polling Consumer 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: Rate Limiting. 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.
-  	
-  	
+    
+      
+         Every Inbound Twitter Channel Adapter is a Polling Consumer which means you have to provide a poller
+         configuration.
+         Twitter defines a concept of Rate Limiting. You can read more
+         about it here: Rate Limiting. 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.
+      
+      
+         With Spring Integration prior to version 3.0, 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.
+      
+    
+    
   	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

From 3470e3306977539baecbf0b5a1c8fc6abea6ec17 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 24 Oct 2013 14:42:57 +0300
Subject: [PATCH 09/10] INT-3177: Generic RedisTemplate for PublishingMH

JIRA: https://jira.springsource.org/browse/INT-3177

* Change `RedisPublishingMessageHandler.StringRedisTemplate` to `RedisTemplate`
* Don't provide `serializer` to the `template`
* Use `serializer` directly for values which are not `byte[]`
* Add `RedisAvailableTests#awaitContainerSubscribed` for tests to avoid race conditions
* Refactor some tests
* Add test for `byte[]` payload

INT-3177: Generic RedisInboundChannelAdapter

Port from Spring XD: allow for `RedisInboundChannelAdapter`
to receive from Redis any object, not only String

* Make `serializer` property 'resettable' to `null`
* Change `RedisInboundChannelAdapterParser` to allow to apply
empty value from `serializer` attribute
* Add parser test and test for `byte[]` messages

INT-3177: Polishing and documentation

JIRA: https://jira.springsource.org/browse/INT-3033

* Add `topic-expression` to ``
* Add tests and docs

INT-3177: Deprecate `RedisPublishMH.defaultTopic`

* Make `topic` and `topic-expression` attributes as mutually exclusive
* Polishing Redis Topic Parsers
* Polishing tests and docs
* Default `serializer` for Redis Topic Adapters is `StringRedisSerializer` for backward compatibility

Polishing according PR's discussion

INT-3177 Doc Polishing
---
 .../RedisInboundChannelAdapterParser.java     |  8 +-
 .../RedisOutboundChannelAdapterParser.java    | 17 ++++-
 .../inbound/RedisInboundChannelAdapter.java   | 12 +--
 .../RedisPublishingMessageHandler.java        | 70 ++++++++++++-----
 .../config/spring-integration-redis-3.0.xsd   | 21 +++++-
 .../SubscribableRedisChannelTests.java        | 12 +--
 ...boundChannelAdapterParserTests-context.xml |  3 +
 ...RedisInboundChannelAdapterParserTests.java |  6 ++
 ...boundChannelAdapterParserTests-context.xml |  8 +-
 ...edisOutboundChannelAdapterParserTests.java | 14 +++-
 .../RedisInboundChannelAdapterTests.java      | 75 ++++++++++---------
 .../RedisPublishingMessageHandlerTests.java   | 17 ++++-
 .../redis/rules/RedisAvailableTests.java      | 17 ++++-
 src/reference/docbook/redis.xml               | 13 +++-
 src/reference/docbook/whats-new.xml           | 18 +++++
 15 files changed, 223 insertions(+), 88 deletions(-)

diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParser.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParser.java
index 6ade9c2c30..13aab72337 100644
--- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParser.java
+++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParser.java
@@ -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();
 	}
 
diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParser.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParser.java
index b97e31572d..eb507557f1 100644
--- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParser.java
+++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParser.java
@@ -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 } 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();
 	}
 
diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapter.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapter.java
index 0fc7eb78be..d1c7d17dc2 100644
--- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapter.java
+++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapter.java
@@ -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,6 +34,7 @@ import org.springframework.util.Assert;
 /**
  * @author Mark Fisher
  * @author Oleg Zhurakousky
+ * @author Gary Russell
  * @since 2.1
  */
 public class RedisInboundChannelAdapter extends MessageProducerSupport {
@@ -52,7 +53,6 @@ public class RedisInboundChannelAdapter extends MessageProducerSupport {
 	}
 
 	public void setSerializer(RedisSerializer serializer) {
-		Assert.notNull(serializer, "'serializer' must not be null");
 		this.serializer = serializer;
 	}
 
@@ -99,16 +99,16 @@ public class RedisInboundChannelAdapter extends MessageProducerSupport {
 		this.container.stop();
 	}
 
-	private Message convertMessage(String s) {
-		return this.messageConverter.toMessage(s);
+	private Message convertMessage(Object object) {
+		return this.messageConverter.toMessage(object);
 	}
 
 
 	private class MessageListenerDelegate {
 
 		@SuppressWarnings("unused")
-		public void handleMessage(String s) {
-			sendMessage(convertMessage(s));
+		public void handleMessage(Object object) {
+			sendMessage(convertMessage(object));
 		}
 	}
 
diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandler.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandler.java
index 731e54265b..2460607608 100644
--- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandler.java
+++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandler.java
@@ -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,10 +17,14 @@
 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.Message;
+import org.springframework.integration.expression.IntegrationEvaluationContextAware;
 import org.springframework.integration.handler.AbstractMessageHandler;
 import org.springframework.integration.support.converter.MessageConverter;
 import org.springframework.integration.support.converter.SimpleMessageConverter;
@@ -28,21 +32,32 @@ import org.springframework.util.Assert;
 
 /**
  * @author Mark Fisher
+ * @author Artem Bilan
  * @since 2.1
  */
-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();
+		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) {
@@ -55,27 +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));
 	}
 
-	@Override
-	protected void handleMessageInternal(Message message) throws Exception {
-		String topic = this.determineTopic(message);
-		Object value = this.messageConverter.fromMessage(message);
-		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);
+
+		if (value instanceof byte[]) {
+			this.template.convertAndSend(topic, value);
+		}
+		else {
+			this.template.convertAndSend(topic, ((RedisSerializer) this.serializer).serialize(value));
+		}
+	}
+
 }
diff --git a/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd b/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd
index 274879d1d4..450916967e 100644
--- a/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd
+++ b/spring-integration-redis/src/main/resources/org/springframework/integration/redis/config/spring-integration-redis-3.0.xsd
@@ -174,7 +174,9 @@
 				
 					
 						
-						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.
 						
 						
 							
@@ -197,7 +199,22 @@
 					
 						
 					
-					
+					
+						
+							
+						
+					
+					
+						
+							
+						
+					
 					
 						
 							
 
+	
+
+
 
diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParserTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParserTests.java
index e9bbe29fdc..ed0e51b9ed 100644
--- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParserTests.java
+++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisInboundChannelAdapterParserTests.java
@@ -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;
@@ -67,6 +69,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
diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests-context.xml b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests-context.xml
index b5126265a3..5041900e54 100644
--- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests-context.xml
+++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests-context.xml
@@ -11,7 +11,7 @@
 
 	
 
@@ -21,6 +21,12 @@
 		
 	
 
+	
+
+	
+		
+	
+
 	
 		
 	
diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests.java
index 52010d1a65..05e8deb1c6 100644
--- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests.java
+++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/config/RedisOutboundChannelAdapterParserTests.java
@@ -25,6 +25,7 @@ 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.expression.Expression;
 import org.springframework.integration.Message;
 import org.springframework.integration.MessageChannel;
 import org.springframework.integration.channel.QueueChannel;
@@ -33,6 +34,7 @@ import org.springframework.integration.message.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.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -53,13 +55,16 @@ public class RedisOutboundChannelAdapterParserTests extends RedisAvailableTests{
 
 	@Test
 	@RedisAvailable
+	@SuppressWarnings("unchecked")
 	public void validateConfiguration() {
 		EventDrivenConsumer adapter = context.getBean("outboundAdapter", EventDrivenConsumer.class);
 		RedisPublishingMessageHandler handler = (RedisPublishingMessageHandler)
 				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 +79,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
diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapterTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapterTests.java
index a94c429741..279ae1e9aa 100644
--- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapterTests.java
+++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisInboundChannelAdapterTests.java
@@ -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.integration.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();
+		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();
 	}
 
 }
diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandlerTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandlerTests.java
index 71f4b97f77..dc3648cc06 100644
--- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandlerTests.java
+++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/outbound/RedisPublishingMessageHandlerTests.java
@@ -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.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();
 		}
+
 	}
 
 }
diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/rules/RedisAvailableTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/rules/RedisAvailableTests.java
index 064f5cf3d7..4adb288a48 100644
--- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/rules/RedisAvailableTests.java
+++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/rules/RedisAvailableTests.java
@@ -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 rt = new RedisTemplate();
 		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();
diff --git a/src/reference/docbook/redis.xml b/src/reference/docbook/redis.xml
index 367ab41d38..1bd7c2023a 100644
--- a/src/reference/docbook/redis.xml
+++ b/src/reference/docbook/redis.xml
@@ -152,6 +152,12 @@ rt.setConnectionFactory(redisConnectionFactory);]]>
 
       Inbound adapters can subscribe to multiple topic names hence the comma-delimited set of values in the
       topics attribute.
+      
+         Inbound adapters can use a RedisSerializer to deserialize the body of Redis Messages.
+         The serializer attribute of the <int-redis:inbound-channel-adapter> can be set to an
+         empty string, which results in a null value for the RedisSerializer property.
+         In this case the raw byte[] bodies of Redis Messages are provided as the message payloads.
+      
     
 
     
@@ -178,11 +184,16 @@ rt.setConnectionFactory(redisConnectionFactory);]]> a RedisConnectionFactory which was defined with 'redisConnectionFactory' as its bean name. This example also includes the optional, custom MessageConverter (the 'testConverter' bean). + + Since Spring Integration 3.0, the <int-redis:outbound-channel-adapter>, + as an alternative to the topic attribute, has the topic-expression attribute to determine + the Redis topic against the Message at runtime. These attributes are mutually exclusive. +
Redis Queue Inbound Channel Adapter - Since Spring Integration 3.0, a Queue Inbound Channel Adapter + Since Spring Integration 3.0, 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. .
+
+ Redis Adapters Changers + + + + The Redis Inbound Channel Adapter can now use a null value for serializer + property, with the raw data being the message payload. + + + The Redis Outbound Channel Adapter now has the topic-expression property to determine + the Redis topic against the Message at runtime. + + + + + For more information, see . + +
From 4cf0e2e673a77828851b0d1834d9a692b5d84e4b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 5 Nov 2013 12:21:47 +0200 Subject: [PATCH 10/10] INT-3196: RedisQueueMDE: Add RecoveryInterval Catch `RedisSystemException` in the `ListenerTask` and sleep current Thread with `recoveryInterval` if the Endpoint is `active` before the next `restart()` JIRA: https://jira.springsource.org/browse/INT-3196 RedisConnFailure & RedisSys Exceptions recovery Introduce `RedisIntegrationEvent`s * Catch all `Exception`s on `this.boundListOperations.rightPop` * Log them and send within `RedisExceptionEvent` * Mark some fields with `volatile` --- ...RedisQueueInboundChannelAdapterParser.java | 35 ++-------- .../redis/event/RedisExceptionEvent.java | 30 ++++++++ .../redis/event/RedisIntegrationEvent.java | 37 ++++++++++ .../integration/redis/event/package-info.java | 4 ++ .../RedisQueueMessageDrivenEndpoint.java | 62 +++++++++++++--- .../RedisQueueOutboundChannelAdapter.java | 2 +- .../RedisQueueMessageDrivenEndpointTests.java | 70 ++++++++++++++++++- 7 files changed, 197 insertions(+), 43 deletions(-) create mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisExceptionEvent.java create mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisIntegrationEvent.java create mode 100644 spring-integration-redis/src/main/java/org/springframework/integration/redis/event/package-info.java diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java index ab0cf50cc3..18996384da 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/config/RedisQueueInboundChannelAdapterParser.java @@ -18,11 +18,10 @@ package org.springframework.integration.redis.config; import org.w3c.dom.Element; -import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractChannelAdapterParser; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; import org.springframework.util.StringUtils; @@ -33,29 +32,11 @@ import org.springframework.util.StringUtils; * @author Artem Bilan * @since 3.0 */ -public class RedisQueueInboundChannelAdapterParser extends AbstractSingleBeanDefinitionParser { +public class RedisQueueInboundChannelAdapterParser extends AbstractChannelAdapterParser { @Override - protected Class getBeanClass(Element element) { - return RedisQueueMessageDrivenEndpoint.class; - } - - @Override - protected final String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) - throws BeanDefinitionStoreException { - String id = element.getAttribute("id"); - if (!element.hasAttribute("channel")) { - // the created channel will get the 'id', so the adapter's bean name includes a suffix - id = id + ".adapter"; - } - else if (!StringUtils.hasText(id)) { - id = parserContext.getReaderContext().generateBeanName(definition); - } - return id; - } - - @Override - protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + 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"); @@ -69,15 +50,9 @@ public class RedisQueueInboundChannelAdapterParser extends AbstractSingleBeanDef IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expect-message"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "receive-timeout"); - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP); - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE); - - String channelName = element.getAttribute("channel"); - if (!StringUtils.hasText(channelName)) { - channelName = IntegrationNamespaceUtils.createDirectChannel(element, parserContext); - } builder.addPropertyReference("outputChannel", channelName); + return builder.getBeanDefinition(); } } diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisExceptionEvent.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisExceptionEvent.java new file mode 100644 index 0000000000..62115bfa6a --- /dev/null +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisExceptionEvent.java @@ -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); + } + +} diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisIntegrationEvent.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisIntegrationEvent.java new file mode 100644 index 0000000000..e2a91068d1 --- /dev/null +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/RedisIntegrationEvent.java @@ -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); + } + +} diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/package-info.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/package-info.java new file mode 100644 index 0000000000..3c4d6dec86 --- /dev/null +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/event/package-info.java @@ -0,0 +1,4 @@ +/** + * Events generated by the redis module + */ +package org.springframework.integration.redis.event; diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java index 1c33335f27..36d7f85a2c 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpoint.java @@ -18,8 +18,9 @@ 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.data.redis.RedisSystemException; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.BoundListOperations; import org.springframework.data.redis.core.RedisTemplate; @@ -31,6 +32,7 @@ import org.springframework.integration.MessageChannel; import org.springframework.integration.MessagingException; 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; @@ -46,13 +48,17 @@ 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 boundListOperations; - private MessageChannel errorChannel; + private volatile ApplicationEventPublisher applicationEventPublisher; + + private volatile MessageChannel errorChannel; private volatile Executor taskExecutor; @@ -62,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; @@ -81,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; } @@ -129,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(); @@ -160,13 +177,12 @@ public class RedisQueueMessageDrivenEndpoint extends MessageProducerSupport { try { value = this.boundListOperations.rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS); } - catch (RedisSystemException e) { - if (this.active) { - throw e; - } - else { - logger.error(e); - } + 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) { @@ -200,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()); } diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java index 2ff459ec78..b25e058888 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/outbound/RedisQueueOutboundChannelAdapter.java @@ -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; diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java index 5a1c33c8a5..ec28d11af0 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/inbound/RedisQueueMessageDrivenEndpointTests.java @@ -18,9 +18,13 @@ 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; @@ -29,7 +33,13 @@ 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; @@ -40,7 +50,9 @@ import org.springframework.integration.MessageChannel; import org.springframework.integration.MessagingException; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.event.IntegrationEvent; import org.springframework.integration.message.ErrorMessage; +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; @@ -68,7 +80,6 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { @Autowired private PollableChannel symmetricalOutputChannel; - @Test @RedisAvailable @SuppressWarnings("unchecked") @@ -158,7 +169,6 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { assertThat(((Exception) receive.getPayload()).getCause().getMessage(), Matchers.containsString("java.lang.String cannot be cast to org.springframework.integration.Message")); - endpoint.stop(); } @@ -194,4 +204,60 @@ public class RedisQueueMessageDrivenEndpointTests extends RedisAvailableTests { assertEquals(payload, receive.getPayload()); } + @Test + @RedisAvailable + @SuppressWarnings("unchecked") + public void testInt3196Recovery() throws Exception { + String queueName = "test.si.Int3196Recovery"; + QueueChannel channel = new QueueChannel(); + + final List exceptionEvents = new ArrayList(); + + 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.> asList(RedisSystemException.class, RedisConnectionFailureException.class))); + } + + ((InitializingBean) this.connectionFactory).afterPropertiesSet(); + + RedisTemplate redisTemplate = new RedisTemplate(); + 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(); + } + }