From 63cbf736a2e316bc77ec967538152306e72b2a20 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 10 Sep 2013 15:39:24 +0300 Subject: [PATCH] INT-3139: Add #jsonPath SpEL Function Support * #jsonPath function is registered automatically if the library is on the classpath * Add otional dependency on `com.jayway.jsonpath:json-path` artifact * Add `JsonPathUtils` class as a wrapper around `JsonPath` * Add `#jsonPath` tests for: ``, ``, ``, `` JIRA: https://jira.springsource.org/browse/INT-3139 INT-3139: Further improvements * Remove `SpelFunctionRegistrar` * Introduce `SpelFunctionFactoryBean` INT-3139: Polishing --- build.gradle | 1 + .../amqp/config/OutboundGatewayTests.java | 2 +- ...tegrationEvaluationContextFactoryBean.java | 42 ++-- .../config/SpelFunctionFactoryBean.java | 91 ++++++++ .../config/SpelFunctionRegistrar.java | 83 ------- .../AbstractIntegrationNamespaceHandler.java | 43 +++- .../config/xml/IntegrationNamespaceUtils.java | 19 +- .../config/xml/SpelFunctionParser.java | 69 +----- .../integration/json/JsonPathUtils.java | 58 +++++ .../aop/PublisherExpressionTests.java | 2 +- .../expression/ChildContext-context.xml | 6 +- .../expression/ParentContextTests.java | 42 ++-- ...essionEvaluatingMessageProcessorTests.java | 15 +- .../json/JsonPathTests-context.xml | 64 ++++++ .../integration/json/JsonPathTests.java | 217 ++++++++++++++++++ .../integration/json/JsonPathTests.json | 35 +++ 16 files changed, 587 insertions(+), 202 deletions(-) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionFactoryBean.java delete mode 100644 spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionRegistrar.java create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/json/JsonPathUtils.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests-context.xml create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.json diff --git a/build.gradle b/build.gradle index 1d10ec80e2..076f7875fe 100644 --- a/build.gradle +++ b/build.gradle @@ -185,6 +185,7 @@ project('spring-integration-core') { compile "com.eaio.uuid:uuid:$eaioUUIDVersion" compile("org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion", optional) compile("com.fasterxml.jackson.core:jackson-databind:$jackson2Version", optional) + compile('com.jayway.jsonpath:json-path:0.8.1', optional) testCompile "org.aspectj:aspectjweaver:$aspectjVersion" } } diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java index 68f34872fc..9036b31854 100644 --- a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/OutboundGatewayTests.java @@ -77,7 +77,7 @@ public class OutboundGatewayTests { }).when(context).getBean(anyString()); when(context.containsBean(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME)).thenReturn(true); IntegrationEvaluationContextFactoryBean integrationEvaluationContextFactoryBean = new IntegrationEvaluationContextFactoryBean(); - integrationEvaluationContextFactoryBean.setBeanFactory(context); + integrationEvaluationContextFactoryBean.setApplicationContext(context); integrationEvaluationContextFactoryBean.afterPropertiesSet(); StandardEvaluationContext evalContext = integrationEvaluationContextFactoryBean.getObject(); when(context.getBean(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, StandardEvaluationContext.class)) 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 39a425b993..90c10c7742 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 @@ -25,11 +25,11 @@ import java.util.Map; import java.util.Map.Entry; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; +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; @@ -66,7 +66,7 @@ import org.springframework.util.Assert; * @since 3.0 */ public class IntegrationEvaluationContextFactoryBean implements FactoryBean, - BeanFactoryAware, InitializingBean { + ApplicationContextAware, InitializingBean { private volatile List propertyAccessors = new ArrayList(); @@ -74,14 +74,13 @@ public class IntegrationEvaluationContextFactoryBean implements FactoryBean functions) { - this.functions.putAll(functions); - } @Override public void afterPropertiesSet() throws Exception { if (this.propertyAccessors.isEmpty()) { this.loadDefaultPropertyAccessors(this.propertyAccessors); } - if (this.beanFactory != null) { - this.beanResolver = new BeanFactoryResolver(this.beanFactory); - ConversionService conversionService = IntegrationContextUtils.getConversionService(this.beanFactory); + if (this.applicationContext != null) { + this.beanResolver = new BeanFactoryResolver(this.applicationContext); + ConversionService conversionService = IntegrationContextUtils.getConversionService(this.applicationContext); if (conversionService != null) { this.typeConverter = new StandardTypeConverter(conversionService); } - try { - SpelFunctionRegistrar functionRegistrar = this.beanFactory.getBean(SpelFunctionRegistrar.class); - if (functionRegistrar != null) { - this.addFunctions(functionRegistrar.getFunctions()); + + 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()); } } - catch (NoSuchBeanDefinitionException e) { - //Ignore it. - //There is no components within application context. - } } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionFactoryBean.java new file mode 100644 index 0000000000..8f533143f7 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionFactoryBean.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.config; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * A {@link FactoryBean} implementation to encapsulate the population of a static {@link Method} + * from the provided {@linkplain #functionClass} and {@linkplain #functionMethodSignature} as + * a valid {@link org.springframework.expression.spel.support.StandardEvaluationContext} function. + * + * @author Artem Bilan + * @since 3.0 + * + * @see org.springframework.expression.spel.support.StandardEvaluationContext#registerFunction + * @see BeanUtils#resolveSignature + */ +public class SpelFunctionFactoryBean implements FactoryBean, InitializingBean, BeanNameAware { + + private final Class functionClass; + + private final String functionMethodSignature; + + private String functionName; + + private Method method; + + public SpelFunctionFactoryBean(Class functionClass, String functionMethodSignature) { + this.functionClass = functionClass; + this.functionMethodSignature = functionMethodSignature; + } + + @Override + public void setBeanName(String name) { + this.functionName = name; + } + + public String getFunctionName() { + return functionName; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.method = BeanUtils.resolveSignature(this.functionMethodSignature, this.functionClass); + + if (this.method == null) { + throw new BeanDefinitionStoreException(String.format("No declared method '%s' in class '%s'", + this.functionMethodSignature, this.functionClass)); + } + if (!Modifier.isStatic(this.method.getModifiers())) { + throw new BeanDefinitionStoreException("SpEL-function method has to be 'static'"); + } + } + + @Override + public Method getObject() throws Exception { + return method; + } + + @Override + public Class getObjectType() { + return Method.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionRegistrar.java deleted file mode 100644 index be98f649be..0000000000 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/SpelFunctionRegistrar.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.integration.config; - -import java.lang.reflect.Method; -import java.util.Map; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -/** - *

