diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayMethodInboundMessageMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayMethodInboundMessageMapper.java index 8d13066eaf..17d9570816 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayMethodInboundMessageMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayMethodInboundMessageMapper.java @@ -242,7 +242,7 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper getMethodParameterList(Method method) { + static List getMethodParameterList(Method method) { List parameterList = new LinkedList<>(); ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); - int parameterCount = method.getParameterTypes().length; - for (int i = 0; i < parameterCount; i++) { + for (int i = 0; i < method.getParameterCount(); i++) { MethodParameter methodParameter = new SynthesizingMethodParameter(method, i); methodParameter.initParameterNameDiscovery(parameterNameDiscoverer); parameterList.add(methodParameter); @@ -307,8 +306,7 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper(); headers.put(MessageHeaders.ERROR_CHANNEL, errorChannel); } + + if (getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory) { + Set headerNames = new HashSet<>(headerExpressions.keySet()); + + if (this.globalMethodMetadata != null) { + headerNames.addAll(this.globalMethodMetadata.getHeaderExpressions().keySet()); + } + + List methodParameters = GatewayMethodInboundMessageMapper.getMethodParameterList(method); + + for (MethodParameter methodParameter : methodParameters) { + Header header = methodParameter.getParameterAnnotation(Header.class); + if (header != null) { + String headerName = GatewayMethodInboundMessageMapper.determineHeaderName(header, methodParameter); + headerNames.add(headerName); + } + } + + for (String header : headerNames) { + if ((MessageHeaders.ID.equals(header) || MessageHeaders.TIMESTAMP.equals(header))) { + throw new BeanInitializationException( + "Messaging Gateway cannot override 'id' and 'timestamp' read-only headers.\n" + + "Wrong headers configuration for " + getComponentName()); + } + } + } + GatewayMethodInboundMessageMapper messageMapper = new GatewayMethodInboundMessageMapper(method, headerExpressions, this.globalMethodMetadata != null ? this.globalMethodMetadata.getHeaderExpressions() : null, diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java b/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java index 360186682d..5b4b3a3596 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java @@ -152,7 +152,12 @@ public final class MessageBuilder extends AbstractIntegrationMessageBuilder removeHeader(String headerName) { - this.headerAccessor.removeHeader(headerName); + if (!this.headerAccessor.isReadOnly(headerName)) { + this.headerAccessor.removeHeader(headerName); + } + else if (logger.isInfoEnabled()) { + logger.info("The header [" + headerName + "] is ignored for removal because it is is readOnly."); + } return this; } 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 8c67e23a1a..c78f8c32bb 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 @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.Lifecycle; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -30,10 +31,12 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.transformer.support.HeaderValueMessageProcessor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -49,22 +52,25 @@ import org.springframework.util.ReflectionUtils; * @author Artem Bilan * @author Liujiong * @author Kris Jacyna + * * @since 2.1 */ -public class ContentEnricher extends AbstractReplyProducingMessageHandler - implements Lifecycle { +public class ContentEnricher extends AbstractReplyProducingMessageHandler implements Lifecycle { + /** + * Customized SpelExpressionParser to allow to specify nested properties when paren is null + */ private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - private volatile Map nullResultPropertyExpressions = new HashMap(); + private volatile Map nullResultPropertyExpressions = new HashMap<>(); private volatile Map> nullResultHeaderExpressions = - new HashMap>(); + new HashMap<>(); - private volatile Map propertyExpressions = new HashMap(); + private volatile Map propertyExpressions = new HashMap<>(); private volatile Map> headerExpressions = - new HashMap>(); + new HashMap<>(); private EvaluationContext sourceEvaluationContext; @@ -93,7 +99,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler private volatile Long replyTimeout; public void setNullResultPropertyExpressions(Map nullResultPropertyExpressions) { - Map localMap = new HashMap(nullResultPropertyExpressions.size()); + Map localMap = new HashMap<>(nullResultPropertyExpressions.size()); for (Map.Entry entry : nullResultPropertyExpressions.entrySet()) { String key = entry.getKey(); Expression value = entry.getValue(); @@ -103,8 +109,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler } public void setNullResultHeaderExpressions(Map> nullResultHeaderExpressions) { - this.nullResultHeaderExpressions = new HashMap>( - nullResultHeaderExpressions); + this.nullResultHeaderExpressions = new HashMap<>(nullResultHeaderExpressions); } /** @@ -117,7 +122,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler Assert.notEmpty(propertyExpressions, "propertyExpressions must not be empty"); Assert.noNullElements(propertyExpressions.keySet().toArray(), "propertyExpressions keys must not be empty"); Assert.noNullElements(propertyExpressions.values().toArray(), "propertyExpressions values must not be empty"); - Map localMap = new HashMap(propertyExpressions.size()); + Map localMap = new HashMap<>(propertyExpressions.size()); for (Map.Entry entry : propertyExpressions.entrySet()) { String key = entry.getKey(); Expression value = entry.getValue(); @@ -137,7 +142,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler Assert.notEmpty(headerExpressions, "headerExpressions must not be empty"); Assert.noNullElements(headerExpressions.keySet().toArray(), "headerExpressions keys must not be empty"); Assert.noNullElements(headerExpressions.values().toArray(), "headerExpressions values must not be empty"); - this.headerExpressions = new HashMap>(headerExpressions); + this.headerExpressions = new HashMap<>(headerExpressions); } /** @@ -316,19 +321,35 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler targetContext.setBeanResolver(null); this.targetEvaluationContext = targetContext; - if (this.getBeanFactory() != null) { - for (HeaderValueMessageProcessor headerValueMessageProcessor : this.headerExpressions.values()) { - if (headerValueMessageProcessor instanceof BeanFactoryAware) { - ((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(getBeanFactory()); + if (getBeanFactory() != 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(getBeanFactory()); } } - for (HeaderValueMessageProcessor headerValueMessageProcessor : this.nullResultHeaderExpressions.values()) { - if (headerValueMessageProcessor instanceof BeanFactoryAware) { - ((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(getBeanFactory()); + + 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(getBeanFactory()); } } } - } @Override diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderEnricher.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderEnricher.java index 2b606490df..5b65bc3718 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderEnricher.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderEnricher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2017 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. @@ -24,12 +24,15 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.integration.context.IntegrationObjectSupport; import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.transformer.support.HeaderValueMessageProcessor; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; /** @@ -112,7 +115,7 @@ public class HeaderEnricher extends IntegrationObjectSupport implements Transfor boolean headerDoesNotExist = headerMap.get(key) == null; - /** + /* * Only evaluate value expression if necessary */ if (headerDoesNotExist || shouldOverwrite) { @@ -155,23 +158,36 @@ public class HeaderEnricher extends IntegrationObjectSupport implements Transfor @Override public void onInit() throws Exception { boolean shouldOverwrite = this.defaultOverwrite; - for (HeaderValueMessageProcessor processor : this.headersToAdd.values()) { - if (processor instanceof BeanFactoryAware && this.getBeanFactory() != null) { - ((BeanFactoryAware) processor).setBeanFactory(this.getBeanFactory()); + boolean checkReadOnlyHeaders = getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory; + + for (Entry> entry : this.headersToAdd.entrySet()) { + if (checkReadOnlyHeaders && + (MessageHeaders.ID.equals(entry.getKey()) || MessageHeaders.TIMESTAMP.equals(entry.getKey()))) { + throw new BeanInitializationException( + "HeaderEnricher cannot override 'id' and 'timestamp' read-only headers.\n" + + "Wrong 'headersToAdd' [" + this.headersToAdd + + "] configuration for " + getComponentName()); + } + + HeaderValueMessageProcessor processor = entry.getValue(); + if (processor instanceof BeanFactoryAware && getBeanFactory() != null) { + ((BeanFactoryAware) processor).setBeanFactory(getBeanFactory()); } Boolean processorOverwrite = processor.isOverwrite(); if (processorOverwrite != null) { shouldOverwrite |= processorOverwrite; } } + if (this.messageProcessor != null && this.messageProcessor instanceof BeanFactoryAware - && this.getBeanFactory() != null) { - ((BeanFactoryAware) this.messageProcessor).setBeanFactory(this.getBeanFactory()); + && getBeanFactory() != null) { + ((BeanFactoryAware) this.messageProcessor).setBeanFactory(getBeanFactory()); } - if (!shouldOverwrite && !this.shouldSkipNulls) { - logger.warn(this.getComponentName() - + " is configured to not overwrite existing headers. 'shouldSkipNulls = false' will have no effect"); + + if (!shouldOverwrite && !this.shouldSkipNulls && logger.isWarnEnabled()) { + logger.warn(getComponentName() + + " is configured to not overwrite existing headers. 'shouldSkipNulls = false' will have no effect"); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderFilter.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderFilter.java index e8e14f35e4..81d6ec12e4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderFilter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/HeaderFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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,14 @@ package org.springframework.integration.transformer; +import java.util.Arrays; + +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.integration.context.IntegrationObjectSupport; import org.springframework.integration.support.AbstractIntegrationMessageBuilder; +import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; /** @@ -27,6 +32,8 @@ import org.springframework.util.Assert; * @author Mark Fisher * @author Oleg Zhurakousky * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 */ public class HeaderFilter extends IntegrationObjectSupport implements Transformer { @@ -50,6 +57,22 @@ public class HeaderFilter extends IntegrationObjectSupport implements Transforme return "header-filter"; } + @Override + protected void onInit() throws Exception { + super.onInit(); + if (getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory) { + for (String header : this.headersToRemove) { + if (!header.contains("*") + && (MessageHeaders.ID.equals(header) || MessageHeaders.TIMESTAMP.equals(header))) { + throw new BeanInitializationException( + "HeaderFilter cannot remove 'id' and 'timestamp' read-only headers.\n" + + "Wrong 'headersToRemove' [" + Arrays.toString(this.headersToRemove) + + "] configuration for " + getComponentName()); + } + } + } + } + @Override public Message transform(Message message) { AbstractIntegrationMessageBuilder builder = this.getMessageBuilderFactory().fromMessage(message); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayProxyFactoryBeanTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayProxyFactoryBeanTests.java index 7774c1e4d1..b70987e5c7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayProxyFactoryBeanTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayProxyFactoryBeanTests.java @@ -16,10 +16,13 @@ package org.springframework.integration.gateway; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -35,6 +38,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -43,6 +47,8 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.annotation.Gateway; +import org.springframework.integration.annotation.GatewayHeader; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.endpoint.EventDrivenConsumer; @@ -50,7 +56,9 @@ import org.springframework.integration.support.utils.IntegrationUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.support.GenericMessage; import org.springframework.util.ReflectionUtils; @@ -83,6 +91,7 @@ public class GatewayProxyFactoryBeanTests { startResponder(requestChannel); GenericConversionService cs = new DefaultConversionService(); Converter stringToByteConverter = new Converter() { + @Override public byte[] convert(String source) { return source.getBytes(); @@ -312,6 +321,7 @@ public class GatewayProxyFactoryBeanTests { GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean(); DirectChannel channel = new DirectChannel(); EventDrivenConsumer consumer = new EventDrivenConsumer(channel, new MessageHandler() { + @Override public void handleMessage(Message message) { Method method = ReflectionUtils.findMethod( @@ -358,6 +368,57 @@ public class GatewayProxyFactoryBeanTests { assertThat(bar, equalTo("bar")); } + @Test + public void testIdHeaderOverrideHeaderExpression() { + GatewayProxyFactoryBean gpfb = new GatewayProxyFactoryBean(); + gpfb.setBeanFactory(mock(BeanFactory.class)); + + GatewayMethodMetadata meta = new GatewayMethodMetadata(); + meta.setHeaderExpressions(Collections.singletonMap(MessageHeaders.ID, new LiteralExpression("bar"))); + gpfb.setGlobalMethodMetadata(meta); + + try { + gpfb.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("Messaging Gateway cannot override 'id' and 'timestamp' read-only headers")); + } + } + + @Test + public void testIdHeaderOverrideGatewayHeaderAnnotation() { + GatewayProxyFactoryBean gpfb = new GatewayProxyFactoryBean(); + gpfb.setBeanFactory(mock(BeanFactory.class)); + gpfb.setServiceInterface(HeadersOverwriteService.class); + + try { + gpfb.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("Messaging Gateway cannot override 'id' and 'timestamp' read-only headers")); + } + } + + @Test + public void testTimeStampHeaderOverrideParamHeaderAnnotation() { + GatewayProxyFactoryBean gpfb = new GatewayProxyFactoryBean(); + gpfb.setBeanFactory(mock(BeanFactory.class)); + gpfb.setServiceInterface(HeadersParamService.class); + + try { + gpfb.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("Messaging Gateway cannot override 'id' and 'timestamp' read-only headers")); + } + } + // @Test // public void testHistory() throws Exception { // GenericApplicationContext context = new GenericApplicationContext(); @@ -413,6 +474,18 @@ public class GatewayProxyFactoryBeanTests { } + interface HeadersOverwriteService { + + @Gateway(headers = @GatewayHeader(name = MessageHeaders.ID, value = "id")) + Message echo(String s); + } + + interface HeadersParamService { + + Message echo(String s, @Header(MessageHeaders.TIMESTAMP) String foo); + } + + interface TestExceptionThrowingInterface { String throwCheckedException(String s) throws TestException; @@ -421,6 +494,7 @@ public class GatewayProxyFactoryBeanTests { @SuppressWarnings("serial") static class TestException extends Exception { + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingHeaderEnricherTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingHeaderEnricherTests.java index f38d84a188..d4bf100052 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingHeaderEnricherTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingHeaderEnricherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2017 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,20 +16,30 @@ package org.springframework.integration.handler; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.Test; -import org.springframework.messaging.Message; -import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.transformer.HeaderEnricher; +import org.springframework.integration.transformer.support.StaticHeaderValueMessageProcessor; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.Payload; /** * @author Mark Fisher + * @author Artem Bilan + * * @since 2.0 */ public class MethodInvokingHeaderEnricherTests { @@ -89,6 +99,21 @@ public class MethodInvokingHeaderEnricherTests { assertEquals("ABC", result.getHeaders().get("bar")); } + @Test + public void overwriteId() { + HeaderEnricher enricher = + new HeaderEnricher(Collections.singletonMap(MessageHeaders.ID, + new StaticHeaderValueMessageProcessor<>("foo"))); + + try { + enricher.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("HeaderEnricher cannot override 'id' and 'timestamp' read-only headers.")); + } + } public static class TestBean { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java index 4e5eeb8d39..8ee909f1fc 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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,6 +18,7 @@ package org.springframework.integration.transformer; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; @@ -27,6 +28,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -34,6 +36,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -47,9 +50,11 @@ import org.springframework.integration.endpoint.PollingConsumer; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.handler.ReplyRequiredException; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.transformer.support.StaticHeaderValueMessageProcessor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessageHeaders; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.PeriodicTrigger; @@ -87,7 +92,7 @@ public class ContentEnricherTests { public void replyChannelReplyTimingOut() throws Exception { final long requestTimeout = 500L; - final long replyTimeout = 700L; + final long replyTimeout = 100L; final DirectChannel replyChannel = new DirectChannel(); final QueueChannel requestChannel = new QueueChannel(1); @@ -521,6 +526,40 @@ public class ContentEnricherTests { assertEquals("failed target", result.getName()); } + @Test + public void testOverwriteTimestamp() { + ContentEnricher contentEnricher = new ContentEnricher(); + contentEnricher.setHeaderExpressions( + Collections.singletonMap(MessageHeaders.TIMESTAMP, new StaticHeaderValueMessageProcessor<>("foo"))); + + contentEnricher.setBeanFactory(mock(BeanFactory.class)); + try { + contentEnricher.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("ContentEnricher cannot override 'id' and 'timestamp' read-only headers.")); + } + } + + @Test + public void testOverwriteIdNullResult() { + ContentEnricher contentEnricher = new ContentEnricher(); + contentEnricher.setNullResultHeaderExpressions( + Collections.singletonMap(MessageHeaders.ID, new StaticHeaderValueMessageProcessor<>("foo"))); + + contentEnricher.setBeanFactory(mock(BeanFactory.class)); + try { + contentEnricher.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("ContentEnricher cannot override 'id' and 'timestamp' read-only headers.")); + } + } + @SuppressWarnings("unused") private static final class Source { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/HeaderFilterTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/HeaderFilterTests.java index 9a7a5c17b0..43dd2453d7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/HeaderFilterTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/HeaderFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2017 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,24 +16,35 @@ package org.springframework.integration.transformer; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; 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.fail; import static org.mockito.Mockito.mock; +import static org.springframework.integration.test.matcher.HeaderMatcher.hasHeaderKey; +import java.util.Date; import java.util.UUID; import org.junit.Test; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; /** * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 */ public class HeaderFilterTests { @@ -76,4 +87,60 @@ public class HeaderFilterTests { assertEquals(correlationId, new IntegrationMessageHeaderAccessor(result).getCorrelationId()); } + @Test + public void testIdHeaderRemoval() { + HeaderFilter filter = new HeaderFilter("foo", MessageHeaders.ID); + try { + filter.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("HeaderFilter cannot remove 'id' and 'timestamp' read-only headers.")); + } + } + + @Test + public void testTimestampHeaderRemoval() { + HeaderFilter filter = new HeaderFilter(MessageHeaders.TIMESTAMP); + try { + filter.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("HeaderFilter cannot remove 'id' and 'timestamp' read-only headers.")); + } + } + + @Test + public void testIdPatternRemoval() { + HeaderFilter filter = new HeaderFilter("*", MessageHeaders.ID); + filter.setPatternMatch(true); + try { + filter.afterPropertiesSet(); + fail("BeanInitializationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanInitializationException.class)); + assertThat(e.getMessage(), containsString("HeaderFilter cannot remove 'id' and 'timestamp' read-only headers.")); + } + } + + @Test + public void testPatternRemoval() { + HeaderFilter filter = new HeaderFilter("time*"); + filter.setPatternMatch(true); + + filter.afterPropertiesSet(); + Message message = MessageBuilder.withPayload("test") + .setHeader("time", new Date()) + .build(); + + Message result = filter.transform(message); + + assertThat(result, hasHeaderKey(MessageHeaders.TIMESTAMP)); + assertThat(result, not(hasHeaderKey("time"))); + } + } diff --git a/src/reference/asciidoc/message.adoc b/src/reference/asciidoc/message.adoc index a4b83fd8c4..3182253c9f 100644 --- a/src/reference/asciidoc/message.adoc +++ b/src/reference/asciidoc/message.adoc @@ -374,9 +374,16 @@ In addition to the default strategy, two additional `IdGenerators` are provided; ===== Read-only Headers The `MessageHeaders.ID` and `MessageHeaders.TIMESTAMP` are read-only headers and the cannot be overridden. -With the `spring.integration.readOnly.headers` global property (see <>) you can specify any header names which become read-only. + +Since _version 4.3.2_, the `MessageBuilder` provides the `readOnlyHeaders(String... readOnlyHeaders)` API to customize a list of headers which should not be copied from an upstream `Message`. +Just the `MessageHeaders.ID` and `MessageHeaders.TIMESTAMP` are read only by default. +The global `spring.integration.readOnly.headers` property (see <>) is provided to customize `DefaultMessageBuilderFactory` for Framework components. +This can be useful when you would like do not populate some out-of-the-box headers, like `contentType` by the `ObjectToJsonTransformer` (see <>). + When you try to build a new message using `MessageBuilder`, this kind of headers are ignored and particular `INFO` message is emitted to logs. +Starting with _version 5.0_, <>, <>, <> and <> don't allow to configure `MessageHeaders.ID` and `MessageHeaders.TIMESTAMP` header names when `DefaultMessageBuilderFactory` is used and they throw `BeanInitializationException`. + [[message-implementations]] ==== Message Implementations @@ -468,8 +475,3 @@ assertEquals(2, lessImportantMessage.getHeaders().getPriority()); The `priority` header is only considered when using a `PriorityChannel` (as described in the next chapter). It is defined as _java.lang.Integer_. - -Since _version 4.3.2_, the `MessageBuilder` provides the `readOnlyHeaders(String... readOnlyHeaders)` API to customize a list of headers which should not be copied from an upstream `Message`. -Just the `MessageHeaders.ID` and `MessageHeaders.TIMESTAMP` are read only by default. -The global `spring.integration.readOnly.headers` property (see <>) is provided to customize `DefaultMessageBuilderFactory` for Framework components. -This can be useful when you would like do not populate some out-of-the-box headers, like `contentType` by the `ObjectToJsonTransformer` (see <>).