diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java index 37dd1d7542..e53e0e5b42 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java @@ -21,6 +21,7 @@ import java.util.concurrent.locks.Lock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.BeanFactory; import org.springframework.integration.EiMessageHeaderAccessor; import org.springframework.integration.channel.NullChannel; @@ -127,6 +128,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH public void setMessageStore(MessageGroupStore store) { this.messageStore = store; store.registerMessageGroupExpiryCallback(new MessageGroupCallback() { + @Override public void execute(MessageGroupStore messageGroupStore, MessageGroup group) { forceComplete(group); } @@ -144,6 +146,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH sequenceAware = this.releaseStrategy instanceof SequenceSizeReleaseStrategy; } + @Override public void setOutputChannel(MessageChannel outputChannel) { Assert.notNull(outputChannel, "'outputChannel' must not be null"); this.outputChannel = outputChannel; @@ -268,17 +271,32 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH try { lock.lockInterruptibly(); try { + MessageGroup groupNow = group; /* - * Refetch the group because it might have changed while we were waiting on + * If the group argument is not already complete, + * re-fetch it because it might have changed while we were waiting on * its lock. If the last modified timestamp changed, defer the completion * because the selection condition may have changed such that the group - * would no longer be eligible. + * would no longer be eligible. If the timestamp changed, it's a completely new + * group and should not be reaped on this cycle. + * + * If the group argument is already complete, do not re-fetch. + * Note: not all message stores provide a direct reference to its internal + * group so the initial 'isComplete()` will only return true for those stores if + * the group was already complete at the time of its selection as a candidate. + * + * If the group is marked complete, only consider it + * for reaping if it's empty (and both timestamps are unaltered). */ - MessageGroup groupNow = this.messageStore.getMessageGroup( - group.getGroupId()); + if (!group.isComplete()) { + groupNow = this.messageStore.getMessageGroup(correlationKey); + } long lastModifiedNow = groupNow.getLastModified(); - if (group.getLastModified() == lastModifiedNow) { - if (groupNow.size() > 0) { + int groupSize = groupNow.size(); + if ((!groupNow.isComplete() || groupSize == 0) + && group.getLastModified() == lastModifiedNow + && group.getTimestamp() == groupNow.getTimestamp()) { + if (groupSize > 0) { if (releaseStrategy.canRelease(groupNow)) { this.completeGroup(correlationKey, groupNow); } @@ -306,7 +324,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH } } } - finally { + finally { if (removeGroup) { this.remove(group); } @@ -347,7 +365,8 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH + correlationKey + "] to: " + outputChannel); } completeGroup(correlationKey, group); - } else { + } + else { if (logger.isDebugEnabled()) { logger.debug("Discarding messages of partially complete group with key [" + correlationKey + "] to: " + discardChannel); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/channel/DefaultHeaderChannelRegistry.java b/spring-integration-core/src/main/java/org/springframework/integration/channel/DefaultHeaderChannelRegistry.java new file mode 100644 index 0000000000..065d4fc31e --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/channel/DefaultHeaderChannelRegistry.java @@ -0,0 +1,250 @@ +/* + * 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.channel; + +import java.util.Date; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.context.SmartLifecycle; +import org.springframework.integration.context.IntegrationObjectSupport; +import org.springframework.integration.support.channel.BeanFactoryChannelResolver; +import org.springframework.integration.support.channel.HeaderChannelRegistry; +import org.springframework.messaging.MessageChannel; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.Assert; + +/** + * Converts a channel to a name, retaining a reference to the channel keyed by the name. + * Allows a downstream {@link BeanFactoryChannelResolver} to find the channel by name + * in the event that the flow serialized the message at some point. + * Channels are expired after a configurable delay (60 seconds by default). + * The actual average expiry time will be 1.5x the delay. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class DefaultHeaderChannelRegistry extends IntegrationObjectSupport + implements HeaderChannelRegistry, SmartLifecycle, Runnable { + + private static final int DEFAULT_REAPER_DELAY = 60000; + + private final Map channels = new ConcurrentHashMap(); + + private static final AtomicLong id = new AtomicLong(); + + private final String uuid = UUID.randomUUID().toString() + ":"; + + private volatile long reaperDelay; + + private volatile ScheduledFuture reaperScheduledFuture; + + private volatile boolean running; + + private volatile int phase; + + private volatile boolean autoStartup = true; + + /** + * Constructs a registry with the default delay for channel expiry. + */ + public DefaultHeaderChannelRegistry() { + this(DEFAULT_REAPER_DELAY); + } + + /** + * Constructs a registry with the provided delay (milliseconds) for + * channel expiry. + * + * @param reaperDelay the delay in milliseconds. + */ + public DefaultHeaderChannelRegistry(long reaperDelay) { + this.setReaperDelay(reaperDelay); + } + + /** + * Set the reaper delay. + * + * @param reaperDelay the delay in milliseconds. + */ + public final void setReaperDelay(long reaperDelay) { + Assert.isTrue(reaperDelay > 0, "'reaperDelay' must be > 0"); + this.reaperDelay = reaperDelay; + } + + public final long getReaperDelay() { + return reaperDelay; + } + + @Override + public void setTaskScheduler(TaskScheduler taskScheduler) { + super.setTaskScheduler(taskScheduler); + } + + @Override + public int getPhase() { + return this.phase; + } + + public final void setPhase(int phase) { + this.phase = phase; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + public final void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public final int size() { + return this.channels.size(); + } + + @Override + protected void onInit() throws Exception { + super.onInit(); + Assert.notNull(this.getTaskScheduler(), "a task scheduler is required"); + } + + @Override + public synchronized void start() { + if (!this.running) { + Assert.notNull(this.getTaskScheduler(), "a task scheduler is required"); + this.reaperScheduledFuture = this.getTaskScheduler().schedule(this, + new Date(System.currentTimeMillis() + this.reaperDelay)); + this.running = true; + } + } + + @Override + public synchronized void stop() { + this.running = false; + if (this.reaperScheduledFuture != null) { + this.reaperScheduledFuture.cancel(true); + } + } + + @Override + public void stop(Runnable callback) { + this.stop(); + callback.run(); + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public Object channelToChannelName(Object channel) { + if (channel != null && channel instanceof MessageChannel) { + String name = this.uuid + DefaultHeaderChannelRegistry.id.incrementAndGet(); + channels.put(name, new MessageChannelWrapper((MessageChannel) channel)); + if (logger.isDebugEnabled()) { + logger.debug("Registered " + channel + " as " + name); + } + return name; + } + else { + return channel; + } + } + + @Override + public MessageChannel channelNameToChannel(String name) { + if (name != null) { + MessageChannelWrapper messageChannelWrapper = this.channels.get(name); + if (logger.isDebugEnabled() && messageChannelWrapper != null) { + logger.debug("Retrieved " + messageChannelWrapper.getChannel() + " with " + name); + } + return messageChannelWrapper == null ? null : messageChannelWrapper.getChannel(); + } + return null; + } + + /** + * Cancel the scheduled reap task and run immediately; then reschedule. + */ + @Override + public void runReaper() { + synchronized(this) { + this.reaperScheduledFuture.cancel(false); + this.reaperScheduledFuture = null; + } + this.run(); + } + + @Override + public void run() { + this.reaperScheduledFuture = null; + if (logger.isTraceEnabled()) { + logger.trace("Reaper started; channels size=" + this.channels.size()); + } + Iterator> iterator = this.channels.entrySet().iterator(); + long threshold = System.currentTimeMillis() - this.reaperDelay; + while (iterator.hasNext()) { + Entry entry = iterator.next(); + if (entry.getValue().getCreated() < threshold) { + if (logger.isDebugEnabled()) { + logger.debug("Expiring " + entry.getKey() + " (" + entry.getValue().getChannel() + ")"); + } + iterator.remove(); + } + } + synchronized (this) { + if (this.reaperScheduledFuture == null) { + this.reaperScheduledFuture = this.getTaskScheduler().schedule(this, + new Date(System.currentTimeMillis() + this.reaperDelay)); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Reaper completed; channels size=" + this.channels.size()); + } + } + + + private class MessageChannelWrapper { + + private final MessageChannel channel; + + private final long created; + + private MessageChannelWrapper(MessageChannel channel) { + this.channel = channel; + this.created = System.currentTimeMillis(); + } + + public final long getCreated() { + return created; + } + + public final MessageChannel getChannel() { + return channel; + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java index 104d17e7e4..dc4d8a1164 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java @@ -16,7 +16,10 @@ package org.springframework.integration.config.xml; -import static org.springframework.integration.context.IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -26,8 +29,10 @@ import org.w3c.dom.Node; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionDecorator; @@ -35,9 +40,14 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.NamespaceHandler; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.integration.channel.DefaultHeaderChannelRegistry; import org.springframework.integration.config.IntegrationEvaluationContextFactoryBean; import org.springframework.integration.config.xml.ChannelInitializer.AutoCreateCandidatesCollector; import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.context.IntegrationProperties; import org.springframework.integration.expression.IntegrationEvaluationContextAwareBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -68,15 +78,53 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa private final NamespaceHandlerDelegate delegate = new NamespaceHandlerDelegate(); + @Override public final BeanDefinition parse(Element element, ParserContext parserContext) { this.verifySchemaVersion(element, parserContext); this.registerImplicitChannelCreator(parserContext); this.registerIntegrationEvaluationContext(parserContext); + this.registerIntegrationProperties(parserContext); + this.registerHeaderChannelRegistry(parserContext); this.registerBuiltInBeans(parserContext); this.registerDefaultConfiguringBeanFactoryPostProcessorIfNecessary(parserContext); return this.delegate.parse(element, parserContext); } + private void registerIntegrationProperties(ParserContext parserContext) { + + boolean alreadyRegistered = false; + BeanDefinitionRegistry registry = parserContext.getRegistry(); + if (registry instanceof ListableBeanFactory) { + alreadyRegistered = ((ListableBeanFactory) registry) + .containsBean(IntegrationContextUtils.INTEGRATION_PROPERTIES_BEAN_NAME); + } + else { + alreadyRegistered = registry.isBeanNameInUse(IntegrationContextUtils.INTEGRATION_PROPERTIES_BEAN_NAME); + } + if (!alreadyRegistered) { + ResourcePatternResolver resourceResolver = + new PathMatchingResourcePatternResolver(parserContext.getReaderContext().getBeanClassLoader()); + try { + Resource[] defaultResources = resourceResolver.getResources("classpath*:META-INF/spring.integration.default.properties"); + Resource[] userResources = resourceResolver.getResources("classpath*:META-INF/spring.integration.properties"); + + List resources = new LinkedList(Arrays.asList(defaultResources)); + resources.addAll(Arrays.asList(userResources)); + + BeanDefinitionBuilder integrationPropertiesBuilder = BeanDefinitionBuilder + .genericBeanDefinition(PropertiesFactoryBean.class) + .addPropertyValue("locations", resources); + + registry.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_PROPERTIES_BEAN_NAME, + integrationPropertiesBuilder.getBeanDefinition()); + } + catch (IOException e) { + parserContext.getReaderContext().warning("Cannot load 'spring.integration.properties' Resources.", null, e); + } + } + } + + @Override public final BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext) { return this.delegate.decorate(source, definition, parserContext); } @@ -98,7 +146,10 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa alreadyRegistered = parserContext.getRegistry().isBeanNameInUse(CHANNEL_INITIALIZER_BEAN_NAME); } if (!alreadyRegistered) { - BeanDefinitionBuilder channelDef = BeanDefinitionBuilder.genericBeanDefinition(ChannelInitializer.class); + String channelsAutoCreateExpression = "#{@" +IntegrationContextUtils.INTEGRATION_PROPERTIES_BEAN_NAME + + "['" + IntegrationProperties.CHANNELS_AUTOCREATE + "']}"; + BeanDefinitionBuilder channelDef = BeanDefinitionBuilder.genericBeanDefinition(ChannelInitializer.class) + .addPropertyValue("autoCreate", channelsAutoCreateExpression); BeanDefinitionHolder channelCreatorHolder = new BeanDefinitionHolder(channelDef.getBeanDefinition(), CHANNEL_INITIALIZER_BEAN_NAME); BeanDefinitionReaderUtils.registerBeanDefinition(channelCreatorHolder, parserContext.getRegistry()); } @@ -127,10 +178,11 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa // unlike DefaultConfiguringBeanFactoryPostProcessor, we need one of these per registry // therefore we need to call containsBeanDefinition(..) which does not consider the parent registry alreadyRegistered = ((ListableBeanFactory) parserContext.getRegistry()).containsBeanDefinition( - INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME); + IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME); } else { - alreadyRegistered = parserContext.getRegistry().isBeanNameInUse(INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME); + alreadyRegistered = parserContext.getRegistry().isBeanNameInUse( + IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME); } if (!alreadyRegistered) { BeanDefinitionBuilder integrationEvaluationContextBuilder = BeanDefinitionBuilder @@ -171,6 +223,31 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa } } + String xpathBeanName = "xpath"; + alreadyRegistered = false; + if (parserContext.getRegistry() instanceof ListableBeanFactory) { + alreadyRegistered = ((ListableBeanFactory) parserContext.getRegistry()).containsBean(xpathBeanName); + } + else { + alreadyRegistered = parserContext.getRegistry().isBeanNameInUse(xpathBeanName); + } + if (!alreadyRegistered) { + Class xpathClass = null; + try { + xpathClass = ClassUtils.forName(IntegrationNamespaceUtils.BASE_PACKAGE + ".xml.xpath.XPathUtils", + parserContext.getReaderContext().getBeanClassLoader()); + } + catch (ClassNotFoundException e) { + logger.debug("SpEL function '#xpath' isn't registered: there is no spring-integration-xml.jar on the classpath."); + } + + if (xpathClass != null) { + IntegrationNamespaceUtils.registerSpelFunctionBean(parserContext.getRegistry(), xpathBeanName, + IntegrationNamespaceUtils.BASE_PACKAGE + ".xml.xpath.XPathUtils", "evaluate"); + } + } + + this.doRegisterBuiltInBeans(parserContext); } @@ -195,6 +272,33 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa } } + /** + * Register a DefaultHeaderChannelRegistry in the given BeanDefinitionRegistry, if necessary. + */ + private void registerHeaderChannelRegistry(ParserContext parserContext) { + boolean alreadyRegistered = false; + if (parserContext.getRegistry() instanceof ListableBeanFactory) { + alreadyRegistered = ((ListableBeanFactory) parserContext.getRegistry()) + .containsBean(IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME); + } + else { + alreadyRegistered = parserContext.getRegistry().isBeanNameInUse( + IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME); + } + if (!alreadyRegistered) { + if (logger.isInfoEnabled()) { + logger.info("No bean named '" + IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME + + "' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created."); + } + BeanDefinitionBuilder schedulerBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultHeaderChannelRegistry.class); + BeanDefinitionHolder replyChannelRegistryComponent = new BeanDefinitionHolder( + schedulerBuilder.getBeanDefinition(), + IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME); + BeanDefinitionReaderUtils.registerBeanDefinition(replyChannelRegistryComponent, parserContext.getRegistry()); + } + } + + protected final void registerBeanDefinitionDecorator(String elementName, BeanDefinitionDecorator decorator) { this.delegate.doRegisterBeanDefinitionDecorator(elementName, decorator); } @@ -225,6 +329,7 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa private class NamespaceHandlerDelegate extends NamespaceHandlerSupport { + @Override public void init() { AbstractIntegrationNamespaceHandler.this.init(); } 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 6da6aec529..2e7d981a87 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 @@ -37,7 +37,6 @@ import org.springframework.util.xml.DomUtils; 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) { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/HeaderEnricherParserSupport.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/HeaderEnricherParserSupport.java index 5dba32f685..fba8d51b2e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/HeaderEnricherParserSupport.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/HeaderEnricherParserSupport.java @@ -29,11 +29,12 @@ import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.expression.DynamicExpression; +import org.springframework.integration.transformer.HeaderEnricher; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; -import org.springframework.integration.expression.DynamicExpression; -import org.springframework.integration.transformer.HeaderEnricher; /** * Base support class for 'header-enricher' parsers. @@ -41,6 +42,7 @@ import org.springframework.integration.transformer.HeaderEnricher; * @author Mark Fisher * @author Oleg Zhurakousky * @author Artem Bilan + * @author Gary Russell * @since 2.0 */ public abstract class HeaderEnricherParserSupport extends AbstractTransformerParser { @@ -49,6 +51,16 @@ public abstract class HeaderEnricherParserSupport extends AbstractTransformerPar private final Map> elementToTypeMap = new HashMap>(); + private final static Map cannedHeaderElementExpressions = new HashMap(); + + static { + cannedHeaderElementExpressions.put("header-channels-to-string", new String[][] { + {"replyChannel", "@" + IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME + + ".channelToChannelName(headers.replyChannel)" }, + {"errorChannel", "@" + IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME + + ".channelToChannelName(headers.errorChannel)" }, + }); + } @Override protected final String getTransformerClassName() { @@ -86,6 +98,8 @@ public abstract class HeaderEnricherParserSupport extends AbstractTransformerPar Element headerElement = (Element) node; String elementName = node.getLocalName(); Class headerType = null; + String expression = null; + String overwrite = headerElement.getAttribute("overwrite"); if ("header".equals(elementName)) { headerName = headerElement.getAttribute(NAME_ATTRIBUTE); } @@ -114,135 +128,157 @@ public abstract class HeaderEnricherParserSupport extends AbstractTransformerPar } } } - if (headerName != null) { - String value = headerElement.getAttribute("value"); - String ref = headerElement.getAttribute(REF_ATTRIBUTE); - String method = headerElement.getAttribute(METHOD_ATTRIBUTE); - String expression = headerElement.getAttribute(EXPRESSION_ATTRIBUTE); - - Element beanElement = null; - Element scriptElement = null; - Element expressionElement = null; - - List subElements = DomUtils.getChildElements(headerElement); - if (!subElements.isEmpty()) { - Element subElement = subElements.get(0); - String subElementLocalName = subElement.getLocalName(); - if ("bean".equals(subElementLocalName)) { - beanElement = subElement; - } - else if ("script".equals(subElementLocalName)) { - scriptElement = subElement; - } - else if ("expression".equals(subElementLocalName)) { - expressionElement = subElement; - } - if (beanElement == null && scriptElement == null && expressionElement == null) { - parserContext.getReaderContext().error("Only 'bean', 'script' or 'expression' can be defined as a sub-element", element); + if (headerName == null) { + if (cannedHeaderElementExpressions.containsKey(elementName)) { + for (int j = 0; j < cannedHeaderElementExpressions.get(elementName).length; j++) { + headerName = cannedHeaderElementExpressions.get(elementName)[j][0]; + expression = cannedHeaderElementExpressions.get(elementName)[j][1]; + overwrite = "true"; + this.addHeader(element, headers, parserContext, headerName, headerElement, headerType, + expression, overwrite); } } - if (StringUtils.hasText(expression) && expressionElement != null) { - parserContext.getReaderContext().error("The 'expression' attribute and sub-element are mutually exclusive", element); - } - - boolean isValue = StringUtils.hasText(value); - boolean isRef = StringUtils.hasText(ref); - boolean hasMethod = StringUtils.hasText(method); - boolean isExpression = StringUtils.hasText(expression) || expressionElement != null; - boolean isScript = scriptElement != null; - - BeanDefinition innerComponentDefinition = null; - - if (beanElement != null) { - innerComponentDefinition = parserContext.getDelegate().parseBeanDefinitionElement(beanElement).getBeanDefinition(); - } - else if (isScript) { - innerComponentDefinition = parserContext.getDelegate().parseCustomElement(scriptElement); - } - - boolean isCustomBean = innerComponentDefinition != null; - - if (hasMethod && isScript) { - parserContext.getReaderContext().error("The 'method' attribute cannot be used when a 'script' sub-element is defined", element); - } - - if (!(isValue ^ (isRef ^ (isExpression ^ isCustomBean)))) { - parserContext.getReaderContext().error( - "Exactly one of the 'ref', 'value', 'expression' or inner bean is required.", element); - } - BeanDefinitionBuilder valueProcessorBuilder = null; - if (isValue) { - if (hasMethod) { - parserContext.getReaderContext().error( - "The 'method' attribute cannot be used with the 'value' attribute.", element); - } - Object headerValue = (headerType != null) ? - new TypedStringValue(value, headerType) : value; - valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( - IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.StaticHeaderValueMessageProcessor"); - valueProcessorBuilder.addConstructorArgValue(headerValue); - } - else if (isExpression) { - if (hasMethod) { - parserContext.getReaderContext().error( - "The 'method' attribute cannot be used with the 'expression' attribute.", element); - } - valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( - IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.ExpressionEvaluatingHeaderValueMessageProcessor"); - if (expressionElement != null) { - BeanDefinitionBuilder dynamicExpressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DynamicExpression.class); - dynamicExpressionBuilder.addConstructorArgValue(expressionElement.getAttribute("key")); - dynamicExpressionBuilder.addConstructorArgReference(expressionElement.getAttribute("source")); - valueProcessorBuilder.addConstructorArgValue(dynamicExpressionBuilder.getBeanDefinition()); - } - else { - valueProcessorBuilder.addConstructorArgValue(expression); - } - valueProcessorBuilder.addConstructorArgValue(headerType); - } - else if (isCustomBean) { - if (StringUtils.hasText(headerElement.getAttribute("type"))) { - parserContext.getReaderContext().error( - "The 'type' attribute cannot be used with an inner bean.", element); - } - if (hasMethod || isScript) { - valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( - IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.MessageProcessingHeaderValueMessageProcessor"); - valueProcessorBuilder.addConstructorArgValue(innerComponentDefinition); - if (hasMethod) { - valueProcessorBuilder.addConstructorArgValue(method); - } - } - else { - valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( - IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.StaticHeaderValueMessageProcessor"); - valueProcessorBuilder.addConstructorArgValue(innerComponentDefinition); - } - } - else { - if (StringUtils.hasText(headerElement.getAttribute("type"))) { - parserContext.getReaderContext().error( - "The 'type' attribute cannot be used with the 'ref' attribute.", element); - } - if (hasMethod) { - valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( - IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.MessageProcessingHeaderValueMessageProcessor"); - valueProcessorBuilder.addConstructorArgReference(ref); - valueProcessorBuilder.addConstructorArgValue(method); - } - else { - valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( - IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.StaticHeaderValueMessageProcessor"); - valueProcessorBuilder.addConstructorArgReference(ref); - } - } - IntegrationNamespaceUtils.setValueIfAttributeDefined(valueProcessorBuilder, headerElement, "overwrite"); - headers.put(headerName, valueProcessorBuilder.getBeanDefinition()); + } + else { + this.addHeader(element, headers, parserContext, headerName, headerElement, headerType, expression, + overwrite); } } } } + private void addHeader(Element element, ManagedMap headers, ParserContext parserContext, + String headerName, Element headerElement, Class headerType, String expression, String overwrite) { + + String value = headerElement.getAttribute("value"); + String ref = headerElement.getAttribute(REF_ATTRIBUTE); + String method = headerElement.getAttribute(METHOD_ATTRIBUTE); + if (expression == null) { + expression = headerElement.getAttribute(EXPRESSION_ATTRIBUTE); + } + + Element beanElement = null; + Element scriptElement = null; + Element expressionElement = null; + + List subElements = DomUtils.getChildElements(headerElement); + if (!subElements.isEmpty()) { + Element subElement = subElements.get(0); + String subElementLocalName = subElement.getLocalName(); + if ("bean".equals(subElementLocalName)) { + beanElement = subElement; + } + else if ("script".equals(subElementLocalName)) { + scriptElement = subElement; + } + else if ("expression".equals(subElementLocalName)) { + expressionElement = subElement; + } + if (beanElement == null && scriptElement == null && expressionElement == null) { + parserContext.getReaderContext().error("Only 'bean', 'script' or 'expression' can be defined as a sub-element", element); + } + } + if (StringUtils.hasText(expression) && expressionElement != null) { + parserContext.getReaderContext().error("The 'expression' attribute and sub-element are mutually exclusive", element); + } + + boolean isValue = StringUtils.hasText(value); + boolean isRef = StringUtils.hasText(ref); + boolean hasMethod = StringUtils.hasText(method); + boolean isExpression = StringUtils.hasText(expression) || expressionElement != null; + boolean isScript = scriptElement != null; + + BeanDefinition innerComponentDefinition = null; + + if (beanElement != null) { + innerComponentDefinition = parserContext.getDelegate().parseBeanDefinitionElement(beanElement).getBeanDefinition(); + } + else if (isScript) { + innerComponentDefinition = parserContext.getDelegate().parseCustomElement(scriptElement); + } + + boolean isCustomBean = innerComponentDefinition != null; + + if (hasMethod && isScript) { + parserContext.getReaderContext().error("The 'method' attribute cannot be used when a 'script' sub-element is defined", element); + } + + if (!(isValue ^ (isRef ^ (isExpression ^ isCustomBean)))) { + parserContext.getReaderContext().error( + "Exactly one of the 'ref', 'value', 'expression' or inner bean is required.", element); + } + BeanDefinitionBuilder valueProcessorBuilder = null; + if (isValue) { + if (hasMethod) { + parserContext.getReaderContext().error( + "The 'method' attribute cannot be used with the 'value' attribute.", element); + } + Object headerValue = (headerType != null) ? + new TypedStringValue(value, headerType) : value; + valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.StaticHeaderValueMessageProcessor"); + valueProcessorBuilder.addConstructorArgValue(headerValue); + } + else if (isExpression) { + if (hasMethod) { + parserContext.getReaderContext().error( + "The 'method' attribute cannot be used with the 'expression' attribute.", element); + } + valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.ExpressionEvaluatingHeaderValueMessageProcessor"); + if (expressionElement != null) { + BeanDefinitionBuilder dynamicExpressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DynamicExpression.class); + dynamicExpressionBuilder.addConstructorArgValue(expressionElement.getAttribute("key")); + dynamicExpressionBuilder.addConstructorArgReference(expressionElement.getAttribute("source")); + valueProcessorBuilder.addConstructorArgValue(dynamicExpressionBuilder.getBeanDefinition()); + } + else { + valueProcessorBuilder.addConstructorArgValue(expression); + } + valueProcessorBuilder.addConstructorArgValue(headerType); + } + else if (isCustomBean) { + if (StringUtils.hasText(headerElement.getAttribute("type"))) { + parserContext.getReaderContext().error( + "The 'type' attribute cannot be used with an inner bean.", element); + } + if (hasMethod || isScript) { + valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.MessageProcessingHeaderValueMessageProcessor"); + valueProcessorBuilder.addConstructorArgValue(innerComponentDefinition); + if (hasMethod) { + valueProcessorBuilder.addConstructorArgValue(method); + } + } + else { + valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.StaticHeaderValueMessageProcessor"); + valueProcessorBuilder.addConstructorArgValue(innerComponentDefinition); + } + } + else { + if (StringUtils.hasText(headerElement.getAttribute("type"))) { + parserContext.getReaderContext().error( + "The 'type' attribute cannot be used with the 'ref' attribute.", element); + } + if (hasMethod) { + valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.MessageProcessingHeaderValueMessageProcessor"); + valueProcessorBuilder.addConstructorArgReference(ref); + valueProcessorBuilder.addConstructorArgValue(method); + } + else { + valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".transformer.support.StaticHeaderValueMessageProcessor"); + valueProcessorBuilder.addConstructorArgReference(ref); + } + } + if (StringUtils.hasText(overwrite)) { + valueProcessorBuilder.addPropertyValue("overwrite", overwrite); + } + headers.put(headerName, valueProcessorBuilder.getBeanDefinition()); + } + /** * Subclasses may override this method to provide any additional processing. */ 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 381a8a896f..d5ad54e417 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 @@ -16,6 +16,8 @@ package org.springframework.integration.context; +import java.util.Properties; + import org.springframework.beans.factory.BeanFactory; import org.springframework.core.convert.ConversionService; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -45,6 +47,12 @@ public abstract class IntegrationContextUtils { public static final String INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME = "integrationEvaluationContext"; + public static final String INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME = "integrationHeaderChannelRegistry"; + + public static final String INTEGRATION_PROPERTIES_BEAN_NAME = "integrationProperties"; + + private static final Properties EMPTY_PROPERTIES = new Properties(); + /** * Return the {@link MetadataStore} bean whose name is "metadataStore". * @param beanFactory BeanFactory for lookup, must not be null. @@ -104,4 +112,24 @@ public abstract class IntegrationContextUtils { return beanFactory.getBean(beanName, type); } + /** + * @return the global {@link IntegrationContextUtils#INTEGRATION_PROPERTIES_BEAN_NAME} + * bean from provided {@code #beanFactory}, which represents the merged + * properties values from all 'META-INF/spring.integration.default.properties' + * and 'META-INF/spring.integration.properties'. + * May return {@link IntegrationContextUtils#EMPTY_PROPERTIES} if there is no + * {@link IntegrationContextUtils#INTEGRATION_PROPERTIES_BEAN_NAME} bean within + * provided {@code #beanFactory} or provided {@code #beanFactory} is null. + */ + public static Properties getIntegrationProperties(BeanFactory beanFactory) { + Properties properties = null; + if (beanFactory != null) { + properties = getBeanOfType(beanFactory, INTEGRATION_PROPERTIES_BEAN_NAME, Properties.class); + } + if (properties == null) { + properties = EMPTY_PROPERTIES; + } + return properties; + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationObjectSupport.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationObjectSupport.java index f3f7756e60..18a3051927 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationObjectSupport.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationObjectSupport.java @@ -16,6 +16,8 @@ package org.springframework.integration.context; +import java.util.Properties; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -47,6 +49,7 @@ import org.springframework.util.StringUtils; * @author Josh Long * @author Stefan Ferstl * @author Gary Russell + * @author Artem Bilan */ public abstract class IntegrationObjectSupport implements BeanNameAware, NamedComponent, ApplicationContextAware, BeanFactoryAware, InitializingBean { @@ -169,6 +172,13 @@ public abstract class IntegrationObjectSupport implements BeanNameAware, NamedCo return this.applicationContext == null ? null : this.applicationContext.getId(); } + /** + * @see IntegrationContextUtils#getIntegrationProperties + */ + protected Properties getIntegrationProperties() { + return IntegrationContextUtils.getIntegrationProperties(this.beanFactory); + } + @Override public String toString() { return (this.beanName != null) ? this.beanName : super.toString(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java new file mode 100644 index 0000000000..c54cb0dae3 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java @@ -0,0 +1,31 @@ +/* + * 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.context; + +/** + * Convention Enumeration to represent keys from 'META-INF/spring.integration.properties'. + * + * @author Artem Bilan + * @since 3.0 + */ +public interface IntegrationProperties { + + String LATE_REPLY_LOGGING_LEVEL = "messagingTemplate.lateReply.logging.level"; + + String CHANNELS_AUTOCREATE = "channels.autoCreate"; + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java index 1e726543a6..5777708b7c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java @@ -23,10 +23,13 @@ import java.util.Set; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.util.Assert; /** *

- * An immutable {@link AbstractMap} implementation that wraps a Map + * An immutable {@link AbstractMap} implementation that wraps a {@code Map}, + * where values must be instances of {@link String} or {@link Expression}, * and evaluates an {@code expression} for the provided {@code key} from the underlying * {@code original} Map. *

@@ -38,16 +41,18 @@ import org.springframework.expression.Expression; *

* A {@link ExpressionEvalMapBuilder} must be used to instantiate this class * via its {@link #from(Map)} method: + *

  * {@code
- * ExpressionEvalMap evalMap = ExpressionEvalMap
- *				.from(expressions)
-				.usingCallback(new EvaluationCallback() {
-						Object evaluate(Expression expression) {
-							// return some expression evaluation
-						}
-					})
-				.build();
- * }
+ *ExpressionEvalMap evalMap = ExpressionEvalMap
+ *	.from(expressions)
+ *	.usingCallback(new EvaluationCallback() {
+ *		Object evaluate(Expression expression) {
+ *			// return some expression evaluation
+ *		}
+ *	})
+ *	.build();
+ *}
+ * 
*

*

* Thread-safety depends on the original underlying Map. @@ -68,11 +73,11 @@ public final class ExpressionEvalMap extends AbstractMap { }; - private final Map original; + private final Map original; private final EvaluationCallback evaluationCallback; - private ExpressionEvalMap(Map original, EvaluationCallback evaluationCallback) { + private ExpressionEvalMap(Map original, EvaluationCallback evaluationCallback) { this.original = original; this.evaluationCallback = evaluationCallback; } @@ -83,8 +88,20 @@ public final class ExpressionEvalMap extends AbstractMap { */ @Override public Object get(Object key) { - Expression expression = original.get(key); - if (expression != null) { + Object value = original.get(key); + if (value != null) { + Expression expression; + if (value instanceof Expression) { + expression = (Expression) value; + } + else if (value instanceof String) { + expression = new LiteralExpression((String) value); + } + else { + throw new IllegalArgumentException("Values must be " + + "'java.lang.String' or 'org.springframework.expression.Expression'; the value type for key " + + key + " is : " + value.getClass()); + } return this.evaluationCallback.evaluate(expression); } return null; @@ -136,7 +153,7 @@ public final class ExpressionEvalMap extends AbstractMap { } @Override - public void putAll(Map m) { + public void putAll(Map m) { throw new UnsupportedOperationException(); } @@ -155,7 +172,13 @@ public final class ExpressionEvalMap extends AbstractMap { throw new UnsupportedOperationException(); } - public static ExpressionEvalMapBuilder from(Map expressions) { + @Override + public String toString() { + return this.original.toString(); + } + + public static ExpressionEvalMapBuilder from(Map expressions) { + Assert.notNull(expressions, "'expressions' must not be null."); return new ExpressionEvalMapBuilder(expressions); } @@ -205,7 +228,7 @@ public final class ExpressionEvalMap extends AbstractMap { */ public static final class ExpressionEvalMapBuilder { - private final Map expressions; + private final Map expressions; private EvaluationCallback evaluationCallback; @@ -219,7 +242,7 @@ public final class ExpressionEvalMap extends AbstractMap { private final ExpressionEvalMapFinalBuilder finalBuilder = new ExpressionEvalMapFinalBuilderImpl(); - private ExpressionEvalMapBuilder(Map expressions) { + private ExpressionEvalMapBuilder(Map expressions) { this.expressions = expressions; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java index 256953d59b..4c9d2bbcbf 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.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. @@ -28,7 +28,6 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.support.converter.SimpleMessageConverter; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.SubscribableChannel; @@ -43,6 +42,7 @@ import org.springframework.util.Assert; * well as the timeout values for sending and receiving Messages. * * @author Mark Fisher + * @author Gary Russell */ public abstract class MessagingGatewaySupport extends AbstractEndpoint implements TrackableComponent { @@ -153,6 +153,7 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement * Specify whether this gateway should be tracked in the Message History * of Messages that originate from its send or sendAndReceive operations. */ + @Override public void setShouldTrack(boolean shouldTrack) { this.historyWritingPostProcessor.setShouldTrack(shouldTrack); } @@ -283,7 +284,11 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement return; } AbstractEndpoint correlator = null; - MessageHandler handler = new BridgeHandler(); + BridgeHandler handler = new BridgeHandler(); + if (this.getBeanFactory() != null) { + handler.setBeanFactory(this.getBeanFactory()); + } + handler.afterPropertiesSet(); if (this.replyChannel instanceof SubscribableChannel) { correlator = new EventDrivenConsumer( (SubscribableChannel) this.replyChannel, handler); @@ -320,6 +325,7 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement private static class DefaultRequestMapper implements InboundMessageMapper { + @Override public Message toMessage(Object object) throws Exception { if (object instanceof Message) { return (Message) object; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java b/spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java index 26c35231ec..b6599cef0d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/metadata/PropertiesPersistingMetadataStore.java @@ -28,6 +28,7 @@ import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; @@ -62,6 +63,7 @@ public class PropertiesPersistingMetadataStore implements MetadataStore, Initial this.baseDirectory = baseDirectory; } + @Override public void afterPropertiesSet() throws Exception { File baseDir = new File(baseDirectory); baseDir.mkdirs(); @@ -78,20 +80,22 @@ public class PropertiesPersistingMetadataStore implements MetadataStore, Initial this.loadMetadata(); } + @Override public void put(String key, String value) { this.metadata.setProperty(key, value); } + @Override public String get(String key) { return this.metadata.getProperty(key); } @Override - @SuppressWarnings("uchecked") public String remove(String key) { return (String) this.metadata.remove(key); } + @Override public void destroy() throws Exception { this.saveMetadata(); } @@ -100,12 +104,12 @@ public class PropertiesPersistingMetadataStore implements MetadataStore, Initial OutputStream outputStream = null; try { outputStream = new BufferedOutputStream(new FileOutputStream(this.file)); - this.persister.store(this.metadata, outputStream, "Last feed entry"); + this.persister.store(this.metadata, outputStream, "Last entry"); } catch (IOException e) { // not fatal for the functionality of the component - logger.warn("Failed to persist feed entry. This may result in a duplicate " - + "feed entry after this component is restarted.", e); + logger.warn("Failed to persist entry. This may result in a duplicate " + + "entry after this component is restarted.", e); } finally { try { @@ -128,8 +132,8 @@ public class PropertiesPersistingMetadataStore implements MetadataStore, Initial } catch (Exception e) { // not fatal for the functionality of the component - logger.warn("Failed to load feed entry from the persistent store. This may result in a duplicate " + - "feed entry after this component is restarted", e); + logger.warn("Failed to load entry from the persistent store. This may result in a duplicate " + + "entry after this component is restarted", e); } finally { try { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/channel/BeanFactoryChannelResolver.java b/spring-integration-core/src/main/java/org/springframework/integration/support/channel/BeanFactoryChannelResolver.java new file mode 100644 index 0000000000..c2164b5da6 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/channel/BeanFactoryChannelResolver.java @@ -0,0 +1,109 @@ +/* + * 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. + */ + +package org.springframework.integration.support.channel; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.core.DestinationResolutionException; +import org.springframework.messaging.core.DestinationResolver; +import org.springframework.util.Assert; + +/** + * {@link ChannelResolver} implementation based on a Spring {@link BeanFactory}. + * + *

Will lookup Spring managed beans identified by bean name, + * expecting them to be of type {@link MessageChannel}. + * + * @author Mark Fisher + * @see org.springframework.beans.factory.BeanFactory + */ +public class BeanFactoryChannelResolver implements DestinationResolver, BeanFactoryAware { + + private final static Log logger = LogFactory.getLog(BeanFactoryChannelResolver.class); + + private volatile BeanFactory beanFactory; + + private volatile HeaderChannelRegistry replyChannelRegistry; + + /** + * Create a new instance of the {@link BeanFactoryChannelResolver} class. + *

The BeanFactory to access must be set via setBeanFactory. + * This will happen automatically if this resolver is defined within an + * ApplicationContext thereby receiving the callback upon initialization. + * @see #setBeanFactory + */ + public BeanFactoryChannelResolver() { + } + + /** + * Create a new instance of the {@link BeanFactoryChannelResolver} class. + *

Use of this constructor is redundant if this object is being created + * by a Spring IoC container as the supplied {@link BeanFactory} will be + * replaced by the {@link BeanFactory} that creates it (c.f. the + * {@link BeanFactoryAware} contract). So only use this constructor if you + * are instantiating this object explicitly rather than defining a bean. + * + * @param beanFactory the bean factory to be used to lookup {@link MessageChannel}s. + */ + public BeanFactoryChannelResolver(BeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.lookupHeaderChannelRegistry(beanFactory); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + this.lookupHeaderChannelRegistry(beanFactory); + } + + private void lookupHeaderChannelRegistry(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + try { + this.replyChannelRegistry = beanFactory.getBean( + IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME, + HeaderChannelRegistry.class); + } + catch (Exception e) { + logger.warn("No HeaderChannelRegistry found", e); + } + } + + @Override + public MessageChannel resolveDestination(String name) { + Assert.state(this.beanFactory != null, "BeanFactory is required"); + try { + return this.beanFactory.getBean(name, MessageChannel.class); + } + catch (BeansException e) { + if (this.replyChannelRegistry != null) { + MessageChannel channel = this.replyChannelRegistry.channelNameToChannel(name); + if (channel != null) { + return channel; + } + } + throw new DestinationResolutionException( + "failed to look up MessageChannel bean with name '" + name + "'", e); + } + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/channel/HeaderChannelRegistry.java b/spring-integration-core/src/main/java/org/springframework/integration/support/channel/HeaderChannelRegistry.java new file mode 100644 index 0000000000..6e0629c2fb --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/channel/HeaderChannelRegistry.java @@ -0,0 +1,62 @@ +/* + * 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.support.channel; + +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.messaging.MessageChannel; + +/** + * Implementations convert a channel to a name, retaining a reference to the channel keyed by the name. + * Allows a downstream {@link BeanFactoryChannelResolver} to find the channel by name in + * the event that the flow serialized the message at some point. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface HeaderChannelRegistry { + + /** + * Converts the channel to a name (String). If the channel is not a + * {@link MessageChannel}, it is returned unchanged. + * + * @param channel The channel. + * @return The channel name, or the channel if it is not a MessageChannel. + */ + public abstract Object channelToChannelName(Object channel); + + /** + * Converts the channel name back to a {@link MessageChannel} (if it is + * registered). + * @param name The name of the channel. + * @return The channel, or null if there is no channel registered with the name. + */ + public abstract MessageChannel channelNameToChannel(String name); + + /** + * @return the current size of the registry + */ + @ManagedAttribute + public abstract int size(); + + /** + * Cancel the scheduled reap task and run immediately; then reschedule. + */ + @ManagedOperation(description = "Cancel the scheduled reap task and run immediately; then reschedule.") + public abstract void runReaper(); + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java index c3f87d4628..dac3be6142 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java @@ -214,6 +214,10 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem this.gateway.setReplyChannel(replyChannel); } + if (this.getBeanFactory() != null) { + this.gateway.setBeanFactory(this.getBeanFactory()); + } + this.gateway.afterPropertiesSet(); } @@ -294,6 +298,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem * Lifecycle implementation. If no requestChannel is defined, this method * has no effect as in that case no Gateway is initialized. */ + @Override public void start() { if (this.gateway != null) { this.gateway.start(); @@ -304,6 +309,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem * Lifecycle implementation. If no requestChannel is defined, this method * has no effect as in that case no Gateway is initialized. */ + @Override public void stop() { if (this.gateway != null) { this.gateway.stop(); @@ -314,6 +320,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem * Lifecycle implementation. If no requestChannel is defined, this method * will return always return true as no Gateway is initialized. */ + @Override public boolean isRunning() { if (this.gateway != null) { return this.gateway.isRunning(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java index a8eef22920..a43593183e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java @@ -28,6 +28,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; @@ -49,15 +50,16 @@ import org.springframework.expression.Expression; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.messaging.Message; import org.springframework.integration.MessageHandlingException; -import org.springframework.messaging.MessagingException; import org.springframework.integration.annotation.Header; import org.springframework.integration.annotation.Headers; import org.springframework.integration.annotation.Payload; import org.springframework.integration.annotation.Payloads; import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.util.ReflectionUtils.MethodFilter; @@ -76,21 +78,31 @@ import org.springframework.util.StringUtils; * @author Gunnar Hillert * @author Soby Chacko * @author Gary Russell + * @author Artem Bilan * * @since 2.0 */ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator { + private static final String CANDIDATE_METHODS = "CANDIDATE_METHODS"; + + private static final String CANDIDATE_MESSAGE_METHODS = "CANDIDATE_MESSAGE_METHODS"; + private final Log logger = LogFactory.getLog(this.getClass()); private final Object targetObject; - private volatile String displayString; private volatile boolean requiresReply; private final Map, HandlerMethod> handlerMethods; + private final Map, HandlerMethod> handlerMessageMethods; + + private final LinkedList, HandlerMethod>> handlerMethodsList; + + private final HandlerMethod handlerMethod; + private final Class expectedType; private final boolean canProcessMessageList; @@ -154,11 +166,12 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator Assert.isTrue(method.getReturnType() != Void.class && method.getReturnType() != Void.TYPE, "method must have a return type"); } - HandlerMethod handlerMethod = new HandlerMethod(method, canProcessMessageList); Assert.notNull(targetObject, "targetObject must not be null"); this.targetObject = targetObject; - this.handlerMethods = Collections., HandlerMethod> singletonMap(handlerMethod.getTargetParameterType() - .getObjectType(), handlerMethod); + this.handlerMethod = new HandlerMethod(method, canProcessMessageList); + this.handlerMethods = null; + this.handlerMessageMethods = null; + this.handlerMethodsList = null; this.prepareEvaluationContext(this.getEvaluationContext(false), method, annotationType); this.setDisplayString(targetObject, method); } @@ -170,7 +183,32 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator this.expectedType = expectedType; this.targetObject = targetObject; this.requiresReply = expectedType != null; - this.handlerMethods = this.findHandlerMethodsForTarget(targetObject, annotationType, methodName, requiresReply); + Map, HandlerMethod>> handlerMethodsForTarget = + this.findHandlerMethodsForTarget(targetObject, annotationType, methodName, requiresReply); + Map, HandlerMethod> handlerMethods = handlerMethodsForTarget.get(CANDIDATE_METHODS); + Map, HandlerMethod> handlerMessageMethods = handlerMethodsForTarget.get(CANDIDATE_MESSAGE_METHODS); + if ((handlerMethods.size() == 1 && handlerMessageMethods.isEmpty()) || + (handlerMessageMethods.size() == 1 && handlerMethods.isEmpty())) { + if (handlerMethods.size() == 1) { + this.handlerMethod = handlerMethods.values().iterator().next(); + } + else { + this.handlerMethod = handlerMessageMethods.values().iterator().next(); + } + this.handlerMethods = null; + this.handlerMessageMethods = null; + this.handlerMethodsList = null; + } + else { + this.handlerMethod = null; + this.handlerMethods = handlerMethods; + this.handlerMessageMethods = handlerMessageMethods; + this.handlerMethodsList = new LinkedList, HandlerMethod>>(); + + //TODO Consider to use global option to determine a precedence of methods + this.handlerMethodsList.add(this.handlerMethods); + this.handlerMethodsList.add(this.handlerMessageMethods); + } this.prepareEvaluationContext(this.getEvaluationContext(false), methodName, annotationType); this.setDisplayString(targetObject, methodName); } @@ -181,7 +219,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator sb.append("." + ((Method) targetMethod).getName()); } else if (targetMethod instanceof String) { - sb.append("." + (String) targetMethod); + sb.append("." + targetMethod); } this.displayString = sb.toString() + "]"; } @@ -192,7 +230,8 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator if (method instanceof Method) { context.registerMethodFilter(targetType, new FixedMethodFilter((Method) method)); if (expectedType != null) { - Assert.state(context.getTypeConverter().canConvert(TypeDescriptor.valueOf(((Method) method).getReturnType()), TypeDescriptor.valueOf(expectedType)), + Assert.state(context.getTypeConverter().canConvert(TypeDescriptor.valueOf(((Method) method).getReturnType()), + TypeDescriptor.valueOf(expectedType)), "Cannot convert to expected type (" + expectedType + ") from " + method); } } @@ -220,64 +259,48 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } private T processInternal(ParametersWrapper parameters) throws Exception { - Throwable evaluationException = null; - List candidates = this.findHandlerMethodsForParameters(parameters); - Assert.state(!candidates.isEmpty(), "No candidate methods found for messages."); - for (HandlerMethod candidate : candidates) { - try { - Expression expression = candidate.getExpression(); - Class expectedType = this.expectedType != null ? this.expectedType : candidate.method.getReturnType(); - @SuppressWarnings("unchecked") - T result = (T) this.evaluateExpression(expression, parameters, expectedType); - if (this.requiresReply) { - Assert.notNull(result, - "Expression evaluation result was null, but this processor requires a reply."); - } - return result; - } - // keep the first exception - catch (EvaluationException e) { - if (evaluationException == null) { - evaluationException = e.getCause(); - } - if (evaluationException == null) { - evaluationException = e; - } - } - catch (MessageHandlingException e) { - if (evaluationException == null) { - evaluationException = e.getCause(); - } - if (evaluationException == null) { - evaluationException = e; - } - } - catch (Exception e) { - if (evaluationException == null) { - evaluationException = e; - } + HandlerMethod candidate = this.findHandlerMethodForParameters(parameters); + Assert.notNull(candidate, "No candidate methods found for messages."); + Expression expression = candidate.getExpression(); + Class expectedType = this.expectedType != null ? this.expectedType : candidate.method.getReturnType(); + try { + @SuppressWarnings("unchecked") + T result = (T) this.evaluateExpression(expression, parameters, expectedType); + if (this.requiresReply) { + Assert.notNull(result, + "Expression evaluation result was null, but this processor requires a reply."); } + return result; } - if (evaluationException instanceof Exception) { - throw (Exception) evaluationException; - } - else if (evaluationException instanceof Error) { - throw (Error) evaluationException; - } - else { - throw new IllegalStateException("Cannot process message", evaluationException); + catch (Exception e) { + Throwable evaluationException = e; + if ((e instanceof EvaluationException || e instanceof MessageHandlingException) && e.getCause() != null) { + evaluationException = e.getCause(); + } + if (evaluationException instanceof Exception) { + throw (Exception) evaluationException; + } + else { + throw new IllegalStateException("Cannot process message", evaluationException); + } } } - private Map, HandlerMethod> findHandlerMethodsForTarget(final Object targetObject, + private Map, HandlerMethod>> findHandlerMethodsForTarget(final Object targetObject, final Class annotationType, final String methodName, final boolean requiresReply) { + Map, HandlerMethod>> handlerMethods = new HashMap, HandlerMethod>>(); + final Map, HandlerMethod> candidateMethods = new HashMap, HandlerMethod>(); + final Map, HandlerMethod> candidateMessageMethods = new HashMap, HandlerMethod>(); final Map, HandlerMethod> fallbackMethods = new HashMap, HandlerMethod>(); + final Map, HandlerMethod> fallbackMessageMethods = new HashMap, HandlerMethod>(); final AtomicReference> ambiguousFallbackType = new AtomicReference>(); + final AtomicReference> ambiguousFallbackMessageGenericType = new AtomicReference>(); final Class targetClass = this.getTargetClass(targetObject); MethodFilter methodFilter = new UniqueMethodFilter(targetClass); ReflectionUtils.doWithMethods(targetClass, new MethodCallback() { + @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { boolean matchesAnnotation = false; if (method.isBridge()) { @@ -311,37 +334,75 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } return; } - Class targetParameterType = handlerMethod.getTargetParameterType().getObjectType(); + Class targetParameterType = handlerMethod.getTargetParameterType(); if (matchesAnnotation || annotationType == null) { - Assert.isTrue(!candidateMethods.containsKey(targetParameterType), - "Found more than one method match for type [" + targetParameterType + "]"); - candidateMethods.put(targetParameterType, handlerMethod); + if (handlerMethod.isMessageMethod()) { + if (candidateMessageMethods.containsKey(targetParameterType)) { + throw new IllegalArgumentException("Found more than one method match for type " + + "[Message<" + targetParameterType + ">]"); + } + candidateMessageMethods.put(targetParameterType, handlerMethod); + } + else { + if (candidateMethods.containsKey(targetParameterType)) { + String exceptionMessage = "Found more than one method match for "; + if (Void.class.equals(targetParameterType)) { + exceptionMessage += "empty parameter for 'payload'"; + } + else { + exceptionMessage += "type [" + targetParameterType + "]"; + } + throw new IllegalArgumentException(exceptionMessage); + } + candidateMethods.put(targetParameterType, handlerMethod); + } } else { - if (fallbackMethods.containsKey(targetParameterType)) { - // we need to check for duplicate type matches, - // but only if we end up falling back - // and we'll only keep track of the first one - ambiguousFallbackType.compareAndSet(null, targetParameterType); + if (handlerMethod.isMessageMethod()) { + if (fallbackMessageMethods.containsKey(targetParameterType)) { + // we need to check for duplicate type matches, + // but only if we end up falling back + // and we'll only keep track of the first one + ambiguousFallbackMessageGenericType.compareAndSet(null, targetParameterType); + } + fallbackMessageMethods.put(targetParameterType, handlerMethod); + } + else { + if (fallbackMethods.containsKey(targetParameterType)) { + // we need to check for duplicate type matches, + // but only if we end up falling back + // and we'll only keep track of the first one + ambiguousFallbackType.compareAndSet(null, targetParameterType); + } + fallbackMethods.put(targetParameterType, handlerMethod); } - fallbackMethods.put(targetParameterType, handlerMethod); } } }, methodFilter); - if (!candidateMethods.isEmpty()) { - return candidateMethods; + + if (!candidateMethods.isEmpty() || !candidateMessageMethods.isEmpty()) { + handlerMethods.put(CANDIDATE_METHODS, candidateMethods); + handlerMethods.put(CANDIDATE_MESSAGE_METHODS, candidateMessageMethods); + return handlerMethods; } - if ((fallbackMethods.isEmpty() || ambiguousFallbackType.get() != null) && ServiceActivator.class.equals(annotationType)) { - // a Service Activator can fallback to either MessageHandler.handleMessage(m) or RequestReplyExchanger.exchange(m) + if ((ambiguousFallbackType.get() != null + || ambiguousFallbackMessageGenericType.get() != null) + && ServiceActivator.class.equals(annotationType)) { + /* + * When there are ambiguous fallback methods, + * a Service Activator can finally fallback to RequestReplyExchanger.exchange(m). + * Ambiguous means > 1 method that takes the same payload type, or > 1 method + * that takes a Message with the same generic type. + */ List frameworkMethods = new ArrayList(); Class[] allInterfaces = org.springframework.util.ClassUtils.getAllInterfacesForClass(targetClass); for (Class iface : allInterfaces) { try { if ("org.springframework.integration.gateway.RequestReplyExchanger".equals(iface.getName())) { frameworkMethods.add(targetClass.getMethod("exchange", Message.class)); - } - else if ("org.springframework.messaging.MessageHandler".equals(iface.getName()) && !requiresReply) { - frameworkMethods.add(targetClass.getMethod("handleMessage", Message.class)); + if (logger.isDebugEnabled()) { + logger.debug(targetObject.getClass() + ": Ambiguous fallback methods; using RequestReplyExchanger.exchange()"); + } } } catch (Exception e) { @@ -350,14 +411,30 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } if (frameworkMethods.size() == 1) { HandlerMethod handlerMethod = new HandlerMethod(frameworkMethods.get(0), canProcessMessageList); - return Collections., HandlerMethod>singletonMap(Object.class, handlerMethod); + handlerMethods.put(CANDIDATE_METHODS, Collections., HandlerMethod>singletonMap(Object.class, handlerMethod)); + handlerMethods.put(CANDIDATE_MESSAGE_METHODS, candidateMessageMethods); + return handlerMethods; } } - Assert.notEmpty(fallbackMethods, "Target object of type [" + this.targetObject.getClass() - + "] has no eligible methods for handling Messages."); + + try { + Assert.state(!fallbackMethods.isEmpty() || !fallbackMessageMethods.isEmpty(), + "Target object of type [" + this.targetObject.getClass() + "] has no eligible methods for handling Messages."); + } + catch (Exception e) { + //TODO backward compatibility + throw new IllegalArgumentException(e.getMessage()); + } + Assert.isNull(ambiguousFallbackType.get(), "Found ambiguous parameter type [" + ambiguousFallbackType + "] for method match: " + fallbackMethods.values()); - return fallbackMethods; + Assert.isNull(ambiguousFallbackMessageGenericType.get(), + "Found ambiguous parameter type [" + ambiguousFallbackMessageGenericType + "] for method match: " + + fallbackMethods.values()); + + handlerMethods.put(CANDIDATE_METHODS, fallbackMethods); + handlerMethods.put(CANDIDATE_MESSAGE_METHODS, fallbackMessageMethods); + return handlerMethods; } private Class getTargetClass(Object targetObject) { @@ -388,22 +465,40 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator return targetClass; } - private List findHandlerMethodsForParameters(ParametersWrapper parameters) { + private HandlerMethod findHandlerMethodForParameters(ParametersWrapper parameters) { + if (this.handlerMethod != null) { + return this.handlerMethod; + } + final Class payloadType = parameters.getFirstParameterType(); + HandlerMethod closestMatch = this.findClosestMatch(payloadType); if (closestMatch != null) { - return Collections.singletonList(closestMatch); + return closestMatch; + } - return new ArrayList(this.handlerMethods.values()); + + if (Iterable.class.isAssignableFrom(payloadType) && this.handlerMethods.containsKey(Iterator.class)) { + return this.handlerMethods.get(Iterator.class); + } + else { + return this.handlerMethods.get(Void.class); + } + } private HandlerMethod findClosestMatch(Class payloadType) { - Set> candidates = this.handlerMethods.keySet(); - Class match = null; - if (candidates != null && !candidates.isEmpty()) { - match = ClassUtils.findClosestMatch(payloadType, candidates, true); + for (Map, HandlerMethod> handlerMethods : handlerMethodsList) { + Set> candidates = handlerMethods.keySet(); + Class match = null; + if (!CollectionUtils.isEmpty(candidates)) { + match = ClassUtils.findClosestMatch(payloadType, candidates, true); + } + if (match != null) { + return handlerMethods.get(match); + } } - return (match != null) ? this.handlerMethods.get(match) : null; + return null; } private static boolean isMethodDefinedOnObjectClass(Method method) { @@ -446,10 +541,13 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator private final Expression expression; - private volatile TypeDescriptor targetParameterType; - private final boolean canProcessMessageList; + private volatile TypeDescriptor targetParameterTypeDescriptor; + + private volatile Class targetParameterType = Void.class; + + private volatile boolean messageMethod; HandlerMethod(Method method, boolean canProcessMessageList) { this.method = method; @@ -462,10 +560,14 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator return this.expression; } - TypeDescriptor getTargetParameterType() { + Class getTargetParameterType() { return this.targetParameterType; } + private boolean isMessageMethod() { + return messageMethod; + } + @Override public String toString() { return this.method.toString(); @@ -476,13 +578,12 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator Class[] parameterTypes = method.getParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); boolean hasUnqualifiedMapParameter = false; - TypeDescriptor defaultParameterTypeDescriptor = TypeDescriptor.valueOf(List.class); for (int i = 0; i < parameterTypes.length; i++) { if (i != 0) { sb.append(", "); } - TypeDescriptor parameterTypeDescriptor = new TypeDescriptor(new MethodParameter(method, i)); - defaultParameterTypeDescriptor = parameterTypeDescriptor; + MethodParameter methodParameter = new MethodParameter(method, i); + TypeDescriptor parameterTypeDescriptor = new TypeDescriptor(methodParameter); Class parameterType = parameterTypeDescriptor.getObjectType(); Annotation mappingAnnotation = findMappingAnnotation(parameterAnnotations[i]); if (mappingAnnotation != null) { @@ -494,7 +595,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator sb.append("." + qualifierExpression); } if (!StringUtils.hasText(qualifierExpression)) { - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } } if (annotationType.equals(Payloads.class)) { @@ -505,7 +606,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } sb.append("]"); if (!StringUtils.hasText(qualifierExpression)) { - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } } else if (annotationType.equals(Headers.class)) { @@ -515,17 +616,18 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } else if (annotationType.equals(Header.class)) { Header headerAnnotation = (Header) mappingAnnotation; - sb.append(this.determineHeaderExpression(headerAnnotation, new MethodParameter(method, i))); + sb.append(this.determineHeaderExpression(headerAnnotation, methodParameter)); } } else if (parameterTypeDescriptor.isAssignableTo(messageTypeDescriptor)) { + this.messageMethod = true; sb.append("message"); - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } else if ((parameterTypeDescriptor.isAssignableTo(messageListTypeDescriptor) || parameterTypeDescriptor .isAssignableTo(messageArrayTypeDescriptor))) { sb.append("messages"); - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } else if (Collection.class.isAssignableFrom(parameterType) || parameterType.isArray()) { if (canProcessMessageList) { @@ -534,11 +636,11 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator else { sb.append("payload"); } - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } else if (Iterator.class.isAssignableFrom(parameterType)) { if (canProcessMessageList) { - Type type = method.getGenericParameterTypes()[0]; + Type type = method.getGenericParameterTypes()[i]; Type parameterizedType = null; if (type instanceof ParameterizedType){ parameterizedType = ((ParameterizedType)type).getActualTypeArguments()[0]; @@ -546,7 +648,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator parameterizedType = ((ParameterizedType) parameterizedType).getRawType(); } } - if (parameterizedType != null && Message.class.isAssignableFrom((Class)parameterizedType)){ + if (parameterizedType != null && Message.class.isAssignableFrom((Class) parameterizedType)){ sb.append("messages.iterator()"); } else { @@ -556,7 +658,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator else { sb.append("payload.iterator()"); } - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } else if (Map.class.isAssignableFrom(parameterType)) { if (Properties.class.isAssignableFrom(parameterType)) { @@ -573,19 +675,19 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } else { sb.append("payload"); - this.setExclusiveTargetParameterType(parameterTypeDescriptor); + this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter); } } if (hasUnqualifiedMapParameter) { - if (targetParameterType != null && Map.class.isAssignableFrom(this.targetParameterType.getObjectType())) { + if (targetParameterType != null && Map.class.isAssignableFrom(this.targetParameterType)) { throw new IllegalArgumentException( "Unable to determine payload matching parameter due to ambiguous Map typed parameters. " + "Consider adding the @Payload and or @Headers annotations as appropriate."); } } sb.append(")"); - if (this.targetParameterType == null) { - this.targetParameterType = defaultParameterTypeDescriptor; + if (this.targetParameterTypeDescriptor == null) { + this.targetParameterTypeDescriptor = TypeDescriptor.valueOf(Void.class); } return EXPRESSION_PARSER.parseExpression(sb.toString()); } @@ -638,15 +740,22 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : " + fallbackExpression; } - private synchronized void setExclusiveTargetParameterType(TypeDescriptor targetParameterType) { - Assert.isNull(this.targetParameterType, "Found more than one parameter type candidate: [" - + this.targetParameterType + "] and [" + targetParameterType + "]"); - this.targetParameterType = targetParameterType; + private synchronized void setExclusiveTargetParameterType(TypeDescriptor targetParameterType, MethodParameter methodParameter) { + Assert.isNull(this.targetParameterTypeDescriptor, "Found more than one parameter type candidate: [" + + this.targetParameterTypeDescriptor + "] and [" + targetParameterType + "]"); + this.targetParameterTypeDescriptor = targetParameterType; + if (Message.class.isAssignableFrom(targetParameterType.getObjectType())) { + methodParameter.increaseNestingLevel(); + this.targetParameterType = methodParameter.getNestedParameterType(); + methodParameter.decreaseNestingLevel(); + } + else { + this.targetParameterType = targetParameterType.getObjectType(); + } } } - @SuppressWarnings("unused") - private static class ParametersWrapper { + public class ParametersWrapper { private final Object payload; @@ -692,7 +801,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator if (payload != null) { return payload.getClass(); } - return Collection.class; + return this.messages.getClass(); } } diff --git a/spring-integration-core/src/main/resources/META-INF/spring.integration.default.properties b/spring-integration-core/src/main/resources/META-INF/spring.integration.default.properties new file mode 100644 index 0000000000..199b366130 --- /dev/null +++ b/spring-integration-core/src/main/resources/META-INF/spring.integration.default.properties @@ -0,0 +1,2 @@ +channels.autoCreate=true +messagingTemplate.lateReply.logging.level=warn 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 9343147740..e4b3b8f93c 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 @@ -1853,6 +1853,15 @@ + + + + Converts the 'replyChannel' and 'errorChannel' headers to a String after registering it in the HeaderChannelRegistry. + Use this when a message is serialized for any reason. No changes are made + if the header does not exist, or if the header does not currently reference a MessageChannel. + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandlerTests.java index af2bf76688..765e2d1592 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandlerTests.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,13 @@ package org.springframework.integration.aggregator; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import java.lang.reflect.Method; import java.util.ArrayList; @@ -30,6 +34,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.Test; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -57,6 +62,7 @@ public class AbstractCorrelatingMessageHandlerTests { AbstractCorrelatingMessageHandler handler = new AbstractCorrelatingMessageHandler( new MessageGroupProcessor() { + @Override public Object processMessageGroup(MessageGroup group) { return group; } @@ -73,6 +79,7 @@ public class AbstractCorrelatingMessageHandlerTests { */ Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override public void run() { try { waitReapStartLatch.await(10, TimeUnit.SECONDS); @@ -98,6 +105,7 @@ public class AbstractCorrelatingMessageHandlerTests { /* * Executes when group 'bar' completes normally */ + @Override public boolean send(Message message, long timeout) { outputMessages.add(message); // wake reaper @@ -115,12 +123,14 @@ public class AbstractCorrelatingMessageHandlerTests { return true; } + @Override public boolean send(Message message) { return this.send(message, 0); } }); handler.setReleaseStrategy(new ReleaseStrategy() { + @Override public boolean canRelease(MessageGroup group) { return group.size() == 2; } @@ -162,6 +172,7 @@ public class AbstractCorrelatingMessageHandlerTests { AggregatingMessageHandler handler = new AggregatingMessageHandler( new MessageGroupProcessor() { + @Override public Object processMessageGroup(MessageGroup group) { return group; } @@ -174,17 +185,20 @@ public class AbstractCorrelatingMessageHandlerTests { /* * Executes when group 'bar' completes normally */ + @Override public boolean send(Message message, long timeout) { outputMessages.add(message); return true; } + @Override public boolean send(Message message) { return this.send(message, 0); } }); handler.setReleaseStrategy(new ReleaseStrategy() { + @Override public boolean canRelease(MessageGroup group) { return group.size() == 1; } @@ -208,6 +222,7 @@ public class AbstractCorrelatingMessageHandlerTests { AggregatingMessageHandler handler = new AggregatingMessageHandler( new MessageGroupProcessor() { + @Override public Object processMessageGroup(MessageGroup group) { return group; } @@ -220,17 +235,20 @@ public class AbstractCorrelatingMessageHandlerTests { /* * Executes when group 'bar' completes normally */ + @Override public boolean send(Message message, long timeout) { outputMessages.add(message); return true; } + @Override public boolean send(Message message) { return this.send(message, 0); } }); handler.setReleaseStrategy(new ReleaseStrategy() { + @Override public boolean canRelease(MessageGroup group) { return group.size() == 1; } @@ -258,6 +276,7 @@ public class AbstractCorrelatingMessageHandlerTests { MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor(); AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp); handler.setReleaseStrategy(new ReleaseStrategy() { + @Override public boolean canRelease(MessageGroup group) { return true; } @@ -283,4 +302,101 @@ public class AbstractCorrelatingMessageHandlerTests { assertEquals(1, payload.size()); } + @Test /* INT-3216 */ + public void testDontReapIfAlreadyComplete() throws Exception { + MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor(); + AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp); + handler.setReleaseStrategy(new ReleaseStrategy() { + + @Override + public boolean canRelease(MessageGroup group) { + return true; + } + + }); + QueueChannel outputChannel = new QueueChannel(); + handler.setOutputChannel(outputChannel); + MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class); + mgs.addMessageToGroup("foo", new GenericMessage("foo")); + mgs.completeGroup("foo"); + mgs = spy(mgs); + new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs); + Method forceComplete = AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class); + forceComplete.setAccessible(true); + MessageGroup group = (MessageGroup) TestUtils.getPropertyValue(mgs, "groupIdToMessageGroup", Map.class).get("foo"); + assertTrue(group.isComplete()); + forceComplete.invoke(handler, group); + verify(mgs, never()).getMessageGroup("foo"); + assertNull(outputChannel.receive(0)); + } + + /* + * INT-3216 - Verifies the complete early exit is taken after a refresh. + */ + @Test + public void testDontReapIfAlreadyCompleteAfterRefetch() throws Exception { + MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor(); + AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp); + handler.setReleaseStrategy(new ReleaseStrategy() { + + @Override + public boolean canRelease(MessageGroup group) { + return true; + } + + }); + QueueChannel outputChannel = new QueueChannel(); + handler.setOutputChannel(outputChannel); + MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class); + mgs.addMessageToGroup("foo", new GenericMessage("foo")); + MessageGroup group = mgs.getMessageGroup("foo"); + mgs.completeGroup("foo"); + mgs = spy(mgs); + new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs); + Method forceComplete = AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class); + forceComplete.setAccessible(true); + MessageGroup groupInStore = (MessageGroup) TestUtils.getPropertyValue(mgs, "groupIdToMessageGroup", Map.class).get("foo"); + assertTrue(groupInStore.isComplete()); + assertFalse(group.isComplete()); + new DirectFieldAccessor(group).setPropertyValue("lastModified", groupInStore.getLastModified()); + forceComplete.invoke(handler, group); + verify(mgs).getMessageGroup("foo"); + assertNull(outputChannel.receive(0)); + } + + /* + * INT-3216 - Verifies we don't complete if it's a completely new group (different timestamp). + */ + @Test + public void testDontReapIfNewGroupFoundDuringRefetch() throws Exception { + MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor(); + AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp); + handler.setReleaseStrategy(new ReleaseStrategy() { + + @Override + public boolean canRelease(MessageGroup group) { + return true; + } + + }); + QueueChannel outputChannel = new QueueChannel(); + handler.setOutputChannel(outputChannel); + MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class); + mgs.addMessageToGroup("foo", new GenericMessage("foo")); + MessageGroup group = mgs.getMessageGroup("foo"); + mgs = spy(mgs); + new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs); + Method forceComplete = AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class); + forceComplete.setAccessible(true); + MessageGroup groupInStore = (MessageGroup) TestUtils.getPropertyValue(mgs, "groupIdToMessageGroup", Map.class).get("foo"); + assertFalse(groupInStore.isComplete()); + assertFalse(group.isComplete()); + DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(group); + directFieldAccessor.setPropertyValue("lastModified", groupInStore.getLastModified()); + directFieldAccessor.setPropertyValue("timestamp", groupInStore.getTimestamp() - 1); + forceComplete.invoke(handler, group); + verify(mgs).getMessageGroup("foo"); + assertNull(outputChannel.receive(0)); + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java index c1a94c0ace..53b0235b01 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java @@ -308,10 +308,9 @@ public class MethodInvokingMessageGroupProcessorTests { assertTrue(((Message)result).getPayload() instanceof Iterator); } - @Test(expected = IllegalArgumentException.class) + @Test public void testTwoMethodsWithSameParameterTypesAmbiguous() { - @SuppressWarnings("unused") class AnnotatedParametersAggregator { public Integer and(List flags) { int result = 0; @@ -327,7 +326,12 @@ public class MethodInvokingMessageGroupProcessorTests { } } - new MethodInvokingMessageGroupProcessor(new AnnotatedParametersAggregator()); + MessageGroupProcessor processor = new MethodInvokingMessageGroupProcessor(new AnnotatedParametersAggregator()); + when(messageGroupMock.getMessages()).thenReturn(messagesUpForProcessing); + Object result = processor.processMessageGroup(messageGroupMock); + Object payload = ((Message) result).getPayload(); + assertTrue(payload instanceof Integer); + assertEquals(7, payload); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/channel/registry/HeaderChannelRegistryTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/channel/registry/HeaderChannelRegistryTests-context.xml new file mode 100644 index 0000000000..5e073d435e --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/channel/registry/HeaderChannelRegistryTests-context.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/channel/registry/HeaderChannelRegistryTests.java b/spring-integration-core/src/test/java/org/springframework/integration/channel/registry/HeaderChannelRegistryTests.java new file mode 100644 index 0000000000..483ab37699 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/channel/registry/HeaderChannelRegistryTests.java @@ -0,0 +1,168 @@ +/* + * 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.channel.registry; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.channel.DefaultHeaderChannelRegistry; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.channel.MessagePublishingErrorHandler; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.core.GenericMessagingTemplate; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class HeaderChannelRegistryTests { + + @Autowired + MessageChannel input; + + @Autowired + MessageChannel inputPolled; + + @Autowired + QueueChannel alreadyAString; + + @Autowired + TaskScheduler taskScheduler; + + @Autowired + Gateway gatewayNoReplyChannel; + + @Autowired + Gateway gatewayExplicitReplyChannel; + + @Test + public void testReplace() { + GenericMessagingTemplate template = new GenericMessagingTemplate(); + template.setDefaultDestination(this.input); + Message reply = template.sendAndReceive(new GenericMessage("foo")); + assertNotNull(reply); + assertEquals("echo:foo", reply.getPayload()); + } + + @Test + public void testReplaceGatewayWithNoReplyChannel() { + String reply = this.gatewayNoReplyChannel.exchange("foo"); + assertNotNull(reply); + assertEquals("echo:foo", reply); + } + + @Test + public void testReplaceGatewayWithExplicitReplyChannel() { + String reply = this.gatewayExplicitReplyChannel.exchange("foo"); + assertNotNull(reply); + assertEquals("echo:foo", reply); + } + + /** + * MessagingTemplate sets the errorChannel to the replyChannel so it gets any async + * exceptions via the default {@link MessagePublishingErrorHandler}. + */ + @Test + public void testReplaceError() { + GenericMessagingTemplate template = new GenericMessagingTemplate(); + template.setDefaultDestination(this.inputPolled); + Message reply = template.sendAndReceive(new GenericMessage("bar")); + assertNotNull(reply); + assertTrue(reply instanceof ErrorMessage); + } + + @Test + public void testAlreadyAString() { + Message requestMessage = MessageBuilder.withPayload("foo") + .setReplyChannelName("alreadyAString") + .setErrorChannelName("alreadyAnotherString") + .build(); + this.input.send(requestMessage); + Message reply = alreadyAString.receive(0); + assertNotNull(reply); + assertEquals("echo:foo", reply.getPayload()); + } + + @Test + public void testNull() { + Message requestMessage = MessageBuilder.withPayload("foo") + .build(); + try { + this.input.send(requestMessage); + fail("expected exception"); + } + catch (Exception e) { + assertThat(e.getMessage(), Matchers.containsString("no output-channel or replyChannel")); + } + } + + @Test + public void testExpire() throws Exception { + DefaultHeaderChannelRegistry registry = new DefaultHeaderChannelRegistry(50); + registry.setTaskScheduler(this.taskScheduler); + registry.start(); + Thread.sleep(200); + String id = (String) registry.channelToChannelName(new DirectChannel()); + Thread.sleep(300); + assertNull(registry.channelNameToChannel(id)); + registry.stop(); + } + + public static class Foo extends AbstractReplyProducingMessageHandler { + + @Override + protected Object handleRequestMessage(Message requestMessage) { + assertThat(requestMessage.getHeaders().getReplyChannel(), + Matchers.anyOf(instanceOf(String.class), Matchers.nullValue())); + assertThat(requestMessage.getHeaders().getErrorChannel(), + Matchers.anyOf(instanceOf(String.class), Matchers.nullValue())); + if (requestMessage.getPayload().equals("bar")) { + throw new RuntimeException("intentional"); + } + return "echo:" + requestMessage.getPayload(); + } + + } + + public interface Gateway { + + String exchange(String foo); + + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java index 8b1b469baa..b106e54315 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java @@ -19,18 +19,19 @@ package org.springframework.integration.config; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; +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 java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import org.junit.Assert; import org.junit.Before; import org.junit.Test; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; @@ -121,14 +122,14 @@ public class AggregatorParserTests { Object consumer = new DirectFieldAccessor(endpoint).getPropertyValue("handler"); assertThat(consumer, is(instanceOf(AggregatingMessageHandler.class))); DirectFieldAccessor accessor = new DirectFieldAccessor(consumer); - Map map = (Map) new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(accessor + Object handlerMethods = new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(accessor .getPropertyValue("outputProcessor")).getPropertyValue("processor")).getPropertyValue("delegate")) .getPropertyValue("handlerMethods"); - assertEquals("The MethodInvokingAggregator is not injected with the appropriate aggregation method", 1, map - .size()); - assertEquals("The release strategy is not injected with the appropriate method", 1, map.size()); - assertTrue("Handler methods do not contain correct method: " + map, map.toString().contains( - "createSingleMessageFromGroup")); + assertNull(handlerMethods); + Object handlerMethod = new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(accessor + .getPropertyValue("outputProcessor")).getPropertyValue("processor")).getPropertyValue("delegate")) + .getPropertyValue("handlerMethod"); + assertTrue(handlerMethod.toString().contains("createSingleMessageFromGroup")); assertEquals("The AggregatorEndpoint is not injected with the appropriate ReleaseStrategy instance", releaseStrategy, accessor.getPropertyValue("releaseStrategy")); assertEquals("The AggregatorEndpoint is not injected with the appropriate CorrelationStrategy instance", @@ -178,10 +179,10 @@ public class AggregatorParserTests { Assert.assertTrue(releaseStrategy instanceof MethodInvokingReleaseStrategy); DirectFieldAccessor releaseStrategyAccessor = new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(releaseStrategy) .getPropertyValue("adapter")).getPropertyValue("delegate")); - Map map = (Map) releaseStrategyAccessor.getPropertyValue("handlerMethods"); - assertEquals("The release strategy is not injected with the appropriate method", 1, map.size()); - assertTrue("Handler methods do not contain correct method: " + map, map.toString() - .contains("checkCompleteness")); + Object handlerMethods = releaseStrategyAccessor.getPropertyValue("handlerMethods"); + assertNull(handlerMethods); + Object handlerMethod = releaseStrategyAccessor.getPropertyValue("handlerMethod"); + assertTrue(handlerMethod.toString().contains("checkCompleteness")); input.send(createMessage(1l, "correllationId", 4, 0, null)); input.send(createMessage(2l, "correllationId", 4, 1, null)); input.send(createMessage(3l, "correllationId", 4, 2, null)); @@ -203,10 +204,10 @@ public class AggregatorParserTests { Assert.assertTrue(releaseStrategy instanceof MethodInvokingReleaseStrategy); DirectFieldAccessor releaseStrategyAccessor = new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(releaseStrategy) .getPropertyValue("adapter")).getPropertyValue("delegate")); - Map map = (Map) releaseStrategyAccessor.getPropertyValue("handlerMethods"); - assertEquals("The release strategy is not injected with the appropriate method", 1, map.size()); - assertTrue("Handler methods do not contain correct method: " + map, map.toString() - .contains("checkCompleteness")); + Object handlerMethods = releaseStrategyAccessor.getPropertyValue("handlerMethods"); + assertNull(handlerMethods); + Object handlerMethod = releaseStrategyAccessor.getPropertyValue("handlerMethod"); + assertTrue(handlerMethod.toString().contains("checkCompleteness")); input.send(createMessage(1l, "correllationId", 4, 0, null)); input.send(createMessage(2l, "correllationId", 4, 1, null)); input.send(createMessage(3l, "correllationId", 4, 2, null)); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml index c7da41a0c6..c079fdca07 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml @@ -40,7 +40,7 @@ - + @@ -49,7 +49,7 @@ - + - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java index 5ef3a7db3d..f1c3269ea4 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java @@ -172,7 +172,7 @@ public class ChainParserTests { public void chainWithAcceptingFilter() { Message message = MessageBuilder.withPayload("test").build(); this.filterInput.send(message); - Message reply = this.output.receive(0); + Message reply = this.output.receive(1000); assertNotNull(reply); assertEquals("foo", reply.getPayload()); } @@ -189,7 +189,7 @@ public class ChainParserTests { public void chainWithHeaderEnricher() { Message message = MessageBuilder.withPayload(123).build(); this.headerEnricherInput.send(message); - Message reply = this.replyOutput.receive(0); + Message reply = this.replyOutput.receive(1000); assertNotNull(reply); assertEquals("foo", reply.getPayload()); assertEquals("ABC", new EiMessageHeaderAccessor(reply).getCorrelationId()); @@ -240,8 +240,8 @@ public class ChainParserTests { Message message2 = MessageBuilder.withPayload(123).build(); this.payloadTypeRouterInput.send(message1); this.payloadTypeRouterInput.send(message2); - Message reply1 = this.strings.receive(0); - Message reply2 = this.numbers.receive(0); + Message reply1 = this.strings.receive(1000); + Message reply2 = this.numbers.receive(1000); assertNotNull(reply1); assertNotNull(reply2); assertEquals("test", reply1.getPayload()); @@ -254,8 +254,8 @@ public class ChainParserTests { Message message2 = MessageBuilder.withPayload(123).setHeader("routingHeader", "numbers").build(); this.headerValueRouterInput.send(message1); this.headerValueRouterInput.send(message2); - Message reply1 = this.strings.receive(0); - Message reply2 = this.numbers.receive(0); + Message reply1 = this.strings.receive(1000); + Message reply2 = this.numbers.receive(1000); assertNotNull(reply1); assertNotNull(reply2); assertEquals("test", reply1.getPayload()); @@ -302,6 +302,7 @@ public class ChainParserTests { final AtomicReference log = new AtomicReference(); when(logger.isWarnEnabled()).thenReturn(true); doAnswer(new Answer() { + @Override public Object answer(InvocationOnMock invocation) throws Throwable { log.set((String) invocation.getArguments()[0]); return null; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java index 490ed89f63..5baa25f5c0 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.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. @@ -17,16 +17,17 @@ package org.springframework.integration.config.annotation; 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.springframework.integration.test.util.TestUtils.getPropertyValue; import java.lang.reflect.Method; -import java.util.Map; import org.junit.Assert; import org.junit.Test; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -45,6 +46,7 @@ import org.springframework.messaging.core.DestinationResolver; /** * @author Marius Bogoevici * @author Mark Fisher + * @author Artem Bilan */ public class AggregatorAnnotationTests { @@ -86,13 +88,12 @@ public class AggregatorAnnotationTests { Object releaseStrategy = getPropertyValue(aggregator, "releaseStrategy"); Assert.assertTrue(releaseStrategy instanceof MethodInvokingReleaseStrategy); MethodInvokingReleaseStrategy releaseStrategyAdapter = (MethodInvokingReleaseStrategy) releaseStrategy; - Map map = (Map) new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(releaseStrategyAdapter) + Object handlerMethods = new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(releaseStrategyAdapter) .getPropertyValue("adapter")).getPropertyValue("delegate")).getPropertyValue("handlerMethods"); - assertEquals("The MethodInvokingAggregator is not injected with the appropriate aggregation method", 1, map - .size()); - assertEquals("The release strategy is not injected with the appropriate method", 1, map.size()); - assertTrue("Handler methods do not contain correct method: " + map, map.toString() - .contains("completionChecker")); + assertNull(handlerMethods); + Object handlerMethod = new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(releaseStrategyAdapter) + .getPropertyValue("adapter")).getPropertyValue("delegate")).getPropertyValue("handlerMethod"); + assertTrue(handlerMethod.toString().contains("completionChecker")); } @Test @@ -108,9 +109,10 @@ public class AggregatorAnnotationTests { .getPropertyValue("processor")).getPropertyValue("delegate")); Object targetObject = processorAccessor.getPropertyValue("targetObject"); assertSame(context.getBean(endpointName), targetObject); - Map handlerMethods = (Map) processorAccessor.getPropertyValue("handlerMethods"); - assertEquals(1, handlerMethods.size()); - DirectFieldAccessor handlerMethodAccessor = new DirectFieldAccessor(handlerMethods.values().iterator().next()); + assertNull(processorAccessor.getPropertyValue("handlerMethods")); + Object handlerMethod = processorAccessor.getPropertyValue("handlerMethod"); + assertNotNull(handlerMethod); + DirectFieldAccessor handlerMethodAccessor = new DirectFieldAccessor(handlerMethod); Method completionCheckerMethod = (Method) handlerMethodAccessor.getPropertyValue("method"); assertEquals("correlate", completionCheckerMethod.getName()); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java index 8d61adb41b..37c24d202c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java @@ -16,8 +16,8 @@ package org.springframework.integration.config.xml; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.util.Date; @@ -28,12 +28,15 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.channel.DefaultHeaderChannelRegistry; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.core.GenericMessagingTemplate; import org.springframework.messaging.support.GenericMessage; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -52,6 +55,9 @@ public class ControlBusTests { @Autowired private PollableChannel output; + @Autowired + private DefaultHeaderChannelRegistry registry; + @Test public void testDefaultEvaluationContext() { Message message = MessageBuilder.withPayload("@service.convert('aardvark')+headers.foo").setHeader("foo", "bar").build(); @@ -71,6 +77,27 @@ public class ControlBusTests { assertNotNull(outputChannel.receive(1000)); } + @Test + public void testControlHeaderChannelReaper() throws InterruptedException { + GenericMessagingTemplate messagingTemplate = new GenericMessagingTemplate(); + messagingTemplate.convertAndSend(input, "@integrationHeaderChannelRegistry.size()"); + Message result = this.output.receive(0); + assertNotNull(result); + assertEquals(0, result.getPayload()); + this.registry.setReaperDelay(10); + this.registry.channelToChannelName(new DirectChannel()); + messagingTemplate.convertAndSend(input, "@integrationHeaderChannelRegistry.size()"); + result = this.output.receive(0); + assertNotNull(result); + assertEquals(1, result.getPayload()); + Thread.sleep(100); + messagingTemplate.convertAndSend(input, "@integrationHeaderChannelRegistry.runReaper()"); + messagingTemplate.convertAndSend(input, "@integrationHeaderChannelRegistry.size()"); + result = this.output.receive(0); + assertNotNull(result); + assertEquals(0, result.getPayload()); + this.registry.setReaperDelay(60000); + } public static class Service { @@ -78,12 +105,14 @@ public class ControlBusTests { public String convert(String input) { return "cat"; } + } public static class AdapterService { public Message receive() { return new GenericMessage(new Date().toString()); } + } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml index b0c558e552..f62be84be9 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml @@ -13,11 +13,17 @@ + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java index 91753f66f6..782bddbc08 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java @@ -17,6 +17,7 @@ package org.springframework.integration.config.xml; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; @@ -79,6 +80,7 @@ public class EnricherParserTests { assertEquals(context.getBean("output"), accessor.getPropertyValue("outputChannel")); assertEquals(true, accessor.getPropertyValue("shouldClonePayload")); assertNull(accessor.getPropertyValue("requestPayloadExpression")); + assertNotNull(TestUtils.getPropertyValue(enricher, "gateway.beanFactory")); Map propertyExpressions = (Map) accessor.getPropertyValue("propertyExpressions"); for (Map.Entry e : propertyExpressions.entrySet()) { @@ -125,12 +127,15 @@ public class EnricherParserTests { @Test public void integrationTest() { SubscribableChannel requests = context.getBean("requests", SubscribableChannel.class); - requests.subscribe(new AbstractReplyProducingMessageHandler() { + class Foo extends AbstractReplyProducingMessageHandler { @Override protected Object handleRequestMessage(Message requestMessage) { return new Source("foo"); } - }); + }; + Foo foo = new Foo(); + foo.setOutputChannel(context.getBean("replies", MessageChannel.class)); + requests.subscribe(foo); Target original = new Target(); Message request = MessageBuilder.withPayload(original) .setHeader("sourceName", "test") diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-after-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-after-context.xml index d96e063f15..b177646005 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-after-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-after-context.xml @@ -7,7 +7,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-before-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-before-context.xml index 5ce379cb63..ece1f92d91 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-before-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/TestEnableChannelAutoCreation-before-context.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd"> - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests-context.xml new file mode 100644 index 0000000000..4bbd23523b --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests-context.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests.java b/spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests.java new file mode 100644 index 0000000000..64d9ee572e --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests.java @@ -0,0 +1,54 @@ +/* + * 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.context; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +import java.util.Properties; + +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.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Artem Bilan + * @since 3.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class IntegrationContextTests { + + @Autowired + @Qualifier(IntegrationContextUtils.INTEGRATION_PROPERTIES_BEAN_NAME) + private Properties integrationProperties; + + @Autowired + @Qualifier("fooService") + private IntegrationObjectSupport serviceActivator; + + @Test + public void testIntegrationContextComponents() { + assertEquals("error", this.integrationProperties.get(IntegrationProperties.LATE_REPLY_LOGGING_LEVEL)); + assertSame(this.integrationProperties, this.serviceActivator.getIntegrationProperties()); + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests-context.xml index 1feb6b2cb0..7f873d3914 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests-context.xml @@ -13,7 +13,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java index 8247371486..b058d39f76 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java @@ -17,13 +17,19 @@ package org.springframework.integration.endpoint; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; + +import java.util.Date; import org.junit.Test; -import org.springframework.messaging.Message; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.gateway.RequestReplyExchanger; import org.springframework.integration.handler.ServiceActivatingHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; /** @@ -66,6 +72,166 @@ public class ServiceActivatorMethodResolutionTests { } + @Test + public void testRequestReplyExchanger() { + RequestReplyExchanger testBean = new RequestReplyExchanger() { + + @Override + public Message exchange(Message request) { + return request; + } + }; + + final Message test = new GenericMessage("foo"); + + ServiceActivatingHandler serviceActivator = new ServiceActivatingHandler(testBean) { + + @Override + protected Object handleRequestMessage(Message message) { + Object o = super.handleRequestMessage(message); + assertSame(test, o); + return null; + } + }; + + serviceActivator.handleMessage(test); + } + + @Test + /* + * A handler and message handler fallback (RRE); don't force RRE + */ + public void testRequestReplyExchangerSeveralMethods() { + RequestReplyExchanger testBean = new RequestReplyExchanger() { + + @Override + public Message exchange(Message request) { + return request; + } + + @SuppressWarnings("unused") + public String foo(String request) { + return request.toUpperCase(); + } + + }; + ServiceActivatingHandler serviceActivator = new ServiceActivatingHandler(testBean); + PollableChannel outputChannel = new QueueChannel(); + serviceActivator.setOutputChannel(outputChannel); + + Message test = new GenericMessage(new Date()); + serviceActivator.handleMessage(test); + assertEquals(test, outputChannel.receive(10)); + + test = new GenericMessage("foo"); + serviceActivator.handleMessage(test); + assertEquals("FOO", outputChannel.receive(10).getPayload()); + } + + @Test + /* + * No handler fallback methods; don't force RRE + */ + public void testRequestReplyExchangerWithGenericMessageMethod() { + RequestReplyExchanger testBean = new RequestReplyExchanger() { + + @Override + public Message exchange(Message request) { + return request; + } + + @SuppressWarnings("unused") + public String foo(Message request) { + return request.getPayload().toUpperCase(); + } + + }; + ServiceActivatingHandler serviceActivator = new ServiceActivatingHandler(testBean); + PollableChannel outputChannel = new QueueChannel(); + serviceActivator.setOutputChannel(outputChannel); + + Message test = new GenericMessage(new Date()); + serviceActivator.handleMessage(test); + assertEquals(test, outputChannel.receive(10)); + + test = new GenericMessage("foo"); + serviceActivator.handleMessage(test); + assertEquals("FOO", outputChannel.receive(10).getPayload()); + } + + @Test + /* + * No handler fallback methods; ambiguous message handler fallbacks; force RRE + */ + public void testRequestReplyExchangerWithAmbiguousGenericMessageMethod() { + RequestReplyExchanger testBean = new RequestReplyExchanger() { + + @Override + public Message exchange(Message request) { + return request; + } + + @SuppressWarnings("unused") + public String foo(Message request) { + return request.getPayload().toUpperCase(); + } + + @SuppressWarnings("unused") + public String bar(Message request) { + return request.getPayload().toUpperCase(); + } + + }; + ServiceActivatingHandler serviceActivator = new ServiceActivatingHandler(testBean); + PollableChannel outputChannel = new QueueChannel(); + serviceActivator.setOutputChannel(outputChannel); + + Message test = new GenericMessage(new Date()); + serviceActivator.handleMessage(test); + assertEquals(test, outputChannel.receive(10)); + + test = new GenericMessage("foo"); + serviceActivator.handleMessage(test); + assertNotEquals("FOO", outputChannel.receive(10).getPayload()); + } + + @Test + /* + * One message handler fallback method (RRE); ambiguous handler fallbacks; force RRE + */ + public void testRequestReplyExchangerWithAmbiguousMethod() { + RequestReplyExchanger testBean = new RequestReplyExchanger() { + + @Override + public Message exchange(Message request) { + return request; + } + + @SuppressWarnings("unused") + public String foo(String request) { + return request.toUpperCase(); + } + + @SuppressWarnings("unused") + public String bar(String request) { + return request.toUpperCase(); + } + + }; + ServiceActivatingHandler serviceActivator = new ServiceActivatingHandler(testBean); + PollableChannel outputChannel = new QueueChannel(); + serviceActivator.setOutputChannel(outputChannel); + + Message test = new GenericMessage(new Date()); + serviceActivator.handleMessage(test); + assertEquals(test, outputChannel.receive(10)); + + test = new GenericMessage("foo"); + serviceActivator.handleMessage(test); + assertNotEquals("FOO", outputChannel.receive(10).getPayload()); + } + + @SuppressWarnings("unused") private static class SingleAnnotationTestBean { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java index bd4961ec47..4c17cfb11a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java @@ -20,18 +20,23 @@ 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.assertThat; +import static org.junit.Assert.fail; import java.lang.reflect.Method; +import java.util.Date; import java.util.Map; import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hamcrest.Description; +import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.messaging.Message; import org.springframework.integration.MessageHandlingException; @@ -355,6 +360,135 @@ public class MethodInvokingMessageProcessorTests { assertSame(RequestReplyExchanger.class, result); } + @Test + public void testInt3199GenericTypeResolvingAndObjectMethod() throws Exception { + + class Foo { + + public String handleMessage(Message message) { + return "" + (message.getPayload().intValue() * 2); + } + + public String objectMethod(Integer foo) { + return foo.toString(); + } + + public String voidMethod() { + return "foo"; + } + + } + + MessagingMethodInvokerHelper helper = new MessagingMethodInvokerHelper(new Foo(), (String) null, false); + assertEquals("4", helper.process(new GenericMessage(2L))); + assertEquals("1", helper.process(new GenericMessage(1))); + assertEquals("foo", helper.process(new GenericMessage(new Date()))); + } + + @Test + public void testInt3199GettersAmbiguity() throws Exception { + + class Foo { + + public String getFoo() { + return "foo"; + } + + public String getBar() { + return "foo"; + } + } + + try { + new MessagingMethodInvokerHelper(new Foo(), (String) null, false); + fail("IllegalArgumentException expected"); + } + catch (Exception e) { + assertThat(e, Matchers.instanceOf(IllegalArgumentException.class)); + assertEquals("Found more than one method match for empty parameter for 'payload'", e.getMessage()); + } + } + + @Test + public void testInt3199MessageMethods() throws Exception { + + class Foo { + + public String m1(Message message) { + return message.getPayload(); + } + + public Integer m2(Message message) { + return message.getPayload(); + } + + public Object m3(Message message) { + return message.getPayload(); + } + + } + + Foo targetObject = new Foo(); + + MessagingMethodInvokerHelper helper = new MessagingMethodInvokerHelper(targetObject, (String) null, false); + assertEquals("foo", helper.process(new GenericMessage("foo"))); + assertEquals(1, helper.process(new GenericMessage(1))); + assertEquals(targetObject, helper.process(new GenericMessage(targetObject))); + } + + @Test + public void testInt3199TypedMethods() throws Exception { + + class Foo { + + public String m1(String payload) { + return payload; + } + + public Integer m2(Integer payload) { + return payload; + } + + public Object m3(Object payload) { + return payload; + } + + } + + Foo targetObject = new Foo(); + + MessagingMethodInvokerHelper helper = new MessagingMethodInvokerHelper(targetObject, (String) null, false); + assertEquals("foo", helper.process(new GenericMessage("foo"))); + assertEquals(1, helper.process(new GenericMessage(1))); + assertEquals(targetObject, helper.process(new GenericMessage(targetObject))); + } + + @Test + public void testInt3199PrecedenceOfCandidates() throws Exception { + + class Foo { + + public Object m1(Message message) { + fail("This method must not be invoked"); + return message; + } + + public Object m2(String payload) { + return payload; + } + + public Object m3() { + return "FOO"; + } + } + + Foo targetObject = new Foo(); + + MessagingMethodInvokerHelper helper = new MessagingMethodInvokerHelper(targetObject, (String) null, false); + assertEquals("foo", helper.process(new GenericMessage("foo"))); + assertEquals("FOO", helper.process(new GenericMessage(targetObject))); + } + private static class ExceptionCauseMatcher extends TypeSafeMatcher { private Throwable cause; @@ -528,5 +662,7 @@ public class MethodInvokingMessageProcessorTests { this.lastArg = s; return s; } + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java index 9e10b40125..85bcddc94e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java @@ -53,6 +53,7 @@ import com.jayway.jsonpath.Filter; /** * @author Artem Bilan + * @author Gary Russell * @since 3.0 */ @ContextConfiguration(classes = JsonPathTests.JsonPathTestsContextConfiguration.class, loader = AnnotationConfigContextLoader.class) @@ -69,7 +70,9 @@ public class JsonPathTests { public static void setUp() throws IOException { ClassPathResource jsonResource = new ClassPathResource("JsonPathTests.json", JsonPathTests.class); JSON_FILE = jsonResource.getFile(); - JSON = new Scanner(JSON_FILE).useDelimiter("\\Z").next(); + Scanner scanner = new Scanner(JSON_FILE); + JSON = scanner.useDelimiter("\\Z").next(); + scanner.close(); testMessage = new GenericMessage(JSON); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/message/PayloadAndHeaderMappingTests.java b/spring-integration-core/src/test/java/org/springframework/integration/message/PayloadAndHeaderMappingTests.java index f358481518..792ecb43c9 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/message/PayloadAndHeaderMappingTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/message/PayloadAndHeaderMappingTests.java @@ -749,14 +749,14 @@ public class PayloadAndHeaderMappingTests { this.lastHeaders.put("foo", header); this.lastPayload = payload; } - + public void payloadMapAndHeaderStrings(Map payload, @Header("foo") String header1, @Header("bar") String header2) { this.lastHeaders = new HashMap(); this.lastHeaders.put("foo", header1); this.lastHeaders.put("bar", header2); - this.lastPayload = payload; + this.lastPayload = payload; } - + public void payloadMapAndHeaderMap(Map payload, @Headers Map headers) { this.lastHeaders = headers; this.lastPayload = payload; @@ -771,7 +771,7 @@ public class PayloadAndHeaderMappingTests { this.lastHeaders = headers; this.lastPayload = payload; } - + public void headerPropertiesPayloadMapAndStringHeader(@Headers Properties headers, Map payload, @Header("foo") String header) { this.lastHeaders = headers; this.lastHeaders.put("foo2", header); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java index 560dd358b2..a0988e35e1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java @@ -171,7 +171,7 @@ public class SpelTransformerIntegrationTests { @Override public Class[] getSpecificTargetClasses() { - return new Class[] {Foo.class}; + return new Class[] {Foo.class}; } @Override diff --git a/spring-integration-core/src/test/resources/META-INF/spring.integration.properties b/spring-integration-core/src/test/resources/META-INF/spring.integration.properties new file mode 100644 index 0000000000..38ffd39c66 --- /dev/null +++ b/spring-integration-core/src/test/resources/META-INF/spring.integration.properties @@ -0,0 +1,2 @@ +#channels.autoCreate=false +messagingTemplate.lateReply.logging.level=error diff --git a/spring-integration-event/src/main/java/org/springframework/integration/event/inbound/ApplicationEventListeningMessageProducer.java b/spring-integration-event/src/main/java/org/springframework/integration/event/inbound/ApplicationEventListeningMessageProducer.java index 48a2076676..77dbac0d28 100644 --- a/spring-integration-event/src/main/java/org/springframework/integration/event/inbound/ApplicationEventListeningMessageProducer.java +++ b/spring-integration-event/src/main/java/org/springframework/integration/event/inbound/ApplicationEventListeningMessageProducer.java @@ -21,8 +21,9 @@ import java.util.Set; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ApplicationContextEvent; import org.springframework.context.event.ApplicationEventMulticaster; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextStoppedEvent; import org.springframework.context.event.SmartApplicationListener; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.Ordered; @@ -40,6 +41,8 @@ import org.springframework.util.CollectionUtils; * * @author Mark Fisher * @author Artem Bilan + * @author Gary Russell + * * @see ApplicationEventMulticaster * @see ExpressionMessageProducerSupport */ @@ -51,6 +54,10 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP private volatile boolean active; + private volatile long stoppedAt; + + private volatile boolean phaseSet; + /** * Set the list of event types (classes that extend ApplicationEvent) that * this adapter should send to the message channel. By default, all event @@ -73,6 +80,12 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP } } + @Override + public void setPhase(int phase) { + super.setPhase(phase); + this.phaseSet = true; + } + @Override public String getComponentType() { return "event:inbound-channel-adapter"; @@ -85,10 +98,15 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP .getBean(AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); Assert.notNull(this.applicationEventMulticaster, "To use ApplicationListeners the 'applicationEventMulticaster' bean must be supplied within ApplicationContext."); + if (!this.phaseSet) { + super.setPhase(Integer.MIN_VALUE + 1000); + } } + @Override public void onApplicationEvent(ApplicationEvent event) { - if (this.active || event instanceof ApplicationContextEvent) { + if (this.active || ((event instanceof ContextStoppedEvent || event instanceof ContextClosedEvent) + && this.stoppedRecently())) { if (event.getSource() instanceof Message) { this.sendMessage((Message) event.getSource()); } @@ -99,6 +117,11 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP } } + private boolean stoppedRecently() { + return this.stoppedAt > System.currentTimeMillis() - 5000; + } + + @Override public boolean supportsEventType(Class eventType) { if (this.eventTypes == null) { return true; @@ -111,10 +134,12 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP return false; } + @Override public boolean supportsSourceType(Class sourceType) { return true; } + @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } @@ -126,6 +151,7 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP @Override protected void doStop() { + this.stoppedAt = System.currentTimeMillis(); this.active = false; } diff --git a/spring-integration-event/src/test/java/org/springframework/integration/event/config/EventOutboundChannelAdapterParserTests.java b/spring-integration-event/src/test/java/org/springframework/integration/event/config/EventOutboundChannelAdapterParserTests.java index dc72323a54..7532d31860 100644 --- a/spring-integration-event/src/test/java/org/springframework/integration/event/config/EventOutboundChannelAdapterParserTests.java +++ b/spring-integration-event/src/test/java/org/springframework/integration/event/config/EventOutboundChannelAdapterParserTests.java @@ -20,9 +20,9 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import org.junit.Assert; - import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEvent; @@ -71,6 +71,7 @@ public class EventOutboundChannelAdapterParserTests { @Test public void validateUsage() { ApplicationListener listener = new ApplicationListener() { + @Override public void onApplicationEvent(ApplicationEvent event) { Object source = event.getSource(); if (source instanceof Message){ @@ -91,6 +92,7 @@ public class EventOutboundChannelAdapterParserTests { public void withAdvice() { receivedEvent = false; ApplicationListener listener = new ApplicationListener() { + @Override public void onApplicationEvent(ApplicationEvent event) { Object source = event.getSource(); if (source instanceof Message){ @@ -112,6 +114,7 @@ public class EventOutboundChannelAdapterParserTests { public void testInsideChain() { receivedEvent = false; ApplicationListener listener = new ApplicationListener() { + @Override public void onApplicationEvent(ApplicationEvent event) { Object source = event.getSource(); if (source instanceof Message){ @@ -128,12 +131,13 @@ public class EventOutboundChannelAdapterParserTests { Assert.assertTrue(receivedEvent); } - @Test(timeout=2000) + @Test(timeout=10000) public void validateUsageWithPollableChannel() throws Exception { receivedEvent = false; ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("EventOutboundChannelAdapterParserTestsWithPollable-context.xml", EventOutboundChannelAdapterParserTests.class); final CyclicBarrier barier = new CyclicBarrier(2); ApplicationListener listener = new ApplicationListener() { + @Override public void onApplicationEvent(ApplicationEvent event) { Object source = event.getSource(); if (source instanceof Message){ diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/DefaultFileNameGenerator.java b/spring-integration-file/src/main/java/org/springframework/integration/file/DefaultFileNameGenerator.java index 487117ef2a..d839ce0ead 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/DefaultFileNameGenerator.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/DefaultFileNameGenerator.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. @@ -18,8 +18,11 @@ package org.springframework.integration.file; import java.io.File; -import org.springframework.messaging.Message; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.util.AbstractExpressionEvaluator; +import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -34,13 +37,17 @@ import org.springframework.util.StringUtils; * associated with the header if no expression has been provided), it checks if * the Message payload is a File instance, and if so, it uses the same name. * Finally, it falls back to the Message ID and adds the suffix '.msg'. - * + * * @author Mark Fisher + * @author Gary Russell */ public class DefaultFileNameGenerator extends AbstractExpressionEvaluator implements FileNameGenerator { - private volatile String expression = "headers['" + FileHeaders.FILENAME + "']"; + private static final String DEFAULT_EXPRESSION = "headers['" + FileHeaders.FILENAME + "']"; + private final static ExpressionParser parser = new SpelExpressionParser(); + + private volatile Expression expression = parser.parseExpression(DEFAULT_EXPRESSION); /** * Specify an expression to be evaluated against the Message @@ -48,7 +55,7 @@ public class DefaultFileNameGenerator extends AbstractExpressionEvaluator implem */ public void setExpression(String expression) { Assert.hasText(expression, "expression must not be empty"); - this.expression = expression; + this.expression = parser.parseExpression(expression); } /** @@ -57,9 +64,10 @@ public class DefaultFileNameGenerator extends AbstractExpressionEvaluator implem */ public void setHeaderName(String headerName) { Assert.notNull(headerName, "'headerName' must not be null"); - this.expression = "headers['" + headerName + "']"; + this.expression = parser.parseExpression("headers['" + headerName + "']"); } + @Override public String generateFileName(Message message) { Object filenameProperty = this.evaluateExpression(this.expression, message); if (filenameProperty instanceof String && StringUtils.hasText((String) filenameProperty)) { diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java index 8fc19e0bc2..2e90df8dd1 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java @@ -23,6 +23,8 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.ExpressionFactoryBean; import org.springframework.integration.config.xml.AbstractConsumerEndpointParser; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.file.filters.RegexPatternFileListFilter; +import org.springframework.integration.file.filters.SimplePatternFileListFilter; import org.springframework.util.StringUtils; /** @@ -42,18 +44,20 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo @Override protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) { + + BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, false); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(getGatewayClassName()); - builder.addConstructorArgReference(element.getAttribute("session-factory")); + builder.addConstructorArgValue(templateDefinition); builder.addConstructorArgValue(element.getAttribute("command")); builder.addConstructorArgValue(element.getAttribute(EXPRESSION_ATTRIBUTE)); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "command-options", "options"); - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "temporary-file-suffix"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout", "sendTimeout"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel"); - this.configureFilter(builder, element, parserContext); - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "remote-file-separator"); + this.configureFilter(builder, element, parserContext, "filter", "filename", "filter"); + this.configureFilter(builder, element, parserContext, "mput-filter", "mput", "mputFilter"); BeanDefinition localDirExpressionDef = IntegrationNamespaceUtils .createExpressionDefinitionFromValueOrExpression("local-directory", "local-directory-expression", @@ -74,10 +78,11 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo return builder; } - protected void configureFilter(BeanDefinitionBuilder builder, Element element, ParserContext parserContext) { - String filter = element.getAttribute("filter"); - String fileNamePattern = element.getAttribute("filename-pattern"); - String fileNameRegex = element.getAttribute("filename-regex"); + protected void configureFilter(BeanDefinitionBuilder builder, Element element, ParserContext parserContext, + String filterAttribute, String patternPrefix, String propertyName) { + String filter = element.getAttribute(filterAttribute); + String fileNamePattern = element.getAttribute(patternPrefix + "-pattern"); + String fileNameRegex = element.getAttribute(patternPrefix + "-regex"); boolean hasFilter = StringUtils.hasText(filter); boolean hasFileNamePattern = StringUtils.hasText(fileNamePattern); boolean hasFileNameRegex = StringUtils.hasText(fileNameRegex); @@ -85,23 +90,27 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo count += hasFileNamePattern ? 1 : 0; count += hasFileNameRegex ? 1 : 0; if (count > 1) { - parserContext.getReaderContext().error("at most one of 'filename-pattern', " + - "'filename-regex', or 'filter' is allowed on remote file inbound adapter", element); + parserContext.getReaderContext().error("at most one of '" + patternPrefix + "-pattern', " + + "'" + patternPrefix + "-regex', or '" + filterAttribute + "' is allowed on a remote file outbound gateway", element); } else if (hasFilter) { - builder.addPropertyReference("filter", filter); + builder.addPropertyReference(propertyName, filter); } else if (hasFileNamePattern) { BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition( - this.getSimplePatternFileListFilterClassName()); + "filter".equals(filterAttribute) ? + this.getSimplePatternFileListFilterClassName() : + SimplePatternFileListFilter.class.getName()); filterBuilder.addConstructorArgValue(fileNamePattern); - builder.addPropertyValue("filter", filterBuilder.getBeanDefinition()); + builder.addPropertyValue(propertyName, filterBuilder.getBeanDefinition()); } else if (hasFileNameRegex) { BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition( - this.getRegexPatternFileListFilterClassName()); + "filter".equals(filterAttribute) ? + this.getRegexPatternFileListFilterClassName() : + RegexPatternFileListFilter.class.getName()); filterBuilder.addConstructorArgValue(fileNameRegex); - builder.addPropertyValue("filter", filterBuilder.getBeanDefinition()); + builder.addPropertyValue(propertyName, filterBuilder.getBeanDefinition()); } } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileParserUtils.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileParserUtils.java new file mode 100644 index 0000000000..e9ea713af8 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileParserUtils.java @@ -0,0 +1,91 @@ +/* + * 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.file.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.file.DefaultFileNameGenerator; +import org.springframework.integration.file.remote.RemoteFileTemplate; +import org.springframework.util.StringUtils; + +/** + * @author Oleg Zhurakousky + * @author Mark Fisher + * @author David Turanski + * @author Gary Russell + * @since 3.0 + * + */ +public final class FileParserUtils { + + private FileParserUtils() { + } + + public static BeanDefinition parseRemoteFileTemplate(Element element, ParserContext parserContext, + boolean atLeastOneRemoteDirectoryAttributeRequired) { + BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(RemoteFileTemplate.class); + + templateBuilder.addConstructorArgReference(element.getAttribute("session-factory")); + // configure MessageHandler properties + + IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "temporary-file-suffix"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "use-temporary-file-name"); + + IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "auto-create-directory"); + + BeanDefinition expressionDef = + IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("remote-directory", + "remote-directory-expression", parserContext, element, atLeastOneRemoteDirectoryAttributeRequired); + if (expressionDef != null) { + templateBuilder.addPropertyValue("remoteDirectoryExpression", expressionDef); + } + expressionDef = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("temporary-remote-directory", + "temporary-remote-directory-expression", parserContext, element, false); + if (expressionDef != null) { + templateBuilder.addPropertyValue("temporaryRemoteDirectoryExpression", expressionDef); + } + + // configure remote FileNameGenerator + String remoteFileNameGenerator = element.getAttribute("remote-filename-generator"); + String remoteFileNameGeneratorExpression = element.getAttribute("remote-filename-generator-expression"); + boolean hasRemoteFileNameGenerator = StringUtils.hasText(remoteFileNameGenerator); + boolean hasRemoteFileNameGeneratorExpression = StringUtils.hasText(remoteFileNameGeneratorExpression); + if (hasRemoteFileNameGenerator || hasRemoteFileNameGeneratorExpression) { + if (hasRemoteFileNameGenerator && hasRemoteFileNameGeneratorExpression) { + parserContext.getReaderContext().error( + "at most one of 'remote-filename-generator-expression' or 'remote-filename-generator' " + + "is allowed on a remote file outbound adapter", element); + } + if (hasRemoteFileNameGenerator) { + templateBuilder.addPropertyReference("fileNameGenerator", remoteFileNameGenerator); + } + else { + BeanDefinitionBuilder fileNameGeneratorBuilder = BeanDefinitionBuilder + .genericBeanDefinition(DefaultFileNameGenerator.class); + fileNameGeneratorBuilder.addPropertyValue("expression", remoteFileNameGeneratorExpression); + templateBuilder.addPropertyValue("fileNameGenerator", fileNameGeneratorBuilder.getBeanDefinition()); + } + } + IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "charset"); + templateBuilder.addPropertyValue("remoteFileSeparator", element.getAttribute("remote-file-separator")); + return templateBuilder.getBeanDefinition(); + } + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/RemoteFileOutboundChannelAdapterParser.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/RemoteFileOutboundChannelAdapterParser.java index 919c79114a..2d4faa9ea6 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/RemoteFileOutboundChannelAdapterParser.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/RemoteFileOutboundChannelAdapterParser.java @@ -18,19 +18,12 @@ package org.springframework.integration.file.config; import org.w3c.dom.Element; -import org.springframework.beans.factory.BeanDefinitionStoreException; 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.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.expression.common.LiteralExpression; -import org.springframework.integration.config.ExpressionFactoryBean; import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser; -import org.springframework.integration.config.xml.IntegrationNamespaceUtils; -import org.springframework.integration.file.DefaultFileNameGenerator; import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler; -import org.springframework.util.StringUtils; /** * @author Oleg Zhurakousky @@ -45,71 +38,10 @@ public class RemoteFileOutboundChannelAdapterParser extends AbstractOutboundChan protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) { BeanDefinitionBuilder handlerBuilder = BeanDefinitionBuilder.genericBeanDefinition(FileTransferringMessageHandler.class); - handlerBuilder.addConstructorArgReference(element.getAttribute("session-factory")); - // configure MessageHandler properties + BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, true); - IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "temporary-file-suffix"); - IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "use-temporary-file-name"); - - IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "auto-create-directory"); - - this.configureRemoteDirectories(element, handlerBuilder); - - // configure remote FileNameGenerator - String remoteFileNameGenerator = element.getAttribute("remote-filename-generator"); - String remoteFileNameGeneratorExpression = element.getAttribute("remote-filename-generator-expression"); - boolean hasRemoteFileNameGenerator = StringUtils.hasText(remoteFileNameGenerator); - boolean hasRemoteFileNameGeneratorExpression = StringUtils.hasText(remoteFileNameGeneratorExpression); - if (hasRemoteFileNameGenerator || hasRemoteFileNameGeneratorExpression) { - if (hasRemoteFileNameGenerator && hasRemoteFileNameGeneratorExpression) { - throw new BeanDefinitionStoreException("at most one of 'remote-filename-generator-expression' or 'remote-filename-generator' " + - "is allowed on a remote file outbound adapter"); - } - if (hasRemoteFileNameGenerator) { - handlerBuilder.addPropertyReference("fileNameGenerator", remoteFileNameGenerator); - } - else { - BeanDefinitionBuilder fileNameGeneratorBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultFileNameGenerator.class); - fileNameGeneratorBuilder.addPropertyValue("expression", remoteFileNameGeneratorExpression); - handlerBuilder.addPropertyValue("fileNameGenerator", fileNameGeneratorBuilder.getBeanDefinition()); - } - } - IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "charset"); - handlerBuilder.addPropertyValue("remoteFileSeparator", element.getAttribute("remote-file-separator")); + handlerBuilder.addConstructorArgValue(templateDefinition); return handlerBuilder.getBeanDefinition(); } - private void configureRemoteDirectories(Element element, BeanDefinitionBuilder handlerBuilder){ - this.doConfigureRemoteDirectory(element, handlerBuilder, "remote-directory", "remote-directory-expression", "remoteDirectoryExpression", true); - this.doConfigureRemoteDirectory(element, handlerBuilder, "temporary-remote-directory", "temporary-remote-directory-expression", "temporaryRemoteDirectoryExpression", false); - } - - private void doConfigureRemoteDirectory(Element element, BeanDefinitionBuilder handlerBuilder, - String directoryAttribute, String directoryExpressionAttribute, - String directoryExpressionPropertyName, boolean atLeastOneRequired){ - String remoteDirectory = element.getAttribute(directoryAttribute); - String remoteDirectoryExpression = element.getAttribute(directoryExpressionAttribute); - boolean hasRemoteDirectory = StringUtils.hasText(remoteDirectory); - boolean hasRemoteDirectoryExpression = StringUtils.hasText(remoteDirectoryExpression); - if (atLeastOneRequired){ - if (!(hasRemoteDirectory ^ hasRemoteDirectoryExpression)) { - throw new BeanDefinitionStoreException("exactly one of '" + directoryAttribute + "' or '" + directoryExpressionAttribute + "' " + - "is required on a remote file outbound adapter"); - } - } - - BeanDefinition remoteDirectoryExpressionDefinition = null; - if (hasRemoteDirectory) { - remoteDirectoryExpressionDefinition = new RootBeanDefinition(LiteralExpression.class); - remoteDirectoryExpressionDefinition.getConstructorArgumentValues().addGenericArgumentValue(remoteDirectory); - } - else if (hasRemoteDirectoryExpression) { - remoteDirectoryExpressionDefinition = new RootBeanDefinition(ExpressionFactoryBean.class); - remoteDirectoryExpressionDefinition.getConstructorArgumentValues().addGenericArgumentValue(remoteDirectoryExpression); - } - if (remoteDirectoryExpressionDefinition != null){ - handlerBuilder.addPropertyValue(directoryExpressionPropertyName, remoteDirectoryExpressionDefinition); - } - } - } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractPersistentAcceptOnceFileListFilter.java b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractPersistentAcceptOnceFileListFilter.java new file mode 100644 index 0000000000..3a44133ca7 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractPersistentAcceptOnceFileListFilter.java @@ -0,0 +1,92 @@ +/* + * 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.file.filters; + +import org.springframework.integration.metadata.MetadataStore; +import org.springframework.util.Assert; + +/** + * Stores "seen" files in a MetadataStore to survive application restarts. + * The default key is 'prefix' plus the absolute file name; value is the timestamp of the file. + * Files are deemed as already 'seen' if they exist in the store and have the + * same modified time as the current file. + * + * @author Gary Russell + * @since 3.0 + * + */ +public abstract class AbstractPersistentAcceptOnceFileListFilter extends AbstractFileListFilter { + + protected final MetadataStore store; + + protected final String prefix; + + private final Object monitor = new Object(); + + public AbstractPersistentAcceptOnceFileListFilter(MetadataStore store, String prefix) { + Assert.notNull(store, "'store' cannot be null"); + Assert.notNull(prefix, "'prefix' cannot be null"); + this.store = store; + this.prefix = prefix; + } + + @Override + protected boolean accept(F file) { + String key = buildKey(file); + synchronized(monitor) { + String value = store.get(key); + if (value != null && isEqual(file, value)) { + return false; + } + store.put(key, value(file)); + } + return true; + } + + /** + * The default value stored for the key is the last modified date. + * @param file The file. + * @return The value to store for the file. + */ + private String value(F file) { + return Long.toString(this.modified(file)); + } + + /** + * Override this method if you wish to use something other than the + * modified timestamp to determine equality. + * @param file The file. + * @param value The current value for the key in the store. + * @return true if equal. + */ + protected boolean isEqual(F file, String value) { + return Long.valueOf(value).longValue() == this.modified(file); + } + + /** + * The default key is the {@link #prefix} plus the full filename. + * @param file The file. + * @return The key. + */ + protected String buildKey(F file) { + return this.prefix + this.fileName(file); + } + + protected abstract long modified(F file); + + protected abstract String fileName(F file); + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemPersistentAcceptOnceFileListFilter.java b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemPersistentAcceptOnceFileListFilter.java new file mode 100644 index 0000000000..6fb2a2c19f --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemPersistentAcceptOnceFileListFilter.java @@ -0,0 +1,44 @@ +/* + * 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.file.filters; + + +import java.io.File; + +import org.springframework.integration.metadata.MetadataStore; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class FileSystemPersistentAcceptOnceFileListFilter extends AbstractPersistentAcceptOnceFileListFilter { + + public FileSystemPersistentAcceptOnceFileListFilter(MetadataStore store, String prefix) { + super(store, prefix); + } + + @Override + protected long modified(File file) { + return file.lastModified(); + } + + @Override + protected String fileName(File file) { + return file.getAbsolutePath(); + } + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/InputStreamCallback.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/InputStreamCallback.java new file mode 100644 index 0000000000..47bf6284af --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/InputStreamCallback.java @@ -0,0 +1,40 @@ +/* + * 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.file.remote; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Callback for stream-based file retrieval using a RemoteFileOperations. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface InputStreamCallback { + + /** + * Called with the InputStream for the remote file. The caller will + * take care of closing the stream and finalizing the file retrieval operation after + * this method exits. + * + * @param stream The InputStream. + * @throws IOException + */ + void doWithInputStream(InputStream stream) throws IOException; + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.java new file mode 100644 index 0000000000..c52a5a5f61 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileOperations.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.file.remote; + +import org.springframework.messaging.Message; + +/** + * Strategy for performing operations on remote files. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RemoteFileOperations { + + /** + * Send a file to a remote server, based on information in a message. + * + * @param message The message. + * @return The remote path, or null if no local file was found. + * @throws Exception + */ + String send(Message message); + + /** + * Send a file to a remote server, based on information in a message. + * The subDirectory is appended to the remote directory evaluated from + * the message. + * + * @param message The message. + * @param subDirectory The sub directory. + * @return The remote path, or null if no local file was found. + * @throws Exception + */ + + String send(Message message, String subDirectory); + /** + * Retrieve a remote file as an InputStream, based on information in a message. + * + * @param callback the callback. + * @return true if the operation was successful. + */ + boolean get(Message message, InputStreamCallback callback); + + /** + * Remove a remote file. + * + * @param path The full path to the file. + * @return true when successful + */ + boolean remove(String path); + + /** + * Rename a remote file, creating directories if needed. + * + * @param fromPath The current path. + * @param toPath The new path. + */ + void rename(String fromPath, String toPath); + + /** + * Execute the callback's doInSession method after obtaining a session. + * Reliably closes the session when the method exits. + * + * @param callback the SessionCallback. + * @return The result of the callback method. + */ + T execute(SessionCallback callback); + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java new file mode 100644 index 0000000000..218fad0051 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java @@ -0,0 +1,423 @@ +/* + * 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.file.remote; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.expression.Expression; +import org.springframework.integration.file.DefaultFileNameGenerator; +import org.springframework.integration.file.FileNameGenerator; +import org.springframework.integration.file.remote.session.Session; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.messaging.MessagingException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Iwein Fuld + * @author Mark Fisher + * @author Josh Long + * @author Oleg Zhurakousky + * @author David Turanski + * @author Gary Russell + * @since 3.0 + * + */ +public class RemoteFileTemplate implements RemoteFileOperations, InitializingBean, BeanFactoryAware { + + private final Log logger = LogFactory.getLog(this.getClass()); + + /** + * the {@link SessionFactory} for acquiring remote file Sessions. + */ + private final SessionFactory sessionFactory; + + private volatile String temporaryFileSuffix =".writing"; + + private volatile boolean autoCreateDirectory = false; + + private volatile boolean useTemporaryFileName = true; + + private volatile ExpressionEvaluatingMessageProcessor directoryExpressionProcessor; + + private volatile ExpressionEvaluatingMessageProcessor temporaryDirectoryExpressionProcessor; + + private volatile ExpressionEvaluatingMessageProcessor fileNameProcessor; + + private volatile FileNameGenerator fileNameGenerator = new DefaultFileNameGenerator(); + + private volatile boolean fileNameGeneratorSet; + + private volatile String charset = "UTF-8"; + + private volatile String remoteFileSeparator = "/"; + + private volatile boolean hasExplicitlySetSuffix; + + private volatile BeanFactory beanFactory; + + public RemoteFileTemplate(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, "sessionFactory must not be null"); + this.sessionFactory = sessionFactory; + } + + public void setAutoCreateDirectory(boolean autoCreateDirectory) { + this.autoCreateDirectory = autoCreateDirectory; + } + + public void setRemoteFileSeparator(String remoteFileSeparator) { + Assert.notNull(remoteFileSeparator, "'remoteFileSeparator' must not be null"); + this.remoteFileSeparator = remoteFileSeparator; + } + + public final String getRemoteFileSeparator() { + return remoteFileSeparator; + } + + public void setRemoteDirectoryExpression(Expression remoteDirectoryExpression) { + Assert.notNull(remoteDirectoryExpression, "remoteDirectoryExpression must not be null"); + this.directoryExpressionProcessor = new ExpressionEvaluatingMessageProcessor(remoteDirectoryExpression, String.class); + } + + public void setTemporaryRemoteDirectoryExpression(Expression temporaryRemoteDirectoryExpression) { + Assert.notNull(temporaryRemoteDirectoryExpression, "temporaryRemoteDirectoryExpression must not be null"); + this.temporaryDirectoryExpressionProcessor = new ExpressionEvaluatingMessageProcessor(temporaryRemoteDirectoryExpression, String.class); + } + + public void setFileNameExpression(Expression fileNameExpression) { + Assert.notNull(fileNameExpression, "fileNameExpression must not be null"); + this.fileNameProcessor = new ExpressionEvaluatingMessageProcessor(fileNameExpression, String.class); + } + + public String getTemporaryFileSuffix() { + return this.temporaryFileSuffix; + } + + public boolean isUseTemporaryFileName() { + return useTemporaryFileName; + } + + public void setUseTemporaryFileName(boolean useTemporaryFileName) { + this.useTemporaryFileName = useTemporaryFileName; + } + + public void setFileNameGenerator(FileNameGenerator fileNameGenerator) { + this.fileNameGenerator = (fileNameGenerator != null) ? fileNameGenerator : new DefaultFileNameGenerator(); + this.fileNameGeneratorSet = fileNameGenerator != null; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public void setTemporaryFileSuffix(String temporaryFileSuffix) { + Assert.notNull(temporaryFileSuffix, "'temporaryFileSuffix' must not be null"); + this.hasExplicitlySetSuffix = true; + this.temporaryFileSuffix = temporaryFileSuffix; + } + + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public void afterPropertiesSet() throws Exception { + BeanFactory beanFactory = this.beanFactory; + if (beanFactory != null) { + if (this.directoryExpressionProcessor != null) { + this.directoryExpressionProcessor.setBeanFactory(beanFactory); + } + if (this.temporaryDirectoryExpressionProcessor != null) { + this.temporaryDirectoryExpressionProcessor.setBeanFactory(beanFactory); + } + if (!this.fileNameGeneratorSet && this.fileNameGenerator instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.fileNameGenerator).setBeanFactory(beanFactory); + } + if (this.fileNameProcessor != null) { + this.fileNameProcessor.setBeanFactory(beanFactory); + } + } + if (this.autoCreateDirectory){ + Assert.hasText(this.remoteFileSeparator, "'remoteFileSeparator' must not be empty when 'autoCreateDirectory' is set to 'true'"); + } + if (hasExplicitlySetSuffix && !useTemporaryFileName){ + this.logger.warn("Since 'use-temporary-file-name' is set to 'false' the value of 'temporary-file-suffix' has no effect"); + } + } + + @Override + public String send(final Message message) { + return this.send(message, null); + } + + @Override + public String send(final Message message, final String subDirectory) { + Assert.notNull(this.directoryExpressionProcessor, "'remoteDirectoryExpression' is required"); + final StreamHolder inputStreamHolder = this.payloadToInputStream(message); + if (inputStreamHolder != null) { + return this.execute(new SessionCallback() { + + @Override + public String doInSession(Session session) throws IOException { + String fileName = inputStreamHolder.getName(); + try { + String remoteDirectory = RemoteFileTemplate.this.directoryExpressionProcessor + .processMessage(message); + remoteDirectory = RemoteFileTemplate.this.normalizeDirectoryPath(remoteDirectory); + if (StringUtils.hasText(subDirectory)) { + if (subDirectory.startsWith(RemoteFileTemplate.this.remoteFileSeparator)) { + remoteDirectory += subDirectory.substring(1); + } + else { + remoteDirectory += RemoteFileTemplate.this.normalizeDirectoryPath(subDirectory); + } + } + String temporaryRemoteDirectory = remoteDirectory; + if (RemoteFileTemplate.this.temporaryDirectoryExpressionProcessor != null) { + temporaryRemoteDirectory = RemoteFileTemplate.this.temporaryDirectoryExpressionProcessor + .processMessage(message); + } + fileName = RemoteFileTemplate.this.fileNameGenerator.generateFileName(message); + RemoteFileTemplate.this.sendFileToRemoteDirectory(inputStreamHolder.getStream(), + temporaryRemoteDirectory, remoteDirectory, fileName, session); + return remoteDirectory + fileName; + } + catch (FileNotFoundException e) { + throw new MessageDeliveryException(message, "File [" + inputStreamHolder.getName() + + "] not found in local working directory; it was moved or deleted unexpectedly.", e); + } + catch (IOException e) { + throw new MessageDeliveryException(message, "Failed to transfer file [" + + inputStreamHolder.getName() + " -> " + fileName + + "] from local directory to remote directory.", e); + } + catch (Exception e) { + throw new MessageDeliveryException(message, "Error handling message for file [" + + inputStreamHolder.getName() + " -> " + fileName + "]", e); + } + } + }); + } + else { + // A null holder means a File payload that does not exist. + if (logger.isWarnEnabled()) { + logger.warn("File " + message.getPayload() + " does not exist"); + } + return null; + } + } + + @Override + public boolean remove(final String path) { + return this.execute(new SessionCallback() { + + @Override + public Boolean doInSession(Session session) throws IOException { + return session.remove(path); + } + }); + } + + @Override + public void rename(final String fromPath, final String toPath) { + Assert.hasText(fromPath, "Old filename cannot be null or empty"); + Assert.hasText(toPath, "New filename cannot be null or empty"); + + this.execute(new SessionCallbackWithoutResult() { + + @Override + public void doInSessionWithoutResult(Session session) throws IOException { + int lastSeparator = toPath.lastIndexOf(RemoteFileTemplate.this.remoteFileSeparator); + if (lastSeparator > 0) { + String remoteFileDirectory = toPath.substring(0, lastSeparator + 1); + RemoteFileUtils.makeDirectories(remoteFileDirectory, session, + RemoteFileTemplate.this.remoteFileSeparator, RemoteFileTemplate.this.logger); + } + session.rename(fromPath, toPath); + } + }); + } + + + @Override + public boolean get(final Message message, final InputStreamCallback callback) { + Assert.notNull(this.fileNameProcessor, "'fileNameProcessor' needed to use get"); + return this.execute(new SessionCallback() { + + @Override + public Boolean doInSession(Session session) throws IOException { + final String remotePath = RemoteFileTemplate.this.fileNameProcessor.processMessage(message); + InputStream inputStream = session.readRaw(remotePath); + callback.doWithInputStream(inputStream); + inputStream.close(); + return session.finalizeRaw(); + } + }); + } + + @Override + public T execute(SessionCallback callback) { + Session session = null; + try { + session = this.sessionFactory.getSession(); + Assert.notNull(session, "failed to acquire a Session"); + return callback.doInSession(session); + } + catch (IOException e) { + throw new MessagingException("Failed to execute on session", e); + } + finally { + if (session != null) { + try { + session.close(); + } + catch (Exception ignored) { + if (logger.isDebugEnabled()) { + logger.debug("failed to close Session", ignored); + } + } + } + } + } + + private StreamHolder payloadToInputStream(Message message) throws MessageDeliveryException { + try { + Object payload = message.getPayload(); + InputStream dataInputStream = null; + String name = null; + if (payload instanceof File) { + File inputFile = (File) payload; + if (inputFile.exists()) { + dataInputStream = new BufferedInputStream(new FileInputStream(inputFile)); + name = inputFile.getAbsolutePath(); + } + } + else if (payload instanceof byte[] || payload instanceof String) { + byte[] bytes = null; + if (payload instanceof String) { + bytes = ((String) payload).getBytes(this.charset); + name = "String payload"; + } + else { + bytes = (byte[]) payload; + name = "byte[] payload"; + } + dataInputStream = new ByteArrayInputStream(bytes); + } + else { + throw new IllegalArgumentException("Unsupported payload type. The only supported payloads are " + + "java.io.File, java.lang.String, and byte[]"); + } + if (dataInputStream == null) { + return null; + } + else { + return new StreamHolder(dataInputStream, name); + } + } + catch (Exception e) { + throw new MessageDeliveryException(message, "Failed to create sendable file.", e); + } + } + + private void sendFileToRemoteDirectory(InputStream inputStream, String temporaryRemoteDirectory, + String remoteDirectory, String fileName, Session session) throws IOException { + + remoteDirectory = this.normalizeDirectoryPath(remoteDirectory); + temporaryRemoteDirectory = this.normalizeDirectoryPath(temporaryRemoteDirectory); + + String remoteFilePath = remoteDirectory + fileName; + String tempRemoteFilePath = temporaryRemoteDirectory + fileName; + // write remote file first with temporary file extension if enabled + + String tempFilePath = tempRemoteFilePath + (useTemporaryFileName ? this.temporaryFileSuffix : ""); + + if (this.autoCreateDirectory) { + try { + RemoteFileUtils.makeDirectories(remoteDirectory, session, this.remoteFileSeparator, this.logger); + } + catch (IllegalStateException e) { + // Revert to old FTP behavior if recursive mkdir fails, for backwards compatibility + session.mkdir(remoteDirectory); + } + } + + try { + session.write(inputStream, tempFilePath); + // then rename it to its final name if necessary + if (useTemporaryFileName){ + session.rename(tempFilePath, remoteFilePath); + } + } + catch (Exception e) { + throw new MessagingException("Failed to write to '" + tempFilePath + "' while uploading the file", e); + } + finally { + inputStream.close(); + } + } + + private String normalizeDirectoryPath(String directoryPath){ + if (!StringUtils.hasText(directoryPath)) { + directoryPath = ""; + } + else if (!directoryPath.endsWith(this.remoteFileSeparator)) { + directoryPath += this.remoteFileSeparator; + } + return directoryPath; + } + + private class StreamHolder { + + private final InputStream stream; + + private final String name; + + private StreamHolder(InputStream stream, String name) { + this.stream = stream; + this.name = name; + } + + public InputStream getStream() { + return stream; + } + + public String getName() { + return name; + } + + } + + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/SessionCallback.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/SessionCallback.java new file mode 100644 index 0000000000..1970bb2a6d --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/SessionCallback.java @@ -0,0 +1,43 @@ +/* + * 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.file.remote; + +import java.io.IOException; + +import org.springframework.integration.file.remote.session.Session; + +/** + * Callback invoked by {@code RemoteFileOperations.execute()} - allows multiple operations + * on a session. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface SessionCallback { + + /** + * Called within the context of a session. + * Perform some operation(s) on the session. The caller will take + * care of closing the session after this method exits. + * + * @param session The session. + * @return The result of type T. + * @throws IOException + */ + T doInSession(Session session) throws IOException; + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/SessionCallbackWithoutResult.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/SessionCallbackWithoutResult.java new file mode 100644 index 0000000000..b13af2c978 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/SessionCallbackWithoutResult.java @@ -0,0 +1,48 @@ +/* + * 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.file.remote; + +import java.io.IOException; + +import org.springframework.integration.file.remote.session.Session; + +/** + * Simple convenience implementation of {@link SessionCallback} for cases where + * no result is returned. + * + * @author Gary Russell + * @since 3.0 + * + */ +public abstract class SessionCallbackWithoutResult implements SessionCallback { + + @Override + public Object doInSession(Session session) throws IOException { + this.doInSessionWithoutResult(session); + return null; + } + + /** + * Called within the context of a session. + * Perform some operation(s) on the session. The caller will take + * care of closing the session after this method exits. + * + * @param session The session. + * @throws IOException + */ + protected abstract void doInSessionWithoutResult(Session session) throws IOException; + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java index 6606a873b8..8102a7fd54 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java @@ -38,7 +38,8 @@ import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.file.FileHeaders; import org.springframework.integration.file.filters.FileListFilter; import org.springframework.integration.file.remote.AbstractFileInfo; -import org.springframework.integration.file.remote.RemoteFileUtils; +import org.springframework.integration.file.remote.RemoteFileTemplate; +import org.springframework.integration.file.remote.SessionCallback; import org.springframework.integration.file.remote.session.Session; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; @@ -59,7 +60,7 @@ import org.springframework.util.StringUtils; */ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReplyProducingMessageHandler { - protected final SessionFactory sessionFactory; + private final RemoteFileTemplate remoteFileTemplate; protected final Command command; @@ -91,7 +92,17 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply /** * Move (rename) a remote file. */ - MV("mv"); + MV("mv"), + + /** + * Put a local file to the remote system. + */ + PUT("put"), + + /** + * Put multiple local files to the remote system. + */ + MPUT("mput"); private String command; @@ -187,25 +198,27 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply protected volatile Set