diff --git a/build.gradle b/build.gradle index 3856a4f9a8..8ab4aa6157 100644 --- a/build.gradle +++ b/build.gradle @@ -136,7 +136,7 @@ subprojects { subproject -> springSecurityVersion = '4.1.4.RELEASE' springSocialTwitterVersion = '1.1.2.RELEASE' springRetryVersion = '1.1.3.RELEASE' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '4.3.14.RELEASE' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '4.3.15.BUILD-SNAPSHOT' springWsVersion = '2.3.0.RELEASE' xmlUnitVersion = '1.6' xstreamVersion = '1.4.7' 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 a283bcebdd..64c28cdc74 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"; @@ -120,12 +123,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)) { @@ -149,7 +164,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 add534915f..28090ad137 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-2015 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,18 +16,22 @@ package org.springframework.integration.expression; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.BeanFactory; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.core.convert.ConversionService; +import org.springframework.expression.EvaluationContext; +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; import org.springframework.integration.support.utils.IntegrationUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - /** * Utility class with static methods for helping with establishing environments for * SpEL expressions. @@ -40,32 +44,22 @@ import org.apache.commons.logging.LogFactory; public abstract class ExpressionUtils { private static final Log logger = LogFactory.getLog(ExpressionUtils.class); - /** - * 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. - */ - 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; - } /** * Used to create a context with no BeanFactory, usually in tests. * @return The evaluation context. */ public static StandardEvaluationContext createStandardEvaluationContext() { - return doCreateContext(null); + 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 SimpleEvaluationContext createSimpleEvaluationContext() { + return (SimpleEvaluationContext) doCreateContext(null, true); } /** @@ -78,22 +72,73 @@ public abstract 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; + } + } + } 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 df0547a64e..6a054d1481 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; @@ -695,7 +696,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 09184c7100..116d216f98 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-2015 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 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 e59176f776..f2a69d4680 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-2015 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. @@ -82,6 +82,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/HttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java index d06c08e4ed..e1be2ff2d4 100755 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java @@ -30,8 +30,10 @@ import javax.xml.transform.Source; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -87,7 +89,11 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe private final RestTemplate restTemplate; - private volatile StandardEvaluationContext evaluationContext; + private StandardEvaluationContext evaluationContext; + + private SimpleEvaluationContext simpleEvaluationContext; + + private boolean trustedSpel; private final Expression uriExpression; @@ -112,7 +118,6 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe private volatile Expression uriVariablesExpression; - /** * Create a handler that will send requests to the provided URI. * @@ -338,6 +343,17 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe 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 public String getComponentType() { return (this.expectReply ? "http:outbound-gateway" : "http:outbound-channel-adapter"); @@ -346,6 +362,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe @Override protected void doInit() { this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory()); + this.simpleEvaluationContext = ExpressionUtils.createSimpleEvaluationContext(this.getBeanFactory()); } @Override @@ -562,7 +579,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe * If all keys and values are Strings, we'll consider the Map to be form data. */ private boolean isFormData(Map map) { - for (Object key : map.keySet()) { + for (Object key : map.keySet()) { if (!(key instanceof String)) { return false; } @@ -599,7 +616,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe || expectedResponseType instanceof String || expectedResponseType instanceof ParameterizedTypeReference, "'expectedResponseType' can be an instance of 'Class', 'String' or 'ParameterizedTypeReference'; " - + "evaluation resulted in a" + expectedResponseType.getClass() + "."); + + "evaluation resulted in a" + expectedResponseType.getClass() + "."); if (expectedResponseType instanceof String && StringUtils.hasText((String) expectedResponseType)) { expectedResponseType = ClassUtils.forName((String) expectedResponseType, getApplicationContext().getClassLoader()); @@ -612,20 +629,24 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe 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) - .withRoot(requestMessage) - .build(); + .usingEvaluationContext(evaluationContextToUse) + .withRoot(requestMessage) + .build(); } diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.3.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.3.xsd index 6a166e437b..f38e1dba6a 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.3.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.3.xsd @@ -397,6 +397,18 @@ + + + + Set to 'true' if you trust SpEL expressions that might be evaluated to generate + URI variables. + The default value is 'false'. + + + + + + @@ -487,6 +499,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 0ca4705550..12f005d6b4 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-2016 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.Matchers.any; @@ -708,6 +710,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 972844b34b..f066578698 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-2015 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. @@ -134,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) { @@ -148,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/http.adoc b/src/reference/asciidoc/http.adoc index 6be010b463..1ce2a39bf5 100644 --- a/src/reference/asciidoc/http.adoc +++ b/src/reference/asciidoc/http.adoc @@ -523,6 +523,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. @@ -581,6 +582,53 @@ 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] +---- +url-expression="T(org.springframework.web.util.UriComponentsBuilder) + .fromHttpUrl('http://HOST:PORT/PATH') + .queryParams(payload) + .build() + .toUri()" +---- + +where `queryParams()` expects a `MultiValueMap` as an argument, so a real set of URL query parameters can be build in advance, before performing request. + +The whole `queryString` can also be presented as an uri variable: + +[source,xml] +---- + + + +---- + +In this case the URL encoding must be provided manually. +For example the `org.apache.http.client.utils.URLEncodedUtils#format()` can be used for this purpose. +A mentioned, manually built, `MultiValueMap` can be converted to the the `List` `format()` method argument using this Java Streams snippet: +[source,java] +---- +List nameValuePairs = + params.entrySet() + .stream() + .flatMap(e -> e + .getValue() + .stream() + .map(v -> new BasicNameValuePair(e.getKey(), v))) + .collect(Collectors.toList()); +---- + ==== Controlling URI Encoding By default, the URL string is encoded (see http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html[UriComponentsBuilder]) to the URI object before sending the request.