diff --git a/build.gradle b/build.gradle index d72b0c1c1c..5215ec4fb9 100644 --- a/build.gradle +++ b/build.gradle @@ -141,7 +141,7 @@ subprojects { subproject -> springSecurityVersion = '5.1.0.BUILD-SNAPSHOT' springSocialTwitterVersion = '1.1.2.RELEASE' springRetryVersion = '1.2.2.RELEASE' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.0.4.RELEASE' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.0.5.BUILD-SNAPSHOT' springWsVersion = '3.0.1.RELEASE' tomcatVersion = "8.5.23" xmlUnitVersion = '1.6' diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/AbstractEvaluationContextFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/AbstractEvaluationContextFactoryBean.java new file mode 100644 index 0000000000..98aab74566 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/AbstractEvaluationContextFactoryBean.java @@ -0,0 +1,143 @@ +/* + * Copyright 2018 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; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.integration.expression.SpelPropertyAccessorRegistrar; +import org.springframework.integration.support.utils.IntegrationUtils; +import org.springframework.util.Assert; + +/** + * Abstract class for integration evaluation context factory beans. + * + * @author Gary Russell + * + * @since 4.3.15 + * + */ +public abstract class AbstractEvaluationContextFactoryBean implements ApplicationContextAware, InitializingBean { + + private Map propertyAccessors = new LinkedHashMap(); + + private Map functions = new LinkedHashMap(); + + private TypeConverter typeConverter = new StandardTypeConverter(); + + private ApplicationContext applicationContext; + + private boolean initialized; + + protected TypeConverter getTypeConverter() { + return this.typeConverter; + } + + protected ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public void setPropertyAccessors(Map accessors) { + Assert.isTrue(!this.initialized, "'propertyAccessors' can't be changed after initialization."); + Assert.notNull(accessors, "'accessors' must not be null."); + Assert.noNullElements(accessors.values().toArray(), "'accessors' cannot have null values."); + this.propertyAccessors = new LinkedHashMap(accessors); + } + + public Map getPropertyAccessors() { + return this.propertyAccessors; + } + + public void setFunctions(Map functionsArg) { + Assert.isTrue(!this.initialized, "'functions' can't be changed after initialization."); + Assert.notNull(functionsArg, "'functions' must not be null."); + Assert.noNullElements(functionsArg.values().toArray(), "'functions' cannot have null values."); + this.functions = new LinkedHashMap(functionsArg); + } + + public Map getFunctions() { + return this.functions; + } + + protected void initialize(String beanName) throws Exception { + if (this.applicationContext != null) { + ConversionService conversionService = IntegrationUtils.getConversionService(getApplicationContext()); + if (conversionService != null) { + this.typeConverter = new StandardTypeConverter(conversionService); + } + + Map functionFactoryBeanMap = BeanFactoryUtils + .beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class); + for (SpelFunctionFactoryBean spelFunctionFactoryBean : functionFactoryBeanMap.values()) { + if (!getFunctions().containsKey(spelFunctionFactoryBean.getFunctionName())) { + getFunctions().put(spelFunctionFactoryBean.getFunctionName(), spelFunctionFactoryBean.getObject()); + } + } + + try { + SpelPropertyAccessorRegistrar propertyAccessorRegistrar = + this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class); + for (Entry entry : propertyAccessorRegistrar.getPropertyAccessors() + .entrySet()) { + if (!getPropertyAccessors().containsKey(entry.getKey())) { + getPropertyAccessors().put(entry.getKey(), entry.getValue()); + } + } + } + catch (NoSuchBeanDefinitionException e) { + // There is no 'SpelPropertyAccessorRegistrar' bean in the application context. + } + + ApplicationContext parent = this.applicationContext.getParent(); + + if (parent != null && parent.containsBean(beanName)) { + AbstractEvaluationContextFactoryBean parentFactoryBean = parent.getBean("&" + beanName, getClass()); + + for (Entry entry : parentFactoryBean.getPropertyAccessors().entrySet()) { + if (!getPropertyAccessors().containsKey(entry.getKey())) { + getPropertyAccessors().put(entry.getKey(), entry.getValue()); + } + } + + for (Entry entry : parentFactoryBean.getFunctions().entrySet()) { + if (!getFunctions().containsKey(entry.getKey())) { + getFunctions().put(entry.getKey(), entry.getValue()); + } + } + } + } + this.initialized = true; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java index b36e5ce6a4..f9bed95651 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * Copyright 2013-2018 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,17 +17,9 @@ package org.springframework.integration.config; import java.lang.reflect.Method; -import java.util.LinkedHashMap; -import java.util.Map; import java.util.Map.Entry; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.core.convert.ConversionService; @@ -36,11 +28,8 @@ import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypeConverter; import org.springframework.expression.TypeLocator; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.expression.SpelPropertyAccessorRegistrar; -import org.springframework.integration.support.utils.IntegrationUtils; -import org.springframework.util.Assert; /** *

@@ -73,107 +62,28 @@ import org.springframework.util.Assert; * * @since 3.0 */ -public class IntegrationEvaluationContextFactoryBean implements FactoryBean, - ApplicationContextAware, InitializingBean { - - private volatile Map propertyAccessors = new LinkedHashMap(); - - private volatile Map functions = new LinkedHashMap(); - - private TypeConverter typeConverter = new StandardTypeConverter(); +public class IntegrationEvaluationContextFactoryBean extends AbstractEvaluationContextFactoryBean + implements FactoryBean { private volatile TypeLocator typeLocator; private BeanResolver beanResolver; - private ApplicationContext applicationContext; - - private volatile boolean initialized; - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - public void setPropertyAccessors(Map accessors) { - Assert.isTrue(!this.initialized, "'propertyAccessors' can't be changed after initialization."); - Assert.notNull(accessors, "'accessors' must not be null."); - Assert.noNullElements(accessors.values().toArray(), "'accessors' cannot have null values."); - this.propertyAccessors = new LinkedHashMap(accessors); - } - - public Map getPropertyAccessors() { - return this.propertyAccessors; - } - - public void setFunctions(Map functionsArg) { - Assert.isTrue(!this.initialized, "'functions' can't be changed after initialization."); - Assert.notNull(functionsArg, "'functions' must not be null."); - Assert.noNullElements(functionsArg.values().toArray(), "'functions' cannot have null values."); - this.functions = new LinkedHashMap(functionsArg); - } - - public Map getFunctions() { - return this.functions; - } - public void setTypeLocator(TypeLocator typeLocator) { this.typeLocator = typeLocator; } + @Override + public boolean isSingleton() { + return false; + } @Override public void afterPropertiesSet() throws Exception { - if (this.applicationContext != null) { - this.beanResolver = new BeanFactoryResolver(this.applicationContext); - ConversionService conversionService = IntegrationUtils.getConversionService(this.applicationContext); - if (conversionService != null) { - this.typeConverter = new StandardTypeConverter(conversionService); - } - - Map functionFactoryBeanMap = - BeanFactoryUtils.beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class); - for (SpelFunctionFactoryBean spelFunctionFactoryBean : functionFactoryBeanMap.values()) { - if (!this.functions.containsKey(spelFunctionFactoryBean.getFunctionName())) { - this.functions.put(spelFunctionFactoryBean.getFunctionName(), spelFunctionFactoryBean.getObject()); - } - } - - try { - SpelPropertyAccessorRegistrar propertyAccessorRegistrar = - this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class); - for (Entry entry : propertyAccessorRegistrar.getPropertyAccessors().entrySet()) { - if (!this.propertyAccessors.containsKey(entry.getKey())) { - this.propertyAccessors.put(entry.getKey(), entry.getValue()); - } - } - } - catch (NoSuchBeanDefinitionException e) { - // There is no 'SpelPropertyAccessorRegistrar' bean in the application context. - } - - ApplicationContext parent = this.applicationContext.getParent(); - - if (parent != null && parent.containsBean(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME)) { - IntegrationEvaluationContextFactoryBean parentFactoryBean = - parent.getBean("&" + IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, - IntegrationEvaluationContextFactoryBean.class); - - for (Entry entry : parentFactoryBean.getPropertyAccessors().entrySet()) { - if (!this.propertyAccessors.containsKey(entry.getKey())) { - this.propertyAccessors.put(entry.getKey(), entry.getValue()); - } - } - - for (Entry entry : parentFactoryBean.getFunctions().entrySet()) { - if (!this.functions.containsKey(entry.getKey())) { - this.functions.put(entry.getKey(), entry.getValue()); - } - } - } + if (getApplicationContext() != null) { + this.beanResolver = new BeanFactoryResolver(getApplicationContext()); } - - this.initialized = true; + initialize(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME); } @Override @@ -184,15 +94,15 @@ public class IntegrationEvaluationContextFactoryBean implements FactoryBean functionEntry : this.functions.entrySet()) { + for (Entry functionEntry : getFunctions().entrySet()) { evaluationContext.registerFunction(functionEntry.getKey(), functionEntry.getValue()); } @@ -204,9 +114,4 @@ public class IntegrationEvaluationContextFactoryBean implements FactoryBean + * {@link FactoryBean} to populate {@link SimpleEvaluationContext} instances enhanced with: + *

    + *
  • + * a {@link TypeConverter} based on the {@link ConversionService} from the application context. + *
  • + *
  • + * a set of provided {@link PropertyAccessor}s including a default {@link MapAccessor}. + *
  • + *
  • + * a set of provided SpEL functions. + *
  • + *
+ *

+ * After initialization this factory populates functions and property accessors from + * {@link SpelFunctionFactoryBean}s and {@link SpelPropertyAccessorRegistrar}, respectively. + * Functions and property accessors are also inherited from any parent context. + *

+ *

+ * This factory returns a new instance for each reference - {@link #isSingleton()} returns false. + *

+ * + * @author Artem Bilan + * @author Gary Russell + * + * @since 4.3.15 + */ +public class IntegrationSimpleEvaluationContextFactoryBean extends AbstractEvaluationContextFactoryBean + implements FactoryBean { + + @Override + public boolean isSingleton() { + return false; + } + + @Override + public void afterPropertiesSet() throws Exception { + initialize(IntegrationContextUtils.INTEGRATION_SIMPLE_EVALUATION_CONTEXT_BEAN_NAME); + } + + @Override + public SimpleEvaluationContext getObject() throws Exception { + Collection accessors = getPropertyAccessors().values(); + PropertyAccessor[] accessorArray = accessors.toArray(new PropertyAccessor[accessors.size() + 2]); + accessorArray[accessors.size()] = new MapAccessor(); + accessorArray[accessors.size() + 1] = DataBindingPropertyAccessor.forReadOnlyAccess(); + SimpleEvaluationContext evaluationContext = + SimpleEvaluationContext.forPropertyAccessors(accessorArray) + .withTypeConverter(getTypeConverter()) + .withInstanceMethods() + .build(); + for (Entry functionEntry : getFunctions().entrySet()) { + evaluationContext.setVariable(functionEntry.getKey(), functionEntry.getValue()); + } + return evaluationContext; + } + + @Override + public Class getObjectType() { + return SimpleEvaluationContext.class; + } + +} 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 0e9ac1fc1f..8d0030eab3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.integration.context; import java.util.Properties; import org.springframework.beans.factory.BeanFactory; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.config.IntegrationConfigUtils; import org.springframework.integration.metadata.MetadataStore; @@ -48,6 +49,8 @@ public abstract class IntegrationContextUtils { public static final String INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME = "integrationEvaluationContext"; + public static final String INTEGRATION_SIMPLE_EVALUATION_CONTEXT_BEAN_NAME = "integrationSimpleEvaluationContext"; + public static final String INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME = "integrationHeaderChannelRegistry"; public static final String INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME = "integrationGlobalProperties"; @@ -131,12 +134,24 @@ public abstract class IntegrationContextUtils { /** * @param beanFactory BeanFactory for lookup, must not be null. - * @return the instance of {@link StandardEvaluationContext} bean whose name is "integrationEvaluationContext" . + * @return the instance of {@link StandardEvaluationContext} bean whose name is + * {@value #INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME}. */ public static StandardEvaluationContext getEvaluationContext(BeanFactory beanFactory) { return getBeanOfType(beanFactory, INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, StandardEvaluationContext.class); } + /** + * @param beanFactory BeanFactory for lookup, must not be null. + * @return the instance of {@link SimpleEvaluationContext} bean whose name is + * {@value #INTEGRATION_SIMPLE_EVALUATION_CONTEXT_BEAN_NAME}. + * @since 4.3.15 + */ + public static SimpleEvaluationContext getSimpleEvaluationContext(BeanFactory beanFactory) { + return getBeanOfType(beanFactory, INTEGRATION_SIMPLE_EVALUATION_CONTEXT_BEAN_NAME, + SimpleEvaluationContext.class); + } + private static T getBeanOfType(BeanFactory beanFactory, String beanName, Class type) { Assert.notNull(beanFactory, "BeanFactory must not be null"); if (!beanFactory.containsBean(beanName)) { @@ -160,7 +175,8 @@ public abstract class IntegrationContextUtils { Properties properties = new Properties(); properties.putAll(IntegrationProperties.defaults()); if (beanFactory != null) { - Properties userProperties = getBeanOfType(beanFactory, INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME, Properties.class); + Properties userProperties = + getBeanOfType(beanFactory, INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME, Properties.class); if (userProperties != null) { properties.putAll(userProperties); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionUtils.java index 8012c54708..ad41aa4a94 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -29,6 +29,9 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.DataBindingPropertyAccessor; +import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.expression.spel.support.SimpleEvaluationContext.Builder; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.integration.context.IntegrationContextUtils; @@ -55,31 +58,20 @@ public final class ExpressionUtils { } /** - * Create a {@link StandardEvaluationContext} with a {@link MapAccessor} in its - * property accessor property and the supplied {@link ConversionService} in its - * conversionService property. - * @param conversionService the conversion service. - * @return the evaluation context. + * Used to create a context with no BeanFactory, usually in tests. + * @return The evaluation context. */ - private static StandardEvaluationContext createStandardEvaluationContext(ConversionService conversionService, - BeanFactory beanFactory) { - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - evaluationContext.addPropertyAccessor(new MapAccessor()); - if (conversionService != null) { - evaluationContext.setTypeConverter(new StandardTypeConverter(conversionService)); - } - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } - return evaluationContext; + public static StandardEvaluationContext createStandardEvaluationContext() { + return (StandardEvaluationContext) doCreateContext(null, false); } /** * Used to create a context with no BeanFactory, usually in tests. * @return The evaluation context. + * @since 4.3.15 */ - public static StandardEvaluationContext createStandardEvaluationContext() { - return doCreateContext(null); + public static SimpleEvaluationContext createSimpleEvaluationContext() { + return (SimpleEvaluationContext) doCreateContext(null, true); } /** @@ -92,24 +84,75 @@ public final class ExpressionUtils { if (beanFactory == null) { logger.warn("Creating EvaluationContext with no beanFactory", new RuntimeException("No beanFactory")); } - return doCreateContext(beanFactory); + return (StandardEvaluationContext) doCreateContext(beanFactory, false); } - private static StandardEvaluationContext doCreateContext(BeanFactory beanFactory) { + /** + * Obtains the context from the beanFactory if not null; emits a warning if the beanFactory + * is null. + * @param beanFactory The bean factory. + * @return The evaluation context. + * @since 4.3.15 + */ + public static SimpleEvaluationContext createSimpleEvaluationContext(BeanFactory beanFactory) { + if (beanFactory == null) { + logger.warn("Creating EvaluationContext with no beanFactory", new RuntimeException("No beanFactory")); + } + return (SimpleEvaluationContext) doCreateContext(beanFactory, true); + } + + private static EvaluationContext doCreateContext(BeanFactory beanFactory, boolean simple) { ConversionService conversionService = null; - StandardEvaluationContext evaluationContext = null; + EvaluationContext evaluationContext = null; if (beanFactory != null) { - evaluationContext = IntegrationContextUtils.getEvaluationContext(beanFactory); + evaluationContext = + simple + ? IntegrationContextUtils.getSimpleEvaluationContext(beanFactory) + : IntegrationContextUtils.getEvaluationContext(beanFactory); } if (evaluationContext == null) { if (beanFactory != null) { conversionService = IntegrationUtils.getConversionService(beanFactory); } - evaluationContext = createStandardEvaluationContext(conversionService, beanFactory); + evaluationContext = createEvaluationContext(conversionService, beanFactory, simple); } return evaluationContext; } + /** + * Create a {@link StandardEvaluationContext} with a {@link MapAccessor} in its + * property accessor property and the supplied {@link ConversionService} in its + * conversionService property. + * @param conversionService the conversion service. + * @param beanFactory the bean factory. + * @param simple true if simple. + * @return the evaluation context. + */ + private static EvaluationContext createEvaluationContext(ConversionService conversionService, + BeanFactory beanFactory, boolean simple) { + + if (simple) { + Builder ecBuilder = SimpleEvaluationContext.forPropertyAccessors( + new MapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) + .withInstanceMethods(); + if (conversionService != null) { + ecBuilder.withConversionService(conversionService); + } + return ecBuilder.build(); + } + else { + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + evaluationContext.addPropertyAccessor(new MapAccessor()); + if (conversionService != null) { + evaluationContext.setTypeConverter(new StandardTypeConverter(conversionService)); + } + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + return evaluationContext; + } + } + /** * Evaluate an expression and return a {@link File} object; the expression can evaluate * to a {@link String} or {@link File}. diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index 3b887551e8..3e1160c7e4 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -75,6 +75,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.serializer.support.SerializingConverter; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.annotation.Aggregator; import org.springframework.integration.annotation.BridgeFrom; import org.springframework.integration.annotation.BridgeTo; @@ -706,7 +707,7 @@ public class EnableIntegrationTests { @Test public void testIntegrationEvaluationContextCustomization() { - EvaluationContext evaluationContext = this.context.getBean(EvaluationContext.class); + EvaluationContext evaluationContext = this.context.getBean(StandardEvaluationContext.class); List propertyAccessors = TestUtils.getPropertyValue(evaluationContext, "propertyAccessors", List.class); assertEquals(4, propertyAccessors.size()); assertThat(propertyAccessors.get(0), instanceOf(JsonPropertyAccessor.class)); diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundChannelAdapterParser.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundChannelAdapterParser.java index 63a2f30a57..0f9ecb8f1e 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundChannelAdapterParser.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundChannelAdapterParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -68,6 +68,7 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda } IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-payload"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "trusted-spel"); HttpAdapterParsingUtils.setExpectedResponseOrExpression(element, parserContext, builder); HttpAdapterParsingUtils.configureUriVariableExpressions(builder, parserContext, element); return builder.getBeanDefinition(); diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java index 3548bcd3e6..ee8f3490b0 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpOutboundGatewayParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -76,6 +76,7 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser { IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-request-payload", "extractPayload"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "trusted-spel"); HttpAdapterParsingUtils.setExpectedResponseOrExpression(element, parserContext, builder); diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java index e26930d4ba..e9ef71162d 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,9 @@ import java.util.function.Supplier; import javax.xml.transform.Source; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -68,6 +70,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Artem Bilan * @author Wallace Wadge * @author Shiliang Li + * * @since 5.0 */ public abstract class AbstractHttpRequestExecutingMessageHandler extends AbstractReplyProducingMessageHandler { @@ -77,10 +80,14 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac private final Map uriVariableExpressions = new HashMap<>(); - private volatile StandardEvaluationContext evaluationContext; - private final Expression uriExpression; + private StandardEvaluationContext evaluationContext; + + private SimpleEvaluationContext simpleEvaluationContext; + + private boolean trustedSpel; + private volatile boolean encodeUri = true; private volatile Expression httpMethodExpression = new ValueExpression<>(HttpMethod.POST); @@ -247,9 +254,21 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac this.transferCookies = transferCookies; } + /** + * Set to true if you trust the source of SpEL expressions used to evaluate URI + * variables. Default is false, which means a {@link SimpleEvaluationContext} is used + * for evaluating such expressions, which restricts the use of some SpEL capabilities. + * @param trustedSpel true to trust. + * @since 4.3.15. + */ + public void setTrustedSpel(boolean trustedSpel) { + this.trustedSpel = trustedSpel; + } + @Override protected void doInit() { this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory()); + this.simpleEvaluationContext = ExpressionUtils.createSimpleEvaluationContext(this.getBeanFactory()); } @Override @@ -526,18 +545,22 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac private Map determineUriVariables(Message requestMessage) { Map expressions; + EvaluationContext evaluationContextToUse = this.evaluationContext; if (this.uriVariablesExpression != null) { Object expressionsObject = this.uriVariablesExpression.getValue(this.evaluationContext, requestMessage); Assert.state(expressionsObject instanceof Map, "The 'uriVariablesExpression' evaluation must result in a 'Map'."); expressions = (Map) expressionsObject; + if (!this.trustedSpel) { + evaluationContextToUse = this.simpleEvaluationContext; + } } else { expressions = this.uriVariableExpressions; } return ExpressionEvalMap.from(expressions) - .usingEvaluationContext(this.evaluationContext) + .usingEvaluationContext(evaluationContextToUse) .withRoot(requestMessage) .build(); diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.1.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.1.xsd index 92078dc4a8..b85ec32c07 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.1.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.1.xsd @@ -398,6 +398,18 @@ + + + + Set to 'true' if you trust SpEL expressions that might be evaluated to generate + URI variables. + The default value is 'false'. + + + + + + @@ -490,6 +502,18 @@ + + + + Set to 'true' if you trust SpEL expressions that might be evaluated to generate + URI variables. + The default value is 'false'. + + + + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpOutboundWithinChainTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpOutboundWithinChainTests-context.xml index 81230b3f50..a267b4134d 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpOutboundWithinChainTests-context.xml +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpOutboundWithinChainTests-context.xml @@ -8,8 +8,9 @@ http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - - + + @@ -22,6 +23,7 @@ diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandlerTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandlerTests.java index abc2a60d48..c36f416ee7 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandlerTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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,11 @@ package org.springframework.integration.http.outbound; +import static org.hamcrest.Matchers.equalTo; 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 static org.mockito.ArgumentMatchers.any; @@ -706,6 +708,9 @@ public class HttpRequestExecutingMessageHandlerTests { channel.send(MessageBuilder.withPayload("test").build()); Mockito.verify(restTemplate).exchange(Mockito.eq(new URI("http://localhost/test1/%2f")), Mockito.eq(HttpMethod.POST), Mockito.any(HttpEntity.class), Mockito.>eq(null)); + HttpRequestExecutingMessageHandler handler = ctx.getBean("chain$child.adapter.handler", + HttpRequestExecutingMessageHandler.class); + assertThat(TestUtils.getPropertyValue(handler, "trustedSpel"), equalTo(Boolean.TRUE)); ctx.close(); } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java index c817dae20b..09d606242f 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.integration.http.outbound; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -132,13 +134,15 @@ public class UriVariableExpressionTests { handler.setUriVariablesExpression(new SpelExpressionParser().parseExpression("headers.uriVariables")); handler.afterPropertiesSet(); - Map expressions = new HashMap(); + Map expressions = new HashMap(); expressions.put("foo", "bar"); Map expressionsMap = ExpressionEvalMap.from(expressions).usingSimpleCallback().build(); try { - handler.handleMessage(MessageBuilder.withPayload("test").setHeader("uriVariables", expressionsMap).build()); + handler.handleMessage(MessageBuilder.withPayload("test") + .setHeader("uriVariables", expressionsMap) + .build()); fail("Exception expected."); } catch (Exception e) { @@ -146,6 +150,42 @@ public class UriVariableExpressionTests { } assertEquals("http://test/bar", uriHolder.get().toString()); + + expressions.put("foo", new SpelExpressionParser().parseExpression("'bar'.toUpperCase()")); + try { + handler.handleMessage(MessageBuilder.withPayload("test") + .setHeader("uriVariables", expressions) + .build()); + fail("Exception expected."); + } + catch (Exception e) { + assertEquals("intentional", e.getCause().getMessage()); + } + + assertEquals("http://test/BAR", uriHolder.get().toString()); + + expressions.put("foo", new SpelExpressionParser().parseExpression("T(Integer).valueOf('42')")); + try { + handler.handleMessage(MessageBuilder.withPayload("test") + .setHeader("uriVariables", expressions) + .build()); + fail("Exception expected."); + } + catch (Exception e) { + assertThat(e.getCause().getMessage(), containsString("Type cannot be found")); + } + + handler.setTrustedSpel(true); + try { + handler.handleMessage(MessageBuilder.withPayload("test") + .setHeader("uriVariables", expressions) + .build()); + fail("Exception expected."); + } + catch (Exception e) { + assertEquals("intentional", e.getCause().getMessage()); + } + assertEquals("http://test/42", uriHolder.get().toString()); } } diff --git a/src/reference/asciidoc/changes-4.2-4.3.adoc b/src/reference/asciidoc/changes-4.2-4.3.adoc index 2d2659c73f..6c41cd08cb 100644 --- a/src/reference/asciidoc/changes-4.2-4.3.adoc +++ b/src/reference/asciidoc/changes-4.2-4.3.adoc @@ -216,6 +216,10 @@ With this release, the content type of such requests is considered to be `applic by RFC 2616. See <> for more information. +`uriVariablesExpression` now uses a `SimpleEvaluationContext` by default (since 4.3.15). + +See <> for more information. + ==== SFTP Changes ===== Factory Bean diff --git a/src/reference/asciidoc/changes-4.3-5.0.adoc b/src/reference/asciidoc/changes-4.3-5.0.adoc index 166be5361f..07f6609116 100644 --- a/src/reference/asciidoc/changes-4.3-5.0.adoc +++ b/src/reference/asciidoc/changes-4.3-5.0.adoc @@ -236,6 +236,10 @@ The `DefaultHttpHeaderMapper.userDefinedHeaderPrefix` property is now an empty s See <> for more information. +`uriVariablesExpression` now uses a `SimpleEvaluationContext` by default (since 5.0.4). + +See <> for more information. + ==== MQTT Changes Inbound messages are now mapped with headers `RECEIVED_TOPIC`, `RECEIVED_QOS` and `RECEIVED_RETAINED` to avoid inadvertent propagation to outbound messages when an application is relaying messages. diff --git a/src/reference/asciidoc/http.adoc b/src/reference/asciidoc/http.adoc index aa4158d60e..ce2336b783 100644 --- a/src/reference/asciidoc/http.adoc +++ b/src/reference/asciidoc/http.adoc @@ -526,6 +526,7 @@ Changes in Spring 3.1 can cause some issues with escaped characters, such as '?' For this reason, it is recommended that if you wish to generate the URL entirely at runtime, you use the 'url-expression' attribute. ===== +[[mapping-uri-variables]] ==== Mapping URI Variables If your URL contains URI variables, you can map them using the `uri-variable` sub-element. @@ -584,6 +585,15 @@ NOTE: The `uri-variables-expression` must evaluate to a `Map`. The values of the Map must be instances of `String` or `Expression`. This Map is provided to an `ExpressionEvalMap` for further resolution of URI variable placeholders using those expressions in the context of the outbound `Message`. +IMPORTANT +==== +The `uriVariablesExpression` property provides a very powerful mechanism for evaluating URI variables. +It is anticipated that simple expressions like the example above will be used. +However, you could also configure something like this `"@uriVariablesBean.populate(#root)"` with an expression in the returned map being `variables.put("foo", EXPRESSION_PARSER.parseExpression(message.getHeaders().get("bar", String.class)));`, where the expression is dynamically provided in the message header `bar`. +Since the header may come from an untrusted source, the HTTP outbound endpoints use a `SimpleEvaluationContext` when evaluating these expressions; allowing only a subset of SpEL features to be used. +If you trust your message sources and wish to use the restricted SpEL constructs, set the `trustedSpel` property of the outbound endpoint to `true`. +==== + Scenarios when we need to supply a dynamic set of URI variables on per message basis can be achieved with the custom `url-expression` and some utilities for building and encoding URL parameters: [source,xml]