Skip to content

Commit e3a46ca

Browse files
authored
GH-9436: Add support for SpEL IndexAccessor configuration (#9451)
Fixes: #9436 Issue link: #9436 * Expose `IndexAccessor` configuration options on the `AbstractEvaluationContextFactoryBean` and `SpelPropertyAccessorRegistrar` * Expose `<index-accessors>` sub-element for the `<spel-property-accessors>` * Adjust tests * Document the feature, including recently added `JsonIndexAccessor`
1 parent 775bfd6 commit e3a46ca

File tree

12 files changed

+263
-146
lines changed

12 files changed

+263
-146
lines changed

spring-integration-core/src/main/java/org/springframework/integration/config/AbstractEvaluationContextFactoryBean.java

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018-2019 the original author or authors.
2+
* Copyright 2018-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,11 +28,13 @@
2828
import org.springframework.context.ApplicationContext;
2929
import org.springframework.context.ApplicationContextAware;
3030
import org.springframework.core.convert.ConversionService;
31+
import org.springframework.expression.IndexAccessor;
3132
import org.springframework.expression.PropertyAccessor;
3233
import org.springframework.expression.TypeConverter;
3334
import org.springframework.expression.spel.support.StandardTypeConverter;
3435
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
3536
import org.springframework.integration.support.utils.IntegrationUtils;
37+
import org.springframework.lang.Nullable;
3638
import org.springframework.util.Assert;
3739

3840
/**
@@ -45,14 +47,19 @@
4547
*/
4648
public abstract class AbstractEvaluationContextFactoryBean implements ApplicationContextAware, InitializingBean {
4749

48-
private Map<String, PropertyAccessor> propertyAccessors = new LinkedHashMap<String, PropertyAccessor>();
50+
private Map<String, PropertyAccessor> propertyAccessors = new LinkedHashMap<>();
4951

50-
private Map<String, Method> functions = new LinkedHashMap<String, Method>();
52+
private Map<String, IndexAccessor> indexAccessors = new LinkedHashMap<>();
53+
54+
private Map<String, Method> functions = new LinkedHashMap<>();
5155

5256
private TypeConverter typeConverter = new StandardTypeConverter();
5357

5458
private ApplicationContext applicationContext;
5559

60+
@Nullable
61+
private SpelPropertyAccessorRegistrar propertyAccessorRegistrar;
62+
5663
private boolean initialized;
5764

5865
protected TypeConverter getTypeConverter() {
@@ -68,22 +75,45 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
6875
this.applicationContext = applicationContext;
6976
}
7077

71-
public void setPropertyAccessors(Map<String, PropertyAccessor> accessors) {
78+
public void setPropertyAccessors(Map<String, PropertyAccessor> propertyAccessors) {
7279
Assert.isTrue(!this.initialized, "'propertyAccessors' can't be changed after initialization.");
73-
Assert.notNull(accessors, "'accessors' must not be null.");
74-
Assert.noNullElements(accessors.values().toArray(), "'accessors' cannot have null values.");
75-
this.propertyAccessors = new LinkedHashMap<String, PropertyAccessor>(accessors);
80+
Assert.notNull(propertyAccessors, "'propertyAccessors' must not be null.");
81+
Assert.noNullElements(propertyAccessors.values().toArray(), "'propertyAccessors' cannot have null values.");
82+
this.propertyAccessors = new LinkedHashMap<>(propertyAccessors);
7683
}
7784

7885
public Map<String, PropertyAccessor> getPropertyAccessors() {
7986
return this.propertyAccessors;
8087
}
8188

89+
/**
90+
* Set a map of {@link IndexAccessor}s to use in the target {@link org.springframework.expression.EvaluationContext}
91+
* @param indexAccessors the map of {@link IndexAccessor}s to use
92+
* @since 6.4
93+
* @see org.springframework.expression.EvaluationContext#getIndexAccessors()
94+
*/
95+
public void setIndexAccessors(Map<String, IndexAccessor> indexAccessors) {
96+
Assert.isTrue(!this.initialized, "'indexAccessors' can't be changed after initialization.");
97+
Assert.notNull(indexAccessors, "'indexAccessors' must not be null.");
98+
Assert.noNullElements(indexAccessors.values().toArray(), "'indexAccessors' cannot have null values.");
99+
this.indexAccessors = new LinkedHashMap<>(indexAccessors);
100+
}
101+
102+
/**
103+
* Return the map of {@link IndexAccessor}s to use in the target {@link org.springframework.expression.EvaluationContext}
104+
* @return the map of {@link IndexAccessor}s to use
105+
* @since 6.4
106+
* @see org.springframework.expression.EvaluationContext#getIndexAccessors()
107+
*/
108+
public Map<String, IndexAccessor> getIndexAccessors() {
109+
return this.indexAccessors;
110+
}
111+
82112
public void setFunctions(Map<String, Method> functionsArg) {
83113
Assert.isTrue(!this.initialized, "'functions' can't be changed after initialization.");
84114
Assert.notNull(functionsArg, "'functions' must not be null.");
85115
Assert.noNullElements(functionsArg.values().toArray(), "'functions' cannot have null values.");
86-
this.functions = new LinkedHashMap<String, Method>(functionsArg);
116+
this.functions = new LinkedHashMap<>(functionsArg);
87117
}
88118

89119
public Map<String, Method> getFunctions() {
@@ -94,7 +124,14 @@ protected void initialize(String beanName) {
94124
if (this.applicationContext != null) {
95125
conversionService();
96126
functions();
127+
try {
128+
this.propertyAccessorRegistrar = this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class);
129+
}
130+
catch (@SuppressWarnings("unused") NoSuchBeanDefinitionException e) {
131+
// There is no 'SpelPropertyAccessorRegistrar' bean in the application context.
132+
}
97133
propertyAccessors();
134+
indexAccessors();
98135
processParentIfPresent(beanName);
99136
}
100137
this.initialized = true;
@@ -108,28 +145,43 @@ private void conversionService() {
108145
}
109146

110147
private void functions() {
111-
Map<String, SpelFunctionFactoryBean> functionFactoryBeanMap = BeanFactoryUtils
112-
.beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class);
113-
for (SpelFunctionFactoryBean spelFunctionFactoryBean : functionFactoryBeanMap.values()) {
114-
if (!getFunctions().containsKey(spelFunctionFactoryBean.getFunctionName())) {
115-
getFunctions().put(spelFunctionFactoryBean.getFunctionName(), spelFunctionFactoryBean.getObject());
148+
Map<String, SpelFunctionFactoryBean> spelFunctions =
149+
BeanFactoryUtils.beansOfTypeIncludingAncestors(this.applicationContext, SpelFunctionFactoryBean.class);
150+
for (SpelFunctionFactoryBean spelFunctionFactoryBean : spelFunctions.values()) {
151+
String functionName = spelFunctionFactoryBean.getFunctionName();
152+
if (!this.functions.containsKey(functionName)) {
153+
this.functions.put(functionName, spelFunctionFactoryBean.getObject());
116154
}
117155
}
118156
}
119157

120158
private void propertyAccessors() {
121-
try {
122-
SpelPropertyAccessorRegistrar propertyAccessorRegistrar =
123-
this.applicationContext.getBean(SpelPropertyAccessorRegistrar.class);
124-
for (Entry<String, PropertyAccessor> entry : propertyAccessorRegistrar.getPropertyAccessors()
125-
.entrySet()) {
126-
if (!getPropertyAccessors().containsKey(entry.getKey())) {
127-
getPropertyAccessors().put(entry.getKey(), entry.getValue());
128-
}
159+
if (this.propertyAccessorRegistrar != null) {
160+
propertyAccessors(this.propertyAccessorRegistrar.getPropertyAccessors());
161+
}
162+
}
163+
164+
private void propertyAccessors(Map<String, PropertyAccessor> propertyAccessors) {
165+
for (Entry<String, PropertyAccessor> entry : propertyAccessors.entrySet()) {
166+
String key = entry.getKey();
167+
if (!this.propertyAccessors.containsKey(key)) {
168+
this.propertyAccessors.put(key, entry.getValue());
129169
}
130170
}
131-
catch (@SuppressWarnings("unused") NoSuchBeanDefinitionException e) {
132-
// There is no 'SpelPropertyAccessorRegistrar' bean in the application context.
171+
}
172+
173+
private void indexAccessors() {
174+
if (this.propertyAccessorRegistrar != null) {
175+
indexAccessors(this.propertyAccessorRegistrar.getIndexAccessors());
176+
}
177+
}
178+
179+
private void indexAccessors(Map<String, IndexAccessor> indexAccessors) {
180+
for (Entry<String, IndexAccessor> entry : indexAccessors.entrySet()) {
181+
String key = entry.getKey();
182+
if (!this.indexAccessors.containsKey(key)) {
183+
this.indexAccessors.put(key, entry.getValue());
184+
}
133185
}
134186
}
135187

@@ -138,16 +190,12 @@ private void processParentIfPresent(String beanName) {
138190

139191
if (parent != null && parent.containsBean(beanName)) {
140192
AbstractEvaluationContextFactoryBean parentFactoryBean = parent.getBean("&" + beanName, getClass());
141-
142-
for (Entry<String, PropertyAccessor> entry : parentFactoryBean.getPropertyAccessors().entrySet()) {
143-
if (!getPropertyAccessors().containsKey(entry.getKey())) {
144-
getPropertyAccessors().put(entry.getKey(), entry.getValue());
145-
}
146-
}
147-
193+
propertyAccessors(parentFactoryBean.getPropertyAccessors());
194+
indexAccessors(parentFactoryBean.getIndexAccessors());
148195
for (Entry<String, Method> entry : parentFactoryBean.getFunctions().entrySet()) {
149-
if (!getFunctions().containsKey(entry.getKey())) {
150-
getFunctions().put(entry.getKey(), entry.getValue());
196+
String key = entry.getKey();
197+
if (!this.functions.containsKey(key)) {
198+
this.functions.put(key, entry.getValue());
151199
}
152200
}
153201
}

spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2019 the original author or authors.
2+
* Copyright 2013-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import org.springframework.context.expression.BeanFactoryResolver;
2424
import org.springframework.context.expression.MapAccessor;
2525
import org.springframework.expression.BeanResolver;
26+
import org.springframework.expression.IndexAccessor;
2627
import org.springframework.expression.PropertyAccessor;
2728
import org.springframework.expression.TypeLocator;
2829
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -102,6 +103,10 @@ public StandardEvaluationContext getObject() {
102103

103104
evaluationContext.addPropertyAccessor(new MapAccessor());
104105

106+
for (IndexAccessor indexAccessor : getIndexAccessors().values()) {
107+
evaluationContext.addIndexAccessor(indexAccessor);
108+
}
109+
105110
for (Entry<String, Method> functionEntry : getFunctions().entrySet()) {
106111
evaluationContext.registerFunction(functionEntry.getKey(), functionEntry.getValue());
107112
}

spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import org.springframework.beans.factory.FactoryBean;
2424
import org.springframework.context.expression.MapAccessor;
25+
import org.springframework.expression.IndexAccessor;
2526
import org.springframework.expression.PropertyAccessor;
2627
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
2728
import org.springframework.expression.spel.support.SimpleEvaluationContext;
@@ -80,6 +81,7 @@ public SimpleEvaluationContext getObject() {
8081
accessorArray[accessors.size() + 1] = DataBindingPropertyAccessor.forReadOnlyAccess();
8182
SimpleEvaluationContext evaluationContext =
8283
SimpleEvaluationContext.forPropertyAccessors(accessorArray)
84+
.withIndexAccessors(getIndexAccessors().values().toArray(new IndexAccessor[0]))
8385
.withTypeConverter(getTypeConverter())
8486
.withInstanceMethods()
8587
.withAssignmentDisabled()

spring-integration-core/src/main/java/org/springframework/integration/config/xml/SpelPropertyAccessorsParser.java

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2023 the original author or authors.
2+
* Copyright 2013-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,10 @@
1616

1717
package org.springframework.integration.config.xml;
1818

19+
import java.util.List;
1920
import java.util.Map;
20-
import java.util.concurrent.locks.Lock;
21-
import java.util.concurrent.locks.ReentrantLock;
2221

2322
import org.w3c.dom.Element;
24-
import org.w3c.dom.Node;
25-
import org.w3c.dom.NodeList;
2623

2724
import org.springframework.beans.factory.config.BeanDefinition;
2825
import org.springframework.beans.factory.config.BeanReference;
@@ -33,7 +30,9 @@
3330
import org.springframework.beans.factory.xml.ParserContext;
3431
import org.springframework.integration.context.IntegrationContextUtils;
3532
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
33+
import org.springframework.util.CollectionUtils;
3634
import org.springframework.util.StringUtils;
35+
import org.springframework.util.xml.DomUtils;
3736

3837
/**
3938
* Parser for the &lt;spel-property-accessors&gt; element.
@@ -46,69 +45,61 @@
4645
*/
4746
public class SpelPropertyAccessorsParser implements BeanDefinitionParser {
4847

49-
private final Lock lock = new ReentrantLock();
50-
51-
private final Map<String, Object> propertyAccessors = new ManagedMap<String, Object>();
52-
5348
@Override
5449
public BeanDefinition parse(Element element, ParserContext parserContext) {
55-
initializeSpelPropertyAccessorRegistrarIfNecessary(parserContext);
56-
57-
BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
50+
Map<String, Object> propertyAccessors = new ManagedMap<>();
51+
Map<String, Object> indexAccessors = new ManagedMap<>();
52+
parseTargetedAccessors(element, parserContext, propertyAccessors);
5853

59-
NodeList children = element.getChildNodes();
60-
61-
for (int i = 0; i < children.getLength(); i++) {
62-
Node node = children.item(i);
63-
String propertyAccessorName;
64-
Object propertyAccessor;
65-
if (node instanceof Element &&
66-
!delegate.nodeNameEquals(node, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)) {
67-
Element ele = (Element) node;
54+
Element indexAccessorsElement = DomUtils.getChildElementByTagName(element, "index-accessors");
55+
if (indexAccessorsElement != null) {
56+
parseTargetedAccessors(indexAccessorsElement, parserContext, indexAccessors);
57+
}
6858

69-
if (delegate.nodeNameEquals(ele, BeanDefinitionParserDelegate.BEAN_ELEMENT)) {
70-
propertyAccessorName = ele.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE);
71-
if (!StringUtils.hasText(propertyAccessorName)) {
72-
parserContext.getReaderContext()
73-
.error("The '<bean>' 'id' attribute is required within 'spel-property-accessors'.", ele);
74-
return null;
75-
}
76-
propertyAccessor = delegate.parseBeanDefinitionElement(ele);
77-
}
78-
else if (delegate.nodeNameEquals(ele, BeanDefinitionParserDelegate.REF_ELEMENT)) {
79-
BeanReference propertyAccessorRef = (BeanReference) delegate.parsePropertySubElement(ele, null);
80-
propertyAccessorName = propertyAccessorRef.getBeanName(); // NOSONAR not null
81-
propertyAccessor = propertyAccessorRef;
82-
}
83-
else {
84-
parserContext.getReaderContext().error("Only '<bean>' and '<ref>' elements are allowed.", element);
85-
return null;
86-
}
59+
BeanDefinitionBuilder registrarBuilder =
60+
BeanDefinitionBuilder.genericBeanDefinition(SpelPropertyAccessorRegistrar.class)
61+
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
62+
if (!CollectionUtils.isEmpty(propertyAccessors)) {
63+
registrarBuilder.addConstructorArgValue(propertyAccessors);
64+
}
8765

88-
this.propertyAccessors.put(propertyAccessorName, propertyAccessor);
89-
}
66+
if (!CollectionUtils.isEmpty(indexAccessors)) {
67+
registrarBuilder.addPropertyValue("indexAccessors", indexAccessors);
9068
}
9169

70+
parserContext.getRegistry()
71+
.registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME,
72+
registrarBuilder.getBeanDefinition());
73+
9274
return null;
9375
}
9476

95-
private void initializeSpelPropertyAccessorRegistrarIfNecessary(ParserContext parserContext) {
96-
this.lock.lock();
97-
try {
98-
if (!parserContext.getRegistry()
99-
.containsBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME)) {
77+
private static void parseTargetedAccessors(Element accessorsElement, ParserContext parserContext,
78+
Map<String, Object> accessorsMap) {
10079

101-
BeanDefinitionBuilder registrarBuilder = BeanDefinitionBuilder
102-
.genericBeanDefinition(SpelPropertyAccessorRegistrar.class)
103-
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
104-
.addConstructorArgValue(this.propertyAccessors);
105-
parserContext.getRegistry()
106-
.registerBeanDefinition(IntegrationContextUtils.SPEL_PROPERTY_ACCESSOR_REGISTRAR_BEAN_NAME,
107-
registrarBuilder.getBeanDefinition());
80+
BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
81+
List<Element> accessorElements = DomUtils.getChildElementsByTagName(accessorsElement,
82+
BeanDefinitionParserDelegate.BEAN_ELEMENT, BeanDefinitionParserDelegate.REF_ELEMENT);
83+
for (Element accessorElement : accessorElements) {
84+
String accessorName;
85+
Object accessor;
86+
if (delegate.nodeNameEquals(accessorElement, BeanDefinitionParserDelegate.BEAN_ELEMENT)) {
87+
accessorName = accessorElement.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE);
88+
if (!StringUtils.hasText(accessorName)) {
89+
parserContext.getReaderContext()
90+
.error("The '<bean>' 'id' attribute is required within 'spel-property-accessors'.",
91+
accessorElement);
92+
return;
93+
}
94+
accessor = delegate.parseBeanDefinitionElement(accessorElement);
10895
}
109-
}
110-
finally {
111-
this.lock.unlock();
96+
else {
97+
BeanReference propertyAccessorRef =
98+
(BeanReference) delegate.parsePropertySubElement(accessorElement, null);
99+
accessorName = propertyAccessorRef.getBeanName(); // NOSONAR not null
100+
accessor = propertyAccessorRef;
101+
}
102+
accessorsMap.put(accessorName, accessor);
112103
}
113104
}
114105

0 commit comments

Comments
 (0)