From 302edf922193e7ce08722cb1ccc7ad8f0057ba79 Mon Sep 17 00:00:00 2001 From: Liujiong Date: Wed, 23 Jul 2014 19:49:42 +0300 Subject: [PATCH] INT-3465:Content Enricher Improvements JIRA: https://jira.spring.io/browse/INT-3465 Add support for adding/removing individual recipients to the RecipientListRouter Modify documentation in what's new and spring-integration-4.1.xsd Polishing `AbstractRemoteFileOutboundGateway`: close `outputStream` before `file.delete()` to release exclusive file-lock --- .../config/xml/EnricherParser.java | 96 ++++++-- .../transformer/ContentEnricher.java | 209 +++++++++++------- .../config/xml/spring-integration-4.1.xsd | 8 + .../config/xml/EnricherParserTests.java | 6 +- .../xml/EnricherParserTests4-context.xml | 49 ++++ .../config/xml/EnricherParserTests4.java | 157 +++++++++++++ .../AbstractRemoteFileOutboundGateway.java | 5 + src/reference/docbook/content-enrichment.xml | 34 ++- src/reference/docbook/whats-new.xml | 8 + 9 files changed, 453 insertions(+), 119 deletions(-) create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4-context.xml create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java index e0d111a48e..32a7cdd873 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java @@ -24,7 +24,9 @@ import org.springframework.beans.factory.config.BeanDefinition; 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.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.expression.ValueExpression; import org.springframework.integration.transformer.ContentEnricher; @@ -38,6 +40,7 @@ import org.springframework.util.xml.DomUtils; * * @author Mark Fisher * @author Artem Bilan + * @author Liujiong * @since 2.1 */ public class EnricherParser extends AbstractConsumerEndpointParser { @@ -51,29 +54,31 @@ public class EnricherParser extends AbstractConsumerEndpointParser { IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "requires-reply"); - List subElements = DomUtils.getChildElementsByTagName(element, "property"); if (!CollectionUtils.isEmpty(subElements)) { ManagedMap expressions = new ManagedMap(); + ManagedMap nullResultExpressions = new ManagedMap(); for (Element subElement : subElements) { String name = subElement.getAttribute("name"); String value = subElement.getAttribute("value"); String type = subElement.getAttribute("type"); String expression = subElement.getAttribute("expression"); - + String nullResultExpression = subElement.getAttribute("null-result-expression"); boolean hasAttributeValue = StringUtils.hasText(value); boolean hasAttributeExpression = StringUtils.hasText(expression); + boolean hasAttributeNullResultExpression = StringUtils.hasText(nullResultExpression); if (hasAttributeValue && hasAttributeExpression){ parserContext.getReaderContext().error("Only one of 'value' or 'expression' is allowed", element); } - if (!hasAttributeValue && !hasAttributeExpression){ - parserContext.getReaderContext().error("One of 'value' or 'expression' is required", element); + if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression){ + parserContext.getReaderContext().error("One of 'value' or 'expression' or 'null-result-expression' is required", element); } - BeanDefinition expressionDef; + BeanDefinition expressionDef = null; + BeanDefinition nullResultExpressionExpressionDef; if (hasAttributeValue) { BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ValueExpression.class); @@ -85,7 +90,7 @@ public class EnricherParser extends AbstractConsumerEndpointParser { } expressionDef = expressionBuilder.getBeanDefinition(); } - else { + else if (hasAttributeExpression) { if (StringUtils.hasText(type)) { parserContext.getReaderContext().error("The 'type' attribute for '' of '' " + "is not allowed with an 'expression' attribute.", element); @@ -95,36 +100,83 @@ public class EnricherParser extends AbstractConsumerEndpointParser { .addConstructorArgValue(expression) .getBeanDefinition(); } - - - - expressions.put(name, expressionDef); + if (expressionDef != null){ + expressions.put(name, expressionDef); + } + if (hasAttributeNullResultExpression) { + nullResultExpressionExpressionDef = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class) + .addConstructorArgValue(nullResultExpression).getBeanDefinition(); + nullResultExpressions.put(name, nullResultExpressionExpressionDef); + } + } + if (expressions.size() > 0) { + builder.addPropertyValue("propertyExpressions", expressions); + } + if (nullResultExpressions.size() > 0) { + builder.addPropertyValue("nullResultPropertyExpressions", nullResultExpressions); } - builder.addPropertyValue("propertyExpressions", expressions); } subElements = DomUtils.getChildElementsByTagName(element, "header"); if (!CollectionUtils.isEmpty(subElements)) { ManagedMap expressions = new ManagedMap(); + ManagedMap nullResultHeaderExpressions = new ManagedMap(); for (Element subElement : subElements) { String name = subElement.getAttribute("name"); - BeanDefinition expressionDefinition = IntegrationNamespaceUtils - .createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext, - subElement, true); + String nullResultHeaderExpression = subElement.getAttribute("null-result-expression"); + String valueElementValue = subElement.getAttribute("value"); + String expressionElementValue = subElement.getAttribute("expression"); + boolean hasAttributeValue = StringUtils.hasText(valueElementValue); + boolean hasAttributeExpression = StringUtils.hasText(expressionElementValue); + boolean hasAttributeNullResultExpression = StringUtils.hasText(nullResultHeaderExpression); + if (hasAttributeValue && hasAttributeExpression){ + parserContext.getReaderContext().error("Only one of '" + "value" + "' or '" + + "expression" + "' is allowed", subElement); + } + + if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression){ + parserContext.getReaderContext().error("One of 'value' or 'expression' or 'null-result-expression' is required", subElement); + } + BeanDefinition expressionDef = null; + if (hasAttributeValue) { + expressionDef = new RootBeanDefinition(LiteralExpression.class); + expressionDef.getConstructorArgumentValues().addGenericArgumentValue(valueElementValue); + } + else if (hasAttributeExpression) { + expressionDef = IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("expression", subElement); + } + if (StringUtils.hasText(subElement.getAttribute("expression")) && StringUtils.hasText(subElement.getAttribute("type"))) { parserContext.getReaderContext() .warning("The use of a 'type' attribute is deprecated since 4.0 " - + "when using 'expression'", element); + + "when using 'expression'", subElement); + } + if (expressionDef != null) { + BeanDefinitionBuilder valueProcessorBuilder = BeanDefinitionBuilder + .genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class) + .addConstructorArgValue(expressionDef) + .addConstructorArgValue(subElement.getAttribute("type")); + IntegrationNamespaceUtils.setValueIfAttributeDefined(valueProcessorBuilder, subElement, "overwrite"); + expressions.put(name, valueProcessorBuilder.getBeanDefinition()); + } + if (hasAttributeNullResultExpression) { + BeanDefinition nullResultExpressionDefinition = IntegrationNamespaceUtils + .createExpressionDefIfAttributeDefined("null-result-expression", subElement); + BeanDefinitionBuilder nullResultValueProcessorBuilder = BeanDefinitionBuilder + .genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class) + .addConstructorArgValue(nullResultExpressionDefinition) + .addConstructorArgValue(subElement.getAttribute("type")); + IntegrationNamespaceUtils.setValueIfAttributeDefined(nullResultValueProcessorBuilder, subElement, "overwrite"); + nullResultHeaderExpressions.put(name, nullResultValueProcessorBuilder.getBeanDefinition()); } - BeanDefinitionBuilder valueProcessorBuilder = BeanDefinitionBuilder - .genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class) - .addConstructorArgValue(expressionDefinition) - .addConstructorArgValue(subElement.getAttribute("type")); - IntegrationNamespaceUtils.setValueIfAttributeDefined(valueProcessorBuilder, subElement, "overwrite"); - expressions.put(name, valueProcessorBuilder.getBeanDefinition()); } - builder.addPropertyValue("headerExpressions", expressions); + if (expressions.size() > 0) { + builder.addPropertyValue("headerExpressions", expressions); + } + if (nullResultHeaderExpressions.size() > 0) { + builder.addPropertyValue("nullResultHeaderExpressions", nullResultHeaderExpressions); + } } IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "should-clone-payload"); 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 2f890a7e96..5e4090effb 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 @@ -40,22 +40,30 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** - * Content Enricher is a Message Transformer that can augment a message's payload - * with either static values or by optionally invoking a downstream message flow - * via its request channel and then applying values from the reply Message to the - * original payload. + * Content Enricher is a Message Transformer that can augment a message's payload with + * either static values or by optionally invoking a downstream message flow via its + * request channel and then applying values from the reply Message to the original + * payload. * * @author Mark Fisher * @author Gunnar Hillert * @author Gary Russell * @author Artem Bilan + * @author Liujiong * @since 2.1 */ -public class ContentEnricher extends AbstractReplyProducingMessageHandler implements Lifecycle, IntegrationEvaluationContextAware { +public class ContentEnricher extends AbstractReplyProducingMessageHandler + implements Lifecycle, IntegrationEvaluationContextAware { + + private volatile Map nullResultPropertyExpressions = new HashMap(); + + private volatile Map> nullResultHeaderExpressions = + new HashMap>(); private volatile Map propertyExpressions = new HashMap(); - private volatile Map> headerExpressions = new HashMap>(); + private volatile Map> headerExpressions = + new HashMap>(); private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); @@ -81,11 +89,25 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem private volatile Long replyTimeout; + public void setNullResultPropertyExpressions(Map nullResultPropertyExpressions) { + Map localMap = new HashMap(nullResultPropertyExpressions.size()); + for (Map.Entry entry : nullResultPropertyExpressions.entrySet()) { + String key = entry.getKey(); + Expression value = entry.getValue(); + localMap.put(parser.parseExpression(key), value); + } + this.nullResultPropertyExpressions = localMap; + } + + public void setNullResultHeaderExpressions(Map> nullResultHeaderExpressions) { + this.nullResultHeaderExpressions = new HashMap>( + nullResultHeaderExpressions); + } + /** - * Provide the map of expressions to evaluate when enriching the target payload. - * The keys should simply be property names, and the values should be Expressions - * that will evaluate against the reply Message as the root object. - * + * Provide the map of expressions to evaluate when enriching the target payload. The + * keys should simply be property names, and the values should be Expressions that + * will evaluate against the reply Message as the root object. * @param propertyExpressions The property expressions. */ public void setPropertyExpressions(Map propertyExpressions) { @@ -103,10 +125,9 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem /** * Provide the map of {@link HeaderValueMessageProcessor} to evaluate when enriching - * the target MessageHeaders. - * The keys should simply be header names, and the values should be Expressions - * that will evaluate against the reply Message as the root object. - * + * the target MessageHeaders. The keys should simply be header names, and the values + * should be Expressions that will evaluate against the reply Message as the root + * object. * @param headerExpressions The header expressions. */ public void setHeaderExpressions(Map> headerExpressions) { @@ -117,11 +138,10 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Sets the content enricher's request channel. If specified, then an internal - * Gateway will be initialized. Setting a request channel is optional. - * Not setting a request channel is useful in situations where - * message payloads shall be enriched with static values only. - * + * Sets the content enricher's request channel. If specified, then an internal Gateway + * will be initialized. Setting a request channel is optional. Not setting a request + * channel is useful in situations where message payloads shall be enriched with + * static values only. * @param requestChannel The request channel. */ public void setRequestChannel(MessageChannel requestChannel) { @@ -134,9 +154,8 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem /** * Sets the content enricher's reply channel. If not specified, yet the request - * channel is set, an anonymous reply channel will automatically created - * for each request. - * + * channel is set, an anonymous reply channel will automatically created for each + * request. * @param replyChannel The reply channel. */ public void setReplyChannel(MessageChannel replyChannel) { @@ -148,9 +167,8 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Set the timeout value for sending request messages. If not explicitly - * configured, the default is one second. - * + * Set the timeout value for sending request messages. If not explicitly configured, + * the default is one second. * @param requestTimeout the timeout value in milliseconds. Must not be null. */ public void setRequestTimeout(Long requestTimeout) { @@ -159,9 +177,8 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Set the timeout value for receiving reply messages. If not explicitly - * configured, the default is one second. - * + * Set the timeout value for receiving reply messages. If not explicitly configured, + * the default is one second. * @param replyTimeout the timeout value in milliseconds. Must not be null. */ public void setReplyTimeout(Long replyTimeout) { @@ -170,28 +187,27 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * By default the original message's payload will be used as the actual payload - * that will be send to the request-channel. - * - * By providing a SpEL expression as value for this setter, a subset of the - * original payload, a header value or any other resolvable SpEL expression - * can be used as the basis for the payload, that will be send to the - * request-channel. - * - * For the Expression evaluation the full message is available as the root object. - * + * By default the original message's payload will be used as the actual payload that + * will be send to the request-channel. + *

