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 index ff45ef133b..af9e9cf194 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2024 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. @@ -28,11 +28,13 @@ 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.IndexAccessor; 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.lang.Nullable; import org.springframework.util.Assert; /** @@ -45,14 +47,19 @@ import org.springframework.util.Assert; */ public abstract class AbstractEvaluationContextFactoryBean implements ApplicationContextAware, InitializingBean { - private Map propertyAccessors = new LinkedHashMap(); + private Map propertyAccessors = new LinkedHashMap<>(); - private Map functions = new LinkedHashMap(); + private Map indexAccessors = new LinkedHashMap<>(); + + private Map functions = new LinkedHashMap<>(); private TypeConverter typeConverter = new StandardTypeConverter(); private ApplicationContext applicationContext; + @Nullable + private SpelPropertyAccessorRegistrar propertyAccessorRegistrar; + private boolean initialized; protected TypeConverter getTypeConverter() { @@ -68,22 +75,45 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio this.applicationContext = applicationContext; } - public void setPropertyAccessors(Map accessors) { + public void setPropertyAccessors(Map propertyAccessors) { 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); + Assert.notNull(propertyAccessors, "'propertyAccessors' must not be null."); + Assert.noNullElements(propertyAccessors.values().toArray(), "'propertyAccessors' cannot have null values."); + this.propertyAccessors = new LinkedHashMap<>(propertyAccessors); } public Map getPropertyAccessors() { return this.propertyAccessors; } + /** + * Set a map of {@link IndexAccessor}s to use in the target {@link org.springframework.expression.EvaluationContext} + * @param indexAccessors the map of {@link IndexAccessor}s to use + * @since 6.4 + * @see org.springframework.expression.EvaluationContext#getIndexAccessors() + */ + public void setIndexAccessors(Map indexAccessors) { + Assert.isTrue(!this.initialized, "'indexAccessors' can't be changed after initialization."); + Assert.notNull(indexAccessors, "'indexAccessors' must not be null."); + Assert.noNullElements(indexAccessors.values().toArray(), "'indexAccessors' cannot have null values."); + this.indexAccessors = new LinkedHashMap<>(indexAccessors); + } + + /** + * Return the map of {@link IndexAccessor}s to use in the target {@link org.springframework.expression.EvaluationContext} + * @return the map of {@link IndexAccessor}s to use + * @since 6.4 + * @see org.springframework.expression.EvaluationContext#getIndexAccessors() + */ + public Map getIndexAccessors() { + return this.indexAccessors; + } + 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); + this.functions = new LinkedHashMap<>(functionsArg); } public Map getFunctions() { @@ -94,7 +124,14 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio if (this.applicationContext != null) { conversionService(); functions(); + try { + this.propertyAccessorRegistrar = this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class); + } + catch (@SuppressWarnings("unused") NoSuchBeanDefinitionException e) { + // There is no 'SpelPropertyAccessorRegistrar' bean in the application context. + } propertyAccessors(); + indexAccessors(); processParentIfPresent(beanName); } this.initialized = true; @@ -108,28 +145,43 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio } private void functions() { - Map functionFactoryBeanMap = BeanFactoryUtils - .beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class); - for (SpelFunctionFactoryBean spelFunctionFactoryBean : functionFactoryBeanMap.values()) { - if (!getFunctions().containsKey(spelFunctionFactoryBean.getFunctionName())) { - getFunctions().put(spelFunctionFactoryBean.getFunctionName(), spelFunctionFactoryBean.getObject()); + Map spelFunctions = + BeanFactoryUtils.beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class); + for (SpelFunctionFactoryBean spelFunctionFactoryBean : spelFunctions.values()) { + String functionName = spelFunctionFactoryBean.getFunctionName(); + if (!this.functions.containsKey(functionName)) { + this.functions.put(functionName, spelFunctionFactoryBean.getObject()); } } } private void propertyAccessors() { - 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()); - } + if (this.propertyAccessorRegistrar != null) { + propertyAccessors(this.propertyAccessorRegistrar.getPropertyAccessors()); + } + } + + private void propertyAccessors(Map propertyAccessors) { + for (Entry entry : propertyAccessors.entrySet()) { + String key = entry.getKey(); + if (!this.propertyAccessors.containsKey(key)) { + this.propertyAccessors.put(key, entry.getValue()); } } - catch (@SuppressWarnings("unused") NoSuchBeanDefinitionException e) { - // There is no 'SpelPropertyAccessorRegistrar' bean in the application context. + } + + private void indexAccessors() { + if (this.propertyAccessorRegistrar != null) { + indexAccessors(this.propertyAccessorRegistrar.getIndexAccessors()); + } + } + + private void indexAccessors(Map indexAccessors) { + for (Entry entry : indexAccessors.entrySet()) { + String key = entry.getKey(); + if (!this.indexAccessors.containsKey(key)) { + this.indexAccessors.put(key, entry.getValue()); + } } } @@ -138,16 +190,12 @@ public abstract class AbstractEvaluationContextFactoryBean implements Applicatio 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()); - } - } - + propertyAccessors(parentFactoryBean.getPropertyAccessors()); + indexAccessors(parentFactoryBean.getIndexAccessors()); for (Entry entry : parentFactoryBean.getFunctions().entrySet()) { - if (!getFunctions().containsKey(entry.getKey())) { - getFunctions().put(entry.getKey(), entry.getValue()); + String key = entry.getKey(); + if (!this.functions.containsKey(key)) { + this.functions.put(key, entry.getValue()); } } } 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 4b1816c0ec..8101a522c5 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-2019 the original author or authors. + * Copyright 2013-2024 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. @@ -23,6 +23,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.BeanResolver; +import org.springframework.expression.IndexAccessor; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypeLocator; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -102,6 +103,10 @@ public class IntegrationEvaluationContextFactoryBean extends AbstractEvaluationC evaluationContext.addPropertyAccessor(new MapAccessor()); + for (IndexAccessor indexAccessor : getIndexAccessors().values()) { + evaluationContext.addIndexAccessor(indexAccessor); + } + for (Entry functionEntry : getFunctions().entrySet()) { evaluationContext.registerFunction(functionEntry.getKey(), functionEntry.getValue()); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java index 24f848d51d..051091d6d9 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java @@ -22,6 +22,7 @@ import java.util.Map.Entry; import org.springframework.beans.factory.FactoryBean; import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.IndexAccessor; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.spel.support.DataBindingPropertyAccessor; import org.springframework.expression.spel.support.SimpleEvaluationContext; @@ -80,6 +81,7 @@ public class IntegrationSimpleEvaluationContextFactoryBean extends AbstractEvalu accessorArray[accessors.size() + 1] = DataBindingPropertyAccessor.forReadOnlyAccess(); SimpleEvaluationContext evaluationContext = SimpleEvaluationContext.forPropertyAccessors(accessorArray) + .withIndexAccessors(getIndexAccessors().values().toArray(new IndexAccessor[0])) .withTypeConverter(getTypeConverter()) .withInstanceMethods() .withAssignmentDisabled() diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java index 0e8c9cdf72..5116444d5b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2024 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,13 +16,10 @@ package org.springframework.integration.config.xml; +import java.util.List; import java.util.Map; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; 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.BeanReference; @@ -33,7 +30,9 @@ import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.expression.SpelPropertyAccessorRegistrar; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; /** * Parser for the <spel-property-accessors> element. @@ -46,69 +45,61 @@ import org.springframework.util.StringUtils; */ public class SpelPropertyAccessorsParser implements BeanDefinitionParser { - private final Lock lock = new ReentrantLock(); - - private final Map propertyAccessors = new ManagedMap(); - @Override public BeanDefinition parse(Element element, ParserContext parserContext) { - initializeSpelPropertyAccessorRegistrarIfNecessary(parserContext); + Map propertyAccessors = new ManagedMap<>(); + Map indexAccessors = new ManagedMap<>(); + parseTargetedAccessors(element, parserContext, propertyAccessors); - BeanDefinitionParserDelegate delegate = parserContext.getDelegate(); - - NodeList children = element.getChildNodes(); - - for (int i = 0; i < children.getLength(); i++) { - Node node = children.item(i); - String propertyAccessorName; - Object propertyAccessor; - if (node instanceof Element && - !delegate.nodeNameEquals(node, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)) { - Element ele = (Element) node; - - if (delegate.nodeNameEquals(ele, BeanDefinitionParserDelegate.BEAN_ELEMENT)) { - propertyAccessorName = ele.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE); - if (!StringUtils.hasText(propertyAccessorName)) { - parserContext.getReaderContext() - .error("The '' 'id' attribute is required within 'spel-property-accessors'.", ele); - return null; - } - propertyAccessor = delegate.parseBeanDefinitionElement(ele); - } - else if (delegate.nodeNameEquals(ele, BeanDefinitionParserDelegate.REF_ELEMENT)) { - BeanReference propertyAccessorRef = (BeanReference) delegate.parsePropertySubElement(ele, null); - propertyAccessorName = propertyAccessorRef.getBeanName(); // NOSONAR not null - propertyAccessor = propertyAccessorRef; - } - else { - parserContext.getReaderContext().error("Only '' and '' elements are allowed.", element); - return null; - } - - this.propertyAccessors.put(propertyAccessorName, propertyAccessor); - } + Element indexAccessorsElement = DomUtils.getChildElementByTagName(element, "index-accessors"); + if (indexAccessorsElement != null) { + parseTargetedAccessors(indexAccessorsElement, parserContext, indexAccessors); } + BeanDefinitionBuilder registrarBuilder = + BeanDefinitionBuilder.genericBeanDefinition(SpelPropertyAccessorRegistrar.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + if (!CollectionUtils.isEmpty(propertyAccessors)) { + registrarBuilder.addConstructorArgValue(propertyAccessors); + } + + if (!CollectionUtils.isEmpty(indexAccessors)) { + registrarBuilder.addPropertyValue("indexAccessors", indexAccessors); + } + + parserContext.getRegistry() + .registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME, + registrarBuilder.getBeanDefinition()); + return null; } - private void initializeSpelPropertyAccessorRegistrarIfNecessary(ParserContext parserContext) { - this.lock.lock(); - try { - if (!parserContext.getRegistry() - .containsBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME)) { + private static void parseTargetedAccessors(Element accessorsElement, ParserContext parserContext, + Map accessorsMap) { - BeanDefinitionBuilder registrarBuilder = BeanDefinitionBuilder - .genericBeanDefinition(SpelPropertyAccessorRegistrar.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .addConstructorArgValue(this.propertyAccessors); - parserContext.getRegistry() - .registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME, - registrarBuilder.getBeanDefinition()); + BeanDefinitionParserDelegate delegate = parserContext.getDelegate(); + List accessorElements = DomUtils.getChildElementsByTagName(accessorsElement, + BeanDefinitionParserDelegate.BEAN_ELEMENT, BeanDefinitionParserDelegate.REF_ELEMENT); + for (Element accessorElement : accessorElements) { + String accessorName; + Object accessor; + if (delegate.nodeNameEquals(accessorElement, BeanDefinitionParserDelegate.BEAN_ELEMENT)) { + accessorName = accessorElement.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE); + if (!StringUtils.hasText(accessorName)) { + parserContext.getReaderContext() + .error("The '' 'id' attribute is required within 'spel-property-accessors'.", + accessorElement); + return; + } + accessor = delegate.parseBeanDefinitionElement(accessorElement); } - } - finally { - this.lock.unlock(); + else { + BeanReference propertyAccessorRef = + (BeanReference) delegate.parsePropertySubElement(accessorElement, null); + accessorName = propertyAccessorRef.getBeanName(); // NOSONAR not null + accessor = propertyAccessorRef; + } + accessorsMap.put(accessorName, accessor); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java index 5db0a43245..a3d0dbd328 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/SpelPropertyAccessorRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import java.beans.Introspector; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.expression.IndexAccessor; import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TargetedAccessor; import org.springframework.util.Assert; /** @@ -35,14 +37,16 @@ import org.springframework.util.Assert; */ public class SpelPropertyAccessorRegistrar { - private final Map propertyAccessors = new LinkedHashMap(); + private final Map propertyAccessors = new LinkedHashMap<>(); + + private final Map indexAccessors = new LinkedHashMap<>(); public SpelPropertyAccessorRegistrar() { } /** - * Create an instance with the provided property accessors. Each accessor name - * will be the class simple name. + * Create an instance with the provided {@link PropertyAccessor} instances. + * Each accessor name will be the class simple name. * @param propertyAccessors the accessors. * @since 4.3.8 */ @@ -54,7 +58,7 @@ public class SpelPropertyAccessorRegistrar { } /** - * Create an instance with the provided named property accessors. + * Create an instance with the provided named {@link PropertyAccessor} instances. * @param propertyAccessors a map of name:accessor. * @since 4.3.8 */ @@ -64,7 +68,8 @@ public class SpelPropertyAccessorRegistrar { } /** - * Return the registered accessors. + * Return the registered {@link PropertyAccessor} instances to use + * in the target {@link org.springframework.expression.EvaluationContext}. * @return the map of name:accessor. * @since 4.3.8 */ @@ -72,6 +77,26 @@ public class SpelPropertyAccessorRegistrar { return this.propertyAccessors; } + /** + * Add a map of {@link IndexAccessor} instances to use + * in the target {@link org.springframework.expression.EvaluationContext}. + * @param indexAccessors the map of name:accessor. + * @since 6.4 + */ + public void setIndexAccessors(Map indexAccessors) { + Assert.notEmpty(indexAccessors, "'indexAccessors' must not be empty"); + this.indexAccessors.putAll(indexAccessors); + } + + /** + * Return the registered {@link IndexAccessor} instances. + * @return the map of name:accessor. + * @since 6.4 + */ + public Map getIndexAccessors() { + return this.indexAccessors; + } + /** * Add the provided named property accessor. * @param name the name. @@ -101,8 +126,37 @@ public class SpelPropertyAccessorRegistrar { return this; } - private static String obtainAccessorKey(PropertyAccessor propertyAccessor) { - return Introspector.decapitalize(propertyAccessor.getClass().getSimpleName()); + /** + * Add the provided named {@link IndexAccessor}. + * @param name the name. + * @param indexAccessor the accessor. + * @return this registrar. + * @since 6.4 + */ + public SpelPropertyAccessorRegistrar add(String name, IndexAccessor indexAccessor) { + Assert.hasText(name, "'name' must not be empty"); + Assert.notNull(indexAccessor, "'indexAccessor' must not be null"); + this.indexAccessors.put(name, indexAccessor); + return this; + } + + /** + * Add the provided {@link IndexAccessor} instances. + * Each accessor name will be the class simple name. + * @param indexAccessors the accessors. + * @return this registrar. + * @since 6.4 + */ + public SpelPropertyAccessorRegistrar add(IndexAccessor... indexAccessors) { + Assert.notEmpty(indexAccessors, "'indexAccessors' must not be empty"); + for (IndexAccessor indexAccessor : indexAccessors) { + this.indexAccessors.put(obtainAccessorKey(indexAccessor), indexAccessor); + } + return this; + } + + private static String obtainAccessorKey(TargetedAccessor targetedAccessor) { + return Introspector.decapitalize(targetedAccessor.getClass().getSimpleName()); } } diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd index 38a89efd03..403d1ce6d7 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd @@ -4698,10 +4698,20 @@ The list of component name patterns you want to track (e.g., tracked-components - - - - + + + + + + + + + + + + + + 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 12a971b7a2..fd3dc15269 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 @@ -64,7 +64,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogAccessor; import org.springframework.core.serializer.support.SerializingConverter; import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.expression.EvaluationContext; +import org.springframework.expression.IndexAccessor; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.annotation.Aggregator; @@ -112,6 +112,7 @@ import org.springframework.integration.handler.ServiceActivatingHandler; import org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice; import org.springframework.integration.history.MessageHistory; import org.springframework.integration.history.MessageHistoryConfigurer; +import org.springframework.integration.json.JsonIndexAccessor; import org.springframework.integration.json.JsonPropertyAccessor; import org.springframework.integration.scheduling.PollerMetadata; import org.springframework.integration.support.MessageBuilder; @@ -786,15 +787,17 @@ public class EnableIntegrationTests { @Test public void testIntegrationEvaluationContextCustomization() { - EvaluationContext evaluationContext = this.context.getBean(StandardEvaluationContext.class); - List propertyAccessors = TestUtils.getPropertyValue(evaluationContext, "propertyAccessors", List.class); + StandardEvaluationContext evaluationContext = this.context.getBean(StandardEvaluationContext.class); + List propertyAccessors = evaluationContext.getPropertyAccessors(); assertThat(propertyAccessors.size()).isEqualTo(4); assertThat(propertyAccessors.get(0)).isInstanceOf(JsonPropertyAccessor.class); assertThat(propertyAccessors.get(1)).isInstanceOf(EnvironmentAccessor.class); assertThat(propertyAccessors.get(2)).isInstanceOf(MapAccessor.class); assertThat(propertyAccessors.get(3)).isInstanceOf(ReflectivePropertyAccessor.class); - Map variables = TestUtils.getPropertyValue(evaluationContext, "variables", Map.class); - Object testSpelFunction = variables.get("testSpelFunction"); + List indexAccessors = evaluationContext.getIndexAccessors(); + assertThat(indexAccessors.size()).isEqualTo(1); + assertThat(indexAccessors.get(0)).isInstanceOf(JsonIndexAccessor.class); + Object testSpelFunction = evaluationContext.lookupVariable("testSpelFunction"); assertThat(testSpelFunction).isEqualTo(ClassUtils.getStaticMethod(TestSpelFunction.class, "bar", Object.class)); } @@ -1244,7 +1247,9 @@ public class EnableIntegrationTests { @Bean public SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() { - return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor(), new EnvironmentAccessor()); + return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor()) + .add(new EnvironmentAccessor()) + .add(new JsonIndexAccessor()); } @Bean diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests-context.xml index 97aee381f7..a7af2dd863 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests-context.xml @@ -42,10 +42,13 @@ - - - - + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java index b468649333..56a715088b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java @@ -16,10 +16,7 @@ package org.springframework.integration.transformer; -import java.util.Map; - -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -33,13 +30,12 @@ import org.springframework.integration.config.IntegrationEvaluationContextFactor import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.handler.ReplyRequiredException; import org.springframework.integration.support.MessageBuilder; -import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -49,8 +45,8 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Artem Bilan * @author Gary Russell */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class SpelTransformerIntegrationTests { @Autowired @@ -96,7 +92,7 @@ public class SpelTransformerIntegrationTests { @Test public void testInt2755ChainChildIdWithinExceptionMessage() { try { - this.transformerChainInput.send(new GenericMessage("foo")); + this.transformerChainInput.send(new GenericMessage<>("foo")); } catch (ReplyRequiredException e) { assertThat(e.getMessage()).contains("No reply produced by handler 'transformerChain$child#0'"); @@ -108,20 +104,20 @@ public class SpelTransformerIntegrationTests { QueueChannel outputChannel = new QueueChannel(); fooHandler.setOutputChannel(outputChannel); Foo foo = new Foo("baz"); - fooHandler.handleMessage(new GenericMessage(foo)); + fooHandler.handleMessage(new GenericMessage<>(foo)); Message reply = outputChannel.receive(0); assertThat(reply).isNotNull(); assertThat(reply.getPayload() instanceof String).isTrue(); assertThat(reply.getPayload()).isEqualTo("baz"); - assertThat(TestUtils.getPropertyValue(this.evaluationContextFactoryBean, "propertyAccessors", Map.class).size()) - .isEqualTo(3); + assertThat(this.evaluationContextFactoryBean.getPropertyAccessors()).hasSize(3); + assertThat(this.evaluationContextFactoryBean.getIndexAccessors()).hasSize(1); } @Test public void testCustomFunction() { QueueChannel outputChannel = new QueueChannel(); barHandler.setOutputChannel(outputChannel); - barHandler.handleMessage(new GenericMessage("foo")); + barHandler.handleMessage(new GenericMessage<>("foo")); Message reply = outputChannel.receive(0); assertThat(reply).isNotNull(); assertThat(reply.getPayload()).isEqualTo("bar"); @@ -169,7 +165,7 @@ public class SpelTransformerIntegrationTests { } @Override - public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + public boolean canRead(EvaluationContext context, Object target, String name) { return "bar".equals(name); } @@ -180,7 +176,7 @@ public class SpelTransformerIntegrationTests { } @Override - public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + public boolean canWrite(EvaluationContext context, Object target, String name) { return "bar".equals(name); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/property-accessor-import-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/transformer/property-accessor-import-context.xml deleted file mode 100644 index b9b9d372a8..0000000000 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/property-accessor-import-context.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/src/reference/antora/modules/ROOT/pages/spel.adoc b/src/reference/antora/modules/ROOT/pages/spel.adoc index 2324d25638..2fe1ed424a 100644 --- a/src/reference/antora/modules/ROOT/pages/spel.adoc +++ b/src/reference/antora/modules/ROOT/pages/spel.adoc @@ -20,6 +20,11 @@ Starting with Spring Integration 3.0, you can add additional `PropertyAccessor` The framework provides the (read-only) `JsonPropertyAccessor`, which you can use to access fields from a `JsonNode` or JSON in a `String`. You can also create your own `PropertyAccessor` if you have specific needs. +Starting with version 6.4, the `JsonIndexAccessor` implementation is provided that knows how to read indexes from JSON arrays, using Jackson's `ArrayNode` API. +Supports indexes supplied as an integer literal, for example, `myJsonArray[1]`. +Also supports negative indexes, for example, `myJsonArray[-1]` which equates to `myJsonArray[myJsonArray.length - 1]`. +Furthermore, `null` is returned for any index that is out of bounds (see `ArrayNode.get(int)` for details). + In addition, you can add custom functions. Custom functions are `static` methods declared on a class. Functions and property accessors are available in any SpEL expression used throughout the framework. @@ -45,6 +50,9 @@ The following configuration shows how to directly configure the `IntegrationEval ---- +Starting with version 6.4, the `AbstractEvaluationContextFactoryBean` supports an injection of `IndexAccessor` instances. +See `AbstractEvaluationContextFactoryBean` method JavaDocs for more information. + For convenience, Spring Integration provides namespace support for both property accessors and functions, as described in the following sections. The framework automatically configures the factory bean on your behalf. @@ -122,7 +130,7 @@ Each context has its own instance of the `integrationEvaluationContext` factory [[built-in-spel-functions]] === Built-in SpEL Functions -Spring Integration provides the folloiwng standard functions, which are registered with the application context automatically on start up: +Spring Integration provides the following standard functions, which are registered with the application context automatically on start up: * `#jsonPath`: Evaluates a 'jsonPath' on a specified object. This function invokes `JsonPathUtils.evaluate(...)`, which delegates to the https://github.com/json-path/JsonPath[Jayway JsonPath library]. @@ -157,12 +165,16 @@ For more information regarding XML and XPath, see xref:xml.adoc[XML Support - De Spring Integration provides namespace support to let you create SpEL custom https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/expression/PropertyAccessor.html[`PropertyAccessor`] implementations. You can use the `` component to provide a list of custom `PropertyAccessor` instances to the `EvaluationContext` used throughout the framework. -Instead of configuring the factory bean shown earlier, you can add one or more of these components, and the framework automatically adds the accessors to the default `integrationEvaluationContext` factory bean. +Instead of configuring the factory bean shown earlier, you can add this component, and the framework automatically adds the accessors to the default `integrationEvaluationContext` factory bean. +Also, starting with version 6.4, a dedicated `` sub-element is provided to configure `IndexAccessor` beans similar way. The following example shows how to do so: [source,xml] ---- + + + @@ -170,15 +182,17 @@ The following example shows how to do so: In the preceding example, two custom `PropertyAccessor` instances are injected into the `EvaluationContext` (in the order in which they are declared). + To provide `PropertyAccessor` instances by using Java Configuration, you should declare a `SpelPropertyAccessorRegistrar` bean with a name of `spelPropertyAccessorRegistrar` (dictated by the `IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME` constant). -The following example shows how to configure two custom `PropertyAccessor` instances with Java: +The following example shows how to configure two custom `PropertyAccessor` (and `IndexAccessor` starting with version 6.4) instances with Java: [source,java] ---- @Bean public SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() { return new SpelPropertyAccessorRegistrar(new JsonPropertyAccessor()) - .add(fooPropertyAccessor()); + .add(fooPropertyAccessor()) + .add(new JsonIndexAccessor()); } ---- diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index d92bd74f9f..c286857483 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -23,6 +23,10 @@ See xref:control-bus.adoc[Control Bus] for more information. Also, a `ControlBusController` (together with an `@EnableControlBusController`) is introduced for managing exposed commands by the mentioned `ControlBusCommandRegistry`. See xref:http.adoc[HTTP Support] for more information. +The SpEL evaluation infrastructure now supports configuration for `IndexAccessor`. +Also, an out-of-the-box `JsonIndexAccessor` is provided. +See xref:spel.adoc[SpEL Support] for more information. + [[x6.4-general]] === General Changes @@ -74,4 +78,3 @@ See xref:sftp/session-factory.adoc[SFTP Session Factory] for more information. Multiple instances of `MqttPahoMessageDrivenChannelAdapter` and `Mqttv5PahoMessageDrivenChannelAdapter` can now be added at runtime using corresponding `ClientManager` through `IntegrationFlowContext` Also a `MqttMessageNotDeliveredEvent` event has been introduced to emit when action callback reacts to the delivery failure. See xref:mqtt.adoc[MQTT Support] for more information. -