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 2177d10423..63287f267b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanInitializationException; @@ -32,9 +34,11 @@ import org.springframework.integration.IntegrationPatternType; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.management.ManageableLifecycle; import org.springframework.integration.transformer.support.HeaderValueMessageProcessor; +import org.springframework.integration.util.JavaUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandlingException; @@ -60,9 +64,10 @@ import org.springframework.util.ReflectionUtils; public class ContentEnricher extends AbstractReplyProducingMessageHandler implements ManageableLifecycle { /** - * Customized SpelExpressionParser to allow to specify nested properties when paren is null + * Customized SpelExpressionParser to allow to specify nested properties when parent is null */ - private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + private static final SpelExpressionParser SPEL_PARSER = + new SpelExpressionParser(new SpelParserConfiguration(true, true)); private Map nullResultPropertyExpressions = new HashMap<>(); @@ -102,16 +107,6 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem this.nullResultPropertyExpressions = convertExpressions(nullResultPropertyExpressions); } - private Map convertExpressions(Map expressions) { - Map localMap = new HashMap<>(expressions.size()); - for (Map.Entry entry : expressions.entrySet()) { - String key = entry.getKey(); - Expression value = entry.getValue(); - localMap.put(this.parser.parseExpression(key), value); - } - return localMap; - } - public void setNullResultHeaderExpressions(Map> nullResultHeaderExpressions) { this.nullResultHeaderExpressions = new HashMap<>(nullResultHeaderExpressions); } @@ -144,7 +139,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Sets the content enricher's request channel. If specified, then an internal Gateway + * Set the content enricher 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. @@ -160,7 +155,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Sets the content enricher's reply channel. If not specified, yet the request + * Set the content enricher reply channel. If not specified, yet the request * channel is set, an anonymous reply channel will automatically created for each * request. * @param replyChannel The reply channel. @@ -175,7 +170,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Set the content enricher's error channel to allow the error handling flow to return + * Set the content enricher error channel to allow the error handling flow to return * of an alternative object to use for enrichment if exceptions occur in the * downstream flow. * @param errorChannel The error channel. @@ -261,11 +256,47 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } /** - * Initializes the Content Enricher. Will instantiate an internal Gateway if the + * Initialize the Content Enricher. Will instantiate an internal Gateway if the * requestChannel is set. */ @Override protected void doInit() { + validateConfiguration(); + + BeanFactory beanFactory = getBeanFactory(); + + if (this.requestChannel != null || this.requestChannelName != null) { + this.gateway = new Gateway(); + + JavaUtils.INSTANCE + .acceptIfNotNull(this.requestChannel, this.gateway::setRequestChannel) + .acceptIfNotNull(this.requestChannelName, this.gateway::setRequestChannelName) + .acceptIfNotNull(this.requestTimeout, this.gateway::setRequestTimeout) + .acceptIfNotNull(this.replyTimeout, this.gateway::setReplyTimeout) + .acceptIfNotNull(this.replyChannel, this.gateway::setReplyChannel) + .acceptIfNotNull(this.replyChannelName, this.gateway::setReplyChannelName) + .acceptIfNotNull(this.errorChannel, this.gateway::setErrorChannel) + .acceptIfNotNull(this.errorChannelName, this.gateway::setErrorChannelName) + .acceptIfNotNull(beanFactory, this.gateway::setBeanFactory); + + this.gateway.afterPropertiesSet(); + } + + if (this.sourceEvaluationContext == null) { + this.sourceEvaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory); + } + + StandardEvaluationContext targetContext = ExpressionUtils.createStandardEvaluationContext(beanFactory); + // bean resolution is NOT allowed for the target of the enrichment + targetContext.setBeanResolver(null); // NOSONAR (null) + this.targetEvaluationContext = targetContext; + + if (beanFactory != null) { + configureHeaderExpressions(beanFactory); + } + } + + private void validateConfiguration() { Assert.state(!(this.requestChannelName != null && this.requestChannel != null), "'requestChannelName' and 'requestChannel' are mutually exclusive."); @@ -283,150 +314,55 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem Assert.state(this.requestChannel != null || this.requestChannelName != null, "If the errorChannel is set, then the requestChannel must not be null"); } + } - BeanFactory beanFactory = getBeanFactory(); + private void configureHeaderExpressions(BeanFactory beanFactory) { + boolean checkReadOnlyHeaders = getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory; - if (this.requestChannel != null || this.requestChannelName != null) { - this.gateway = new Gateway(); - if (this.requestChannel != null) { - this.gateway.setRequestChannel(this.requestChannel); + for (Map.Entry> entry : this.headerExpressions.entrySet()) { + if (checkReadOnlyHeaders && + (MessageHeaders.ID.equals(entry.getKey()) || MessageHeaders.TIMESTAMP.equals(entry.getKey()))) { + throw new BeanInitializationException( + "ContentEnricher cannot override 'id' and 'timestamp' read-only headers.\n" + + "Wrong 'headerExpressions' [" + this.headerExpressions + + "] configuration for " + getComponentName()); } - if (this.requestChannelName != null) { - this.gateway.setRequestChannelName(this.requestChannelName); + if (entry.getValue() instanceof BeanFactoryAware) { + ((BeanFactoryAware) entry.getValue()).setBeanFactory(beanFactory); } - - if (this.requestTimeout != null) { - this.gateway.setRequestTimeout(this.requestTimeout); - } - if (this.replyTimeout != null) { - this.gateway.setReplyTimeout(this.replyTimeout); - } - - this.gateway.setReplyChannel(this.replyChannel); - if (this.replyChannelName != null) { - this.gateway.setReplyChannelName(this.replyChannelName); - } - - this.gateway.setErrorChannel(this.errorChannel); - if (this.errorChannelName != null) { - this.gateway.setErrorChannelName(this.errorChannelName); - } - - if (beanFactory != null) { - this.gateway.setBeanFactory(beanFactory); - } - - this.gateway.afterPropertiesSet(); } - - if (this.sourceEvaluationContext == null) { - this.sourceEvaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory); - } - - StandardEvaluationContext targetContext = ExpressionUtils.createStandardEvaluationContext(beanFactory); - // bean resolution is NOT allowed for the target of the enrichment - targetContext.setBeanResolver(null); // NOSONAR (null) - this.targetEvaluationContext = targetContext; - - if (beanFactory != null) { - boolean checkReadOnlyHeaders = getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory; - - for (Map.Entry> entry : this.headerExpressions.entrySet()) { - if (checkReadOnlyHeaders && - (MessageHeaders.ID.equals(entry.getKey()) || MessageHeaders.TIMESTAMP.equals(entry.getKey()))) { - throw new BeanInitializationException( - "ContentEnricher cannot override 'id' and 'timestamp' read-only headers.\n" + - "Wrong 'headerExpressions' [" + this.headerExpressions - + "] configuration for " + getComponentName()); - } - if (entry.getValue() instanceof BeanFactoryAware) { - ((BeanFactoryAware) entry.getValue()).setBeanFactory(beanFactory); - } + for (Map.Entry> entry : + this.nullResultHeaderExpressions.entrySet()) { + if (checkReadOnlyHeaders && + (MessageHeaders.ID.equals(entry.getKey()) || MessageHeaders.TIMESTAMP.equals(entry.getKey()))) { + throw new BeanInitializationException( + "ContentEnricher cannot override 'id' and 'timestamp' read-only headers.\n" + + "Wrong 'nullResultHeaderExpressions' [" + this.nullResultHeaderExpressions + + "] configuration for " + getComponentName()); } - - for (Map.Entry> entry : - this.nullResultHeaderExpressions.entrySet()) { - if (checkReadOnlyHeaders && - (MessageHeaders.ID.equals(entry.getKey()) || MessageHeaders.TIMESTAMP.equals(entry.getKey()))) { - throw new BeanInitializationException( - "ContentEnricher cannot override 'id' and 'timestamp' read-only headers.\n" + - "Wrong 'nullResultHeaderExpressions' [" + this.nullResultHeaderExpressions - + "] configuration for " + getComponentName()); - } - if (entry.getValue() instanceof BeanFactoryAware) { - ((BeanFactoryAware) entry.getValue()).setBeanFactory(beanFactory); - } + if (entry.getValue() instanceof BeanFactoryAware) { + ((BeanFactoryAware) entry.getValue()).setBeanFactory(beanFactory); } } } @Override protected Object handleRequestMessage(Message requestMessage) { - final Object requestPayload = requestMessage.getPayload(); - final Object targetPayload; - if (requestPayload instanceof Cloneable && this.shouldClonePayload) { - try { - Method cloneMethod = requestPayload.getClass().getMethod("clone"); - targetPayload = ReflectionUtils.invokeMethod(cloneMethod, requestPayload); - } - catch (Exception e) { - throw new MessageHandlingException(requestMessage, - "Failed to clone payload object in the [" + this + ']', e); - } - } - else { - targetPayload = requestPayload; - } - final Message actualRequestMessage; - if (this.requestPayloadExpression == null) { - actualRequestMessage = requestMessage; - } - else { - final Object requestMessagePayload = - this.requestPayloadExpression.getValue(this.sourceEvaluationContext, requestMessage); - Assert.state(requestMessagePayload != null, - () -> "Request payload expression produced null for " + requestMessage); - actualRequestMessage = getMessageBuilderFactory().withPayload(requestMessagePayload) - .copyHeaders(requestMessage.getHeaders()).build(); - } - final Message replyMessage; + Object targetPayload = prepareTargetPayload(requestMessage); + Message actualRequestMessage = prepareActualRequestMessage(requestMessage); + + Message replyMessage; if (this.gateway == null) { replyMessage = actualRequestMessage; } else { replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage); if (replyMessage == null) { - 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(); - } + return processNullReply(requestMessage, targetPayload); } } + for (Map.Entry entry : this.propertyExpressions.entrySet()) { Expression propertyExpression = entry.getKey(); Expression valueExpression = entry.getValue(); @@ -438,23 +374,84 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem return targetPayload; } else { - Map targetHeaders = new HashMap<>(this.headerExpressions.size()); - for (Map.Entry> entry : this.headerExpressions.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(replyMessage); - targetHeaders.put(header, value); - } - } - return getMessageBuilderFactory() - .withPayload(targetPayload) - .copyHeaders(targetHeaders); + return buildReplyWithHeaders(replyMessage, targetPayload, this.headerExpressions); } } + private Object prepareTargetPayload(Message requestMessage) { + Object requestPayload = requestMessage.getPayload(); + if (requestPayload instanceof Cloneable && this.shouldClonePayload) { + try { + Method cloneMethod = requestPayload.getClass().getMethod("clone"); + Object result = ReflectionUtils.invokeMethod(cloneMethod, requestPayload); + Assert.notNull(result, "The 'clone()' method cannot return null for content enricher"); + return result; + } + catch (Exception ex) { + throw new MessageHandlingException(requestMessage, + "Failed to clone payload object in the [" + this + ']', ex); + } + } + return requestPayload; + } + + private Message prepareActualRequestMessage(Message requestMessage) { + if (this.requestPayloadExpression != null) { + Object requestMessagePayload = + this.requestPayloadExpression.getValue(this.sourceEvaluationContext, requestMessage); + Assert.state(requestMessagePayload != null, + () -> "Request payload expression produced null for " + requestMessage); + return getMessageBuilderFactory() + .withPayload(requestMessagePayload) + .copyHeaders(requestMessage.getHeaders()) + .build(); + } + return requestMessage; + } + + @Nullable + private Object processNullReply(Message requestMessage, Object targetPayload) { + 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 { + return buildReplyWithHeaders(requestMessage, targetPayload, this.nullResultHeaderExpressions); + + } + } + + private AbstractIntegrationMessageBuilder buildReplyWithHeaders(Message message, + Object targetPayload, Map> headerExpressions) { + + Map targetHeaders = new HashMap<>(headerExpressions.size()); + for (Map.Entry> entry : headerExpressions.entrySet()) { + String header = entry.getKey(); + HeaderValueMessageProcessor valueProcessor = entry.getValue(); + Boolean overwrite = valueProcessor.isOverwrite(); + overwrite = overwrite != null ? overwrite : true; + if (overwrite || !message.getHeaders().containsKey(header)) { + Object value = valueProcessor.processMessage(message); + targetHeaders.put(header, value); + } + } + + return getMessageBuilderFactory() + .withPayload(targetPayload) + .copyHeaders(targetHeaders); + } + + /** * Lifecycle implementation. If no requestChannel is defined, this method has no * effect as in that case no Gateway is initialized. @@ -486,6 +483,16 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem return this.gateway == null || this.gateway.isRunning(); } + private static Map convertExpressions(Map expressions) { + Map localMap = new HashMap<>(expressions.size()); + for (Map.Entry entry : expressions.entrySet()) { + String key = entry.getKey(); + Expression value = entry.getValue(); + localMap.put(SPEL_PARSER.parseExpression(key), value); + } + return localMap; + } + /** * Internal gateway implementation for request/reply handling. Simply exposes the * sendAndReceiveMessage method.