+ * By providing a SpEL expression as value for this setter, a subset of the original + * payload, a header value or any other resolvable SpEL expression can be used as the + * basis for the payload, that will be send to the request-channel. + *

+ * For the Expression evaluation the full message is available as the root + * object. + *

* For instance the following SpEL expressions (among others) are possible: - * + *

*

    - *
  • payload.foo
  • - *
  • headers.foobar
  • - *
  • new java.util.Date()
  • - *
  • 'foo' + 'bar'
  • + *
  • payload.foo
  • + *
  • headers.foobar
  • + *
  • new java.util.Date()
  • + *
  • 'foo' + 'bar'
  • *
- * - * If more sophisticated logic is required (e.g. changing the message - * headers etc.) please use additional downstream transformers. - * + *

+ * If more sophisticated logic is required (e.g. changing the message headers etc.) + * please use additional downstream transformers. * @param requestPayloadExpression The request payload expression. * */ @@ -200,9 +216,8 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Specify whether to clone payload objects to create the target object. - * This is only applicable for payload types that implement Cloneable. - * + * Specify whether to clone payload objects to create the target object. This is only + * applicable for payload types that implement Cloneable. * @param shouldClonePayload true if the payload should be cloned. */ public void setShouldClonePayload(boolean shouldClonePayload) { @@ -220,8 +235,8 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Initializes the Content Enricher. Will instantiate an internal Gateway if - * the requestChannel is set. + * Initializes the Content Enricher. Will instantiate an internal Gateway if the + * requestChannel is set. */ @Override protected void doInit() { @@ -239,16 +254,16 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem Assert.notNull(this.requestChannel, "If the replyChannel is set, then the requestChannel must not be null"); } if (this.requestChannel != null) { - this.gateway = new Gateway(); - this.gateway.setRequestChannel(requestChannel); + this.gateway = new Gateway(); + this.gateway.setRequestChannel(requestChannel); - if (this.requestTimeout != null) { - this.gateway.setRequestTimeout(this.requestTimeout); - } + if (this.requestTimeout != null) { + this.gateway.setRequestTimeout(this.requestTimeout); + } - if (this.replyTimeout != null) { - this.gateway.setReplyTimeout(this.replyTimeout); - } + if (this.replyTimeout != null) { + this.gateway.setReplyTimeout(this.replyTimeout); + } if (replyChannel != null) { this.gateway.setReplyChannel(replyChannel); @@ -262,25 +277,29 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } if (this.sourceEvaluationContext == null) { - this.sourceEvaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory()); + this.sourceEvaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); } - StandardEvaluationContext targetContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory()); + StandardEvaluationContext targetContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); // bean resolution is NOT allowed for the target of the enrichment targetContext.setBeanResolver(null); this.targetEvaluationContext = targetContext; if (this.getBeanFactory() != null) { - for (HeaderValueMessageProcessor headerValueMessageProcessor : headerExpressions.values()) { - if (headerValueMessageProcessor instanceof BeanFactoryAware) { - ((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(this.getBeanFactory()); - } + for (HeaderValueMessageProcessor headerValueMessageProcessor : this.headerExpressions.values()) { + if (headerValueMessageProcessor instanceof BeanFactoryAware) { + ((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(getBeanFactory()); + } + } + for (HeaderValueMessageProcessor headerValueMessageProcessor : this.nullResultHeaderExpressions.values()) { + if (headerValueMessageProcessor instanceof BeanFactoryAware) { + ((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(getBeanFactory()); + } } } } - @Override protected Object handleRequestMessage(Message requestMessage) { final Object requestPayload = requestMessage.getPayload(); @@ -302,7 +321,8 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem actualRequestMessage = requestMessage; } else { - final Object requestMessagePayload = this.requestPayloadExpression.getValue(this.sourceEvaluationContext, requestMessage); + final Object requestMessagePayload = + this.requestPayloadExpression.getValue(this.sourceEvaluationContext, requestMessage); actualRequestMessage = this.getMessageBuilderFactory().withPayload(requestMessagePayload) .copyHeaders(requestMessage.getHeaders()).build(); } @@ -313,7 +333,35 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem else { replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage); if (replyMessage == null) { - return replyMessage; + if (this.nullResultPropertyExpressions.isEmpty() && this.nullResultHeaderExpressions.isEmpty()) { + return null; + } + for (Map.Entry entry : this.nullResultPropertyExpressions.entrySet()) { + Expression propertyExpression = entry.getKey(); + Expression valueExpression = entry.getValue(); + Object value = valueExpression.getValue(this.sourceEvaluationContext, requestMessage); + propertyExpression.setValue(this.targetEvaluationContext, targetPayload, value); + } + if (this.nullResultHeaderExpressions.isEmpty()) { + return targetPayload; + } + else { + Map targetHeaders = new HashMap( + this.nullResultHeaderExpressions.size()); + for (Map.Entry> entry : this.nullResultHeaderExpressions + .entrySet()) { + String header = entry.getKey(); + HeaderValueMessageProcessor valueProcessor = entry.getValue(); + Boolean overwrite = valueProcessor.isOverwrite(); + overwrite = overwrite != null ? overwrite : true; + if (overwrite || !requestMessage.getHeaders().containsKey(header)) { + Object value = valueProcessor.processMessage(requestMessage); + targetHeaders.put(header, value); + } + } + return this.getMessageBuilderFactory().withPayload(targetPayload).copyHeaders(targetHeaders) + .build(); + } } } for (Map.Entry entry : this.propertyExpressions.entrySet()) { @@ -343,8 +391,8 @@ 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. + * Lifecycle implementation. If no requestChannel is defined, this method has no + * effect as in that case no Gateway is initialized. */ @Override public void start() { @@ -354,8 +402,8 @@ 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. + * Lifecycle implementation. If no requestChannel is defined, this method has no + * effect as in that case no Gateway is initialized. */ @Override public void stop() { @@ -365,22 +413,17 @@ 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. + * 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(); - } - else { - return true; - } + return this.gateway == null || this.gateway.isRunning(); } /** - * Internal gateway implementation for request/reply handling. - * Simply exposes the sendAndReceiveMessage method. + * Internal gateway implementation for request/reply handling. Simply exposes the + * sendAndReceiveMessage method. */ private static final class Gateway extends MessagingGatewaySupport { diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.1.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.1.xsd index 3c81d9806b..e4553b2359 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.1.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.1.xsd @@ -1458,6 +1458,14 @@ Sub-element type for the 'enricher' element. ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+
+
+
+ + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java new file mode 100644 index 0000000000..b6f7a4cd20 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.config.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Liujiong + * + * @since 4.1 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class EnricherParserTests4 { + + @Autowired + private ApplicationContext context; + + private static volatile int adviceCalled; + + @Test + public void nullResultIntegrationTest() { + SubscribableChannel requests = context.getBean("requests", SubscribableChannel.class); + + class NullFoo extends AbstractReplyProducingMessageHandler { + + @Override + protected Object handleRequestMessage(Message requestMessage) { + return null; + } + } + + NullFoo foo = new NullFoo(); + foo.setOutputChannel(context.getBean("replies", MessageChannel.class)); + requests.subscribe(foo); + Target original = new Target(); + Message request = MessageBuilder.withPayload(original).setHeader("sourceName", "test") + .setHeader("notOverwrite", "test").build(); + context.getBean("input", MessageChannel.class).send(request); + Message reply = context.getBean("output", PollableChannel.class).receive(0); + Target enriched = (Target) reply.getPayload(); + assertEquals("Could not determine the name", enriched.getName()); + assertEquals(11, enriched.getAge()); + assertEquals(null, enriched.getGender()); + assertTrue(enriched.isMarried()); + assertNotSame(original, enriched); + assertEquals(1, adviceCalled); + + MessageHeaders headers = reply.getHeaders(); + assertEquals("Could not determine the foo", headers.get("foo")); + assertEquals("Could not determine the testBean", headers.get("testBean")); + assertEquals("Could not determine the sourceName", headers.get("sourceName")); + assertEquals("test", headers.get("notOverwrite")); + adviceCalled--; + requests.unsubscribe(foo); + } + + + public static class Target implements Cloneable { + + private volatile String name; + + private volatile int age; + + private volatile Gender gender; + + private volatile boolean married; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Gender getGender() { + return gender; + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public boolean isMarried() { + return married; + } + + public void setMarried(boolean married) { + this.married = married; + } + + @Override + public Object clone() { + Target copy = new Target(); + copy.setName(this.name); + copy.setAge(this.age); + copy.setGender(this.gender); + copy.setMarried(this.married); + return copy; + } + } + + public static enum Gender { + MALE, FEMALE + } + + public static class FooAdvice extends AbstractRequestHandlerAdvice { + + @Override + protected Object doInvoke(ExecutionCallback callback, Object target, Message message) throws Exception { + adviceCalled++; + return callback.execute(); + } + + } +} 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 f2b774b246..04899044f2 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 @@ -653,7 +653,12 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply session.read(remoteFilePath, outputStream); } catch (Exception e) { + /* Some operation systems acquire exclusive file-lock during file processing + and the file can't be deleted without closing streams before. + */ + outputStream.close(); tempFile.delete(); + if (e instanceof RuntimeException){ throw (RuntimeException) e; } diff --git a/src/reference/docbook/content-enrichment.xml b/src/reference/docbook/content-enrichment.xml index 6c68cf3ab7..74d805c8e1 100644 --- a/src/reference/docbook/content-enrichment.xml +++ b/src/reference/docbook/content-enrichment.xml @@ -273,10 +273,10 @@ send-timeout="" ]]> ]]> ]]> ]]> - ]]> + ]]> + ]]> ]]> @@ -408,7 +408,7 @@ application context (using the '@<beanName>.<beanProperty>' SpEL syntax). - + Starting with 4.0, when specifying a value attribute, you can also specify an optional type attribute. When the destination is a @@ -420,7 +420,12 @@ type attribute allows you to, say, convert a String containing a number to an Integer value in the target payload. - + + + Starting with 4.1, you can also specify an optional + null-result-expression attribute. When the enricher + returns null, it will be evaluated and the output of the evaluation will be returned instead. + @@ -435,12 +440,17 @@ input Message if there is no request channel, or the application context (using the '@<beanName>.<beanProperty>' SpEL syntax). - Note, similar to the <header-enricher>, the <enricher>'s - header element has type and overwrite attributes. - However, a difference is that, with the <enricher>, - the overwrite attribute is true by default, - to be consistent with <enricher>'s - <property> sub-element. + Note, similar to the <header-enricher>, the <enricher>'s + header element has type and overwrite attributes. + However, a difference is that, with the <enricher>, + the overwrite attribute is true by default, + to be consistent with <enricher>'s + <property> sub-element. + + + Starting with 4.1, you can also specify an optional + null-result-expression attribute. When the enricher + returns null, it will be evaluated and the output of the evaluation will be returned instead. diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index a5618c45c3..b9e85f702b 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -89,5 +89,13 @@ See for more information. +
+ Content Enricher Improvements + + Add null-result-expression attribute, which will be evaluated and returned if <enricher> returns null. + It can be added in <header> and <property>. + See for more information. + +