- * Utility class that keeps track of a Map of SpEL functions in order to register - * them with the "integrationEvaluationContext" upon initialization. - *

- *

- * The {@link org.springframework.integration.config.xml.SpelFunctionParser} - * doesn't register a bean for each <spel-function> within application context. - * There is no automatic way to get 'functions' from a parent context - * and this class provide a hook to get 'functions' from the parent context. - *

- * - * @author Artem Bilan - * - * @since 3.0 - */ -class SpelFunctionRegistrar implements ApplicationContextAware, InitializingBean { - - private final Map functions; - - private ApplicationContext applicationContext; - - SpelFunctionRegistrar(Map functions) { - this.functions = functions; - } - - Map getFunctions() { - return functions; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - @Override - public void afterPropertiesSet() throws Exception { - ApplicationContext parent = this.applicationContext.getParent(); - if (parent != null) { - try { - SpelFunctionRegistrar parentFunctionRegistrar = parent.getBean(SpelFunctionRegistrar.class); - Map parentFunctions = parentFunctionRegistrar.getFunctions(); - for (String key : parentFunctions.keySet()) { - if(!this.functions.containsKey(key)) { - this.functions.put(key, parentFunctions.get(key)); - } - } - } - catch (NoSuchBeanDefinitionException e) { - //Ignore it. - //There is no components within parent application context. - } - } - } - -} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java index 57985316a0..104d17e7e4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java @@ -18,6 +18,8 @@ package org.springframework.integration.config.xml; import static org.springframework.integration.context.IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -37,6 +39,7 @@ import org.springframework.integration.config.IntegrationEvaluationContextFactor import org.springframework.integration.config.xml.ChannelInitializer.AutoCreateCandidatesCollector; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.expression.IntegrationEvaluationContextAwareBeanPostProcessor; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -49,6 +52,8 @@ import org.springframework.util.StringUtils; */ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHandler { + protected final Log logger = LogFactory.getLog(this.getClass()); + private static final String VERSION = "3.0"; public static final String CHANNEL_INITIALIZER_BEAN_NAME = "channelInitializer"; @@ -67,6 +72,7 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa this.verifySchemaVersion(element, parserContext); this.registerImplicitChannelCreator(parserContext); this.registerIntegrationEvaluationContext(parserContext); + this.registerBuiltInBeans(parserContext); this.registerDefaultConfiguringBeanFactoryPostProcessorIfNecessary(parserContext); return this.delegate.parse(element, parserContext); } @@ -141,6 +147,37 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa } } + private void registerBuiltInBeans(ParserContext parserContext) { + String jsonPathBeanName = "jsonPath"; + boolean alreadyRegistered = false; + if (parserContext.getRegistry() instanceof ListableBeanFactory) { + alreadyRegistered = ((ListableBeanFactory) parserContext.getRegistry()).containsBean(jsonPathBeanName); + } + else { + alreadyRegistered = parserContext.getRegistry().isBeanNameInUse(jsonPathBeanName); + } + if (!alreadyRegistered) { + Class jsonPathClass = null; + try { + jsonPathClass = ClassUtils.forName("com.jayway.jsonpath.JsonPath", parserContext.getReaderContext().getBeanClassLoader()); + } + catch (ClassNotFoundException e) { + logger.debug("SpEL function '#jsonPath' isn't registered: there is no jayway json-path.jar on the classpath."); + } + + if (jsonPathClass != null) { + IntegrationNamespaceUtils.registerSpelFunctionBean(parserContext.getRegistry(), jsonPathBeanName, + IntegrationNamespaceUtils.BASE_PACKAGE + ".json.JsonPathUtils", "evaluate"); + } + } + + this.doRegisterBuiltInBeans(parserContext); + } + + protected void doRegisterBuiltInBeans(ParserContext parserContext) { + + } + private void registerDefaultConfiguringBeanFactoryPostProcessorIfNecessary(ParserContext parserContext) { boolean alreadyRegistered = false; if (parserContext.getRegistry() instanceof ListableBeanFactory) { @@ -171,10 +208,10 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa } private void verifySchemaVersion(Element element, ParserContext parserContext) { - if (!(matchesVersion(element) && matchesVersion(element.getOwnerDocument().getDocumentElement()))) { + if (!(matchesVersion(element) && matchesVersion(element.getOwnerDocument().getDocumentElement()))) { parserContext.getReaderContext().error( - "You cannot use prior versions of Spring Integration schemas with Spring Integration " + VERSION + - ". Please upgrade your schema declarations or use versionless aliases (e.g. spring-integration.xsd).", element); + "You cannot use prior versions of Spring Integration schemas with Spring Integration " + VERSION + + ". Please upgrade your schema declarations or use versionless aliases (e.g. spring-integration.xsd).", element); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java index 98a6a00a8c..160e2a3564 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java @@ -17,12 +17,17 @@ import static org.springframework.beans.factory.xml.AbstractBeanDefinitionParser import java.util.List; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; @@ -30,6 +35,7 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.core.Conventions; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.config.ExpressionFactoryBean; +import org.springframework.integration.config.SpelFunctionFactoryBean; import org.springframework.integration.endpoint.AbstractPollingEndpoint; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.interceptor.MatchAlwaysTransactionAttributeSource; @@ -37,9 +43,6 @@ import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; /** * Shared utility methods for integration namespace parsers. @@ -456,4 +459,14 @@ public abstract class IntegrationNamespaceUtils { } return expressionDef; } + + + public static void registerSpelFunctionBean(BeanDefinitionRegistry registry, String functionId, String className, + String methodSignature) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SpelFunctionFactoryBean.class) + .addConstructorArgValue(className) + .addConstructorArgValue(methodSignature); + registry.registerBeanDefinition(functionId, builder.getBeanDefinition()); + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelFunctionParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelFunctionParser.java index b734c1dc65..1028f42928 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelFunctionParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelFunctionParser.java @@ -16,78 +16,29 @@ package org.springframework.integration.config.xml; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.LinkedHashMap; -import java.util.Map; - import org.w3c.dom.Element; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; -import org.springframework.beans.factory.xml.BeanDefinitionParser; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.util.ClassUtils; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.integration.config.SpelFunctionFactoryBean; /** * Parser for the <spel-function> element. - * Doesn't register a bean within application context, collects 'functions' - * within {@link org.springframework.integration.config.SpelFunctionRegistrar} bean. * * @author Artem Bilan * @since 3.0 */ -public class SpelFunctionParser implements BeanDefinitionParser { - - private final Map functions = new LinkedHashMap(); - - private volatile boolean initialized; +public class SpelFunctionParser extends AbstractSingleBeanDefinitionParser { @Override - public BeanDefinition parse(Element element, ParserContext parserContext) { - - this.initializeSpelFunctionRegistrarIfNecessary(parserContext); - - String id = element.getAttribute("id"); - String className = element.getAttribute("class"); - String signature = element.getAttribute("method"); - - Class clazz = null; - try { - clazz = ClassUtils.forName(className, parserContext.getReaderContext().getBeanClassLoader()); - } - catch (ClassNotFoundException e) { - parserContext.getReaderContext().error(e.getMessage(), element); - } - - Method method = BeanUtils.resolveSignature(signature, clazz); - - if (method == null) { - parserContext.getReaderContext().error(String.format("No declared method '%s' in class '%s'", - signature, className), element); - return null; - } - if (!Modifier.isStatic(method.getModifiers())) { - parserContext.getReaderContext().error("SpEL-function method has to be 'static'", element); - } - - this.functions.put(id, method); - - return null; + protected Class getBeanClass(Element element) { + return SpelFunctionFactoryBean.class; } - private synchronized void initializeSpelFunctionRegistrarIfNecessary(ParserContext parserContext) { - if (!this.initialized) { - BeanDefinitionBuilder registrarBuilder = BeanDefinitionBuilder - .genericBeanDefinition(IntegrationNamespaceUtils.BASE_PACKAGE + ".config.SpelFunctionRegistrar") - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .addConstructorArgValue(this.functions); - BeanDefinitionReaderUtils.registerWithGeneratedName(registrarBuilder.getBeanDefinition(), - parserContext.getRegistry()); - this.initialized = true; - } - } + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + builder.addConstructorArgValue(element.getAttribute("class")) + .addConstructorArgValue(element.getAttribute("method")); + } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPathUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPathUtils.java new file mode 100644 index 0000000000..59138ae110 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPathUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.json; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +import com.jayway.jsonpath.Filter; +import com.jayway.jsonpath.JsonPath; + +/** + * Utility class to {@link #evaluate} a jsonPath on the provided object. + * Delegates evaluation to JsonPath. + * Note {@link #evaluate} is used as {@code #jsonPath()} SpEL function. + * + * @author Artem Bilan + * @since 3.0 + */ +public final class JsonPathUtils { + + public static T evaluate(Object json, String jsonPath, Filter... filters) throws Exception { + if (json instanceof String) { + return JsonPath.read((String) json, jsonPath, filters); + } + else if (json instanceof File) { + return JsonPath.read((File) json, jsonPath, filters); + } + else if (json instanceof URL) { + return JsonPath.read((URL) json, jsonPath, filters); + } + else if (json instanceof InputStream) { + return JsonPath.read((InputStream) json, jsonPath, filters); + } + else { + return JsonPath.read(json, jsonPath, filters); + } + + } + + private JsonPathUtils() { + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java index 1106317967..052122b182 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java @@ -47,7 +47,7 @@ public class PublisherExpressionTests { public void setup() throws Exception { context.registerSingleton("testChannel", QueueChannel.class); IntegrationEvaluationContextFactoryBean factory = new IntegrationEvaluationContextFactoryBean(); - factory.setBeanFactory(context); + factory.setApplicationContext(context); factory.afterPropertiesSet(); EvaluationContext ec = factory.getObject(); context.getBeanFactory().registerSingleton(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, ec); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/expression/ChildContext-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/expression/ChildContext-context.xml index 625fcfd41d..0243875dd6 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/expression/ChildContext-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/expression/ChildContext-context.xml @@ -5,11 +5,11 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd"> - + - + @@ -21,4 +21,6 @@ + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java b/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java index 1da8524b28..961f90db20 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java @@ -19,22 +19,26 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.junit.Test; -import org.springframework.context.ApplicationContext; +import org.hamcrest.Matchers; + import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.context.support.GenericApplicationContext; import org.springframework.expression.EvaluationContext; import org.springframework.integration.Message; import org.springframework.integration.MessageChannel; import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.IntegrationEvaluationContextFactoryBean; +import org.springframework.integration.json.JsonPathUtils; import org.springframework.integration.message.GenericMessage; import org.springframework.integration.test.util.TestUtils; @@ -56,34 +60,34 @@ public class ParentContextTests { * and parent contexts work. */ @Test - public void testSpelBeanReferencesInChildAndParent() throws ClassNotFoundException { - //To check if 'org.springframework.integration.config.SpelFunctionRegistrar#afterPropertiesSet()' - //doesn't throw any Exception - AbstractApplicationContext superParent = new GenericApplicationContext(); - superParent.refresh(); + public void testSpelBeanReferencesInChildAndParent() throws Exception { + AbstractApplicationContext parent = new ClassPathXmlApplicationContext("ParentContext-context.xml", this.getClass()); - AbstractApplicationContext parent = new ClassPathXmlApplicationContext(new String[]{"ParentContext-context.xml"}, - this.getClass(), superParent); - - Class spelFunctionRegistrarClass = Class.forName("org.springframework.integration.config.SpelFunctionRegistrar"); - Object parentSpelFunctionRegistrar = parent.getBean(spelFunctionRegistrarClass); - assertEquals(2, TestUtils.getPropertyValue(parentSpelFunctionRegistrar, "functions", Map.class).size()); + Object parentEvaluationContextFactoryBean = parent.getBean(IntegrationEvaluationContextFactoryBean.class); + Map parentFunctions = TestUtils.getPropertyValue(parentEvaluationContextFactoryBean, "functions", Map.class); + assertEquals(3, parentFunctions.size()); + Object jsonPath = parentFunctions.get("jsonPath"); + assertNotNull(jsonPath); + assertThat((Method) jsonPath, Matchers.isOneOf(JsonPathUtils.class.getMethods())); assertEquals(2, evalContexts.size()); ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext(parent); child.setConfigLocation("org/springframework/integration/expression/ChildContext-context.xml"); child.refresh(); - Object childSpelFunctionRegistrar = child.getBean(spelFunctionRegistrarClass); - Map functions = TestUtils.getPropertyValue(childSpelFunctionRegistrar, "functions", Map.class); - assertEquals(3, functions.size()); - assertTrue(functions.containsKey("barParent")); + Object childEvaluationContextFactoryBean = child.getBean(IntegrationEvaluationContextFactoryBean.class); + Map childFunctions = TestUtils.getPropertyValue(childEvaluationContextFactoryBean, "functions", Map.class); + assertEquals(4, childFunctions.size()); + assertTrue(childFunctions.containsKey("barParent")); + jsonPath = childFunctions.get("jsonPath"); + assertNotNull(jsonPath); + assertThat((Method) jsonPath, Matchers.not(Matchers.isOneOf(JsonPathUtils.class.getMethods()))); assertEquals(3, evalContexts.size()); assertSame(evalContexts.get(0).getBeanResolver(), evalContexts.get(1).getBeanResolver()); assertNotSame(evalContexts.get(1).getBeanResolver(), evalContexts.get(2).getBeanResolver()); - assertSame(parent.getBeanFactory(), TestUtils.getPropertyValue(evalContexts.get(0).getBeanResolver(), "beanFactory")); - assertSame(child.getBeanFactory(), TestUtils.getPropertyValue(evalContexts.get(2).getBeanResolver(), "beanFactory")); + assertSame(parent, TestUtils.getPropertyValue(evalContexts.get(0).getBeanResolver(), "beanFactory")); + assertSame(child, TestUtils.getPropertyValue(evalContexts.get(2).getBeanResolver(), "beanFactory")); // Test transformer expressions child.getBean("input", MessageChannel.class).send(new GenericMessage("baz")); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java index f21c648dae..6dd5ddd45d 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java @@ -29,8 +29,8 @@ import org.junit.rules.ExpectedException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.Resource; @@ -52,6 +52,7 @@ import org.springframework.integration.test.util.TestUtils; * @author Mark Fisher * @author Gunnar Hillert * @author Gary Russell + * @author Artem Bilan * @since 2.0 */ public class ExpressionEvaluatingMessageProcessorTests { @@ -121,11 +122,11 @@ public class ExpressionEvaluatingMessageProcessorTests { } Expression expression = expressionParser.parseExpression("#target.find(payload)"); ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor(expression); - ConfigurableListableBeanFactory beanFactory = new GenericApplicationContext().getBeanFactory(); - processor.setBeanFactory(beanFactory); + AbstractApplicationContext applicationContext = new GenericApplicationContext(); + processor.setBeanFactory(applicationContext); IntegrationEvaluationContextFactoryBean factoryBean = new IntegrationEvaluationContextFactoryBean(); - factoryBean.setBeanFactory(beanFactory); - beanFactory.registerSingleton(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, + factoryBean.setApplicationContext(applicationContext); + applicationContext.getBeanFactory().registerSingleton(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, factoryBean.getObject()); processor.afterPropertiesSet(); EvaluationContext evaluationContext = TestUtils.getPropertyValue(processor, "evaluationContext", EvaluationContext.class); @@ -173,6 +174,8 @@ public class ExpressionEvaluatingMessageProcessorTests { context.registerBeanDefinition("testString", beanDefinition); context.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, new RootBeanDefinition(IntegrationEvaluationContextFactoryBean.class)); + context.refresh(); + Expression expression = expressionParser.parseExpression("payload.concat(@testString)"); ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor(expression); processor.setBeanFactory(context); @@ -190,6 +193,8 @@ public class ExpressionEvaluatingMessageProcessorTests { context.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME, new RootBeanDefinition(IntegrationEvaluationContextFactoryBean.class)); context.registerBeanDefinition("testString", beanDefinition); + context.refresh(); + Expression expression = expressionParser.parseExpression("@testString.concat(payload)"); ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor(expression); processor.setBeanFactory(context); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests-context.xml new file mode 100644 index 0000000000..524c29e33d --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests-context.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java new file mode 100644 index 0000000000..7fae338604 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java @@ -0,0 +1,217 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.json; + +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 java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Scanner; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.hamcrest.Matchers; + +import com.jayway.jsonpath.Criteria; +import com.jayway.jsonpath.Filter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.MessageRejectedException; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.message.GenericMessage; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +/** + * @author Artem Bilan + * @since 3.0 + */ +@ContextConfiguration(classes = JsonPathTests.JsonPathTestsContextConfiguration.class, loader = AnnotationConfigContextLoader.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class JsonPathTests { + + private static File JSON_FILE; + + private static String JSON; + + private static Message testMessage; + + @BeforeClass + public static void setUp() throws IOException { + ClassPathResource jsonResource = new ClassPathResource("JsonPathTests.json", JsonPathTests.class); + JSON_FILE = jsonResource.getFile(); + JSON = new Scanner(JSON_FILE).useDelimiter("\\Z").next(); + testMessage = new GenericMessage(JSON); + } + + @Autowired + private PollableChannel output; + + @Autowired + private volatile MessageChannel transformerInput; + + @Autowired + private volatile MessageChannel filterInput1; + + @Autowired + private PollableChannel discardChannel; + + @Autowired + private volatile MessageChannel filterInput2; + + @Autowired + private volatile MessageChannel filterInput3; + + @Autowired + private volatile MessageChannel filterInput4; + + @Autowired + private volatile MessageChannel splitterInput; + + @Autowired + private PollableChannel splitterOutput; + + @Autowired + private volatile MessageChannel routerInput; + + @Autowired + private PollableChannel routerOutput1; + + @Autowired + private PollableChannel routerOutput2; + + + @Test + public void testInt3139JsonPathTransformer() throws IOException { + this.transformerInput.send(testMessage); + Message receive = this.output.receive(1000); + assertNotNull(receive); + assertEquals("Nigel Rees", receive.getPayload()); + + this.transformerInput.send(new GenericMessage(JSON_FILE)); + receive = this.output.receive(1000); + assertNotNull(receive); + + assertEquals("Nigel Rees", receive.getPayload()); + try { + this.transformerInput.send(new GenericMessage(new Object())); + fail("IllegalArgumentException expected"); + } + catch (Exception e) { + //MessageTransformationException / MessageHandlingException / InvocationTargetException / IllegalArgumentException + Throwable cause = e.getCause().getCause().getCause(); + assertTrue(cause instanceof IllegalArgumentException); + assertEquals("Invalid container object", cause.getMessage()); + } + } + + @Test + public void testInt3139JsonPathFilter() { + this.filterInput1.send(testMessage); + Message receive = this.output.receive(1000); + assertNotNull(receive); + assertEquals(JSON, receive.getPayload()); + + this.filterInput2.send(testMessage); + receive = this.output.receive(1000); + assertNotNull(receive); + + Message message = MessageBuilder.withPayload(JSON) + .setHeader("price", 10) + .build(); + this.filterInput3.send(message); + receive = this.output.receive(1000); + assertNotNull(receive); + + this.filterInput4.send(testMessage); + receive = this.output.receive(1000); + assertNotNull(receive); + + try { + this.filterInput1.send(new GenericMessage("{foo:{}}")); + fail("MessageRejectedException is expected."); + } + catch (Exception e) { + assertThat(e, Matchers.instanceOf(MessageRejectedException.class)); + } + receive = this.output.receive(0); + assertNull(receive); + + receive = this.discardChannel.receive(1000); + assertNotNull(receive); + + } + + @Test + public void testInt3139JsonPathSplitter() { + this.splitterInput.send(testMessage); + for(int i = 0; i < 3; i++) { + Message receive = this.splitterOutput.receive(1000); + assertNotNull(receive); + assertTrue(receive.getPayload() instanceof Map); + } + } + + @Test + public void testInt3139JsonPathRouter() { + Message message = MessageBuilder.withPayload(JSON) + .setHeader("jsonPath", "$.store.book[0].category") + .build(); + this.routerInput.send(message); + Message receive = this.routerOutput1.receive(1000); + assertNotNull(receive); + assertEquals(JSON, receive.getPayload()); + assertNull(this.routerOutput2.receive(10)); + + message = MessageBuilder.withPayload(JSON) + .setHeader("jsonPath", "$.store.book[2].category") + .build(); + this.routerInput.send(message); + receive = this.routerOutput2.receive(1000); + assertNotNull(receive); + assertEquals(JSON, receive.getPayload()); + assertNull(this.routerOutput1.receive(10)); + } + + + @Configuration + @ImportResource("classpath:org/springframework/integration/json/JsonPathTests-context.xml") + public static class JsonPathTestsContextConfiguration { + + @Bean + public Filter jsonPathFilter() { + return Filter.filter(Criteria.where("isbn").exists(true).and("category").ne("fiction")); + } + + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.json b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.json new file mode 100644 index 0000000000..06619679a9 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.json @@ -0,0 +1,35 @@ +{ "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } +} +}