Skip to content

Commit 50a0000

Browse files
ljmn3211sbrannen
ljmn3211
authored andcommitted
Introduce IndexAccessor SPI to customize the SpEL Indexer
See spring-projectsgh-26409 See spring-projectsgh-26478
1 parent d6e9562 commit 50a0000

File tree

7 files changed

+312
-1
lines changed

7 files changed

+312
-1
lines changed

spring-expression/spring-expression.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ dependencies {
88
testImplementation(testFixtures(project(":spring-core")))
99
testImplementation("org.jetbrains.kotlin:kotlin-reflect")
1010
testImplementation("org.jetbrains.kotlin:kotlin-stdlib")
11+
testImplementation("com.fasterxml.jackson.core:jackson-databind")
12+
testImplementation("com.fasterxml.jackson.core:jackson-core")
1113
}

spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public interface EvaluationContext {
5656
*/
5757
List<PropertyAccessor> getPropertyAccessors();
5858

59+
/**
60+
* Return a list of index accessors that will be asked in turn to read/write a property.
61+
*/
62+
List<IndexAccessor> getIndexAccessors();
63+
5964
/**
6065
* Return a list of resolvers that will be asked in turn to locate a constructor.
6166
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression;
18+
19+
import org.springframework.expression.spel.ast.ValueRef;
20+
import org.springframework.lang.Nullable;
21+
22+
/**
23+
* A index accessor is able to read from (and possibly write to) an array's elements.
24+
*
25+
* <p>This interface places no restrictions, and so implementors are free to access elements
26+
* directly as fields or through getters or in any other way they see as appropriate.
27+
*
28+
* <p>A resolver can optionally specify an array of target classes for which it should be
29+
* called. However, if it returns {@code null} from {@link #getSpecificTargetClasses()},
30+
* it will be called for all property references and given a chance to determine if it
31+
* can read or write them.
32+
*
33+
* <p>Property resolvers are considered to be ordered, and each will be called in turn.
34+
* The only rule that affects the call order is that any resolver naming the target
35+
* class directly in {@link #getSpecificTargetClasses()} will be called first, before
36+
* the general resolvers.
37+
*
38+
* @author jackmiking lee
39+
* @since 3.0
40+
*/
41+
public interface IndexAccessor {
42+
/**
43+
* Return an array of classes for which this resolver should be called.
44+
* <p>Returning {@code null} indicates this is a general resolver that
45+
* can be called in an attempt to resolve a property on any type.
46+
* @return an array of classes that this resolver is suitable for
47+
* (or {@code null} if a general resolver)
48+
*/
49+
@Nullable
50+
Class<?>[] getSpecificTargetClasses();
51+
52+
/**
53+
* Called to determine if a resolver instance is able to access a specified property
54+
* on a specified target object.
55+
* @param context the evaluation context in which the access is being attempted
56+
* @param target the target object upon which the property is being accessed
57+
* @param index the index of the array being accessed
58+
* @return true if this resolver is able to read the property
59+
* @throws AccessException if there is any problem determining whether the property can be read
60+
*/
61+
boolean canRead(EvaluationContext context, @Nullable Object target, Object index) throws AccessException;
62+
63+
/**
64+
* Called to read a property from a specified target object.
65+
* Should only succeed if {@link #canRead} also returns {@code true}.
66+
* @param context the evaluation context in which the access is being attempted
67+
* @param target the target object upon which the property is being accessed
68+
* @param index the index of the array being accessed
69+
* @return a TypedValue object wrapping the property value read and a type descriptor for it
70+
* @throws AccessException if there is any problem accessing the property value
71+
*/
72+
ValueRef read(EvaluationContext context, @Nullable Object target,Object index) throws AccessException;
73+
74+
/**
75+
* Called to determine if a resolver instance is able to write to a specified
76+
* property on a specified target object.
77+
* @param context the evaluation context in which the access is being attempted
78+
* @param target the target object upon which the property is being accessed
79+
* @param index the index of the array being accessed
80+
* @return true if this resolver is able to write to the property
81+
* @throws AccessException if there is any problem determining whether the
82+
* property can be written to
83+
*/
84+
boolean canWrite(EvaluationContext context, @Nullable Object target, Object index) throws AccessException;
85+
86+
/**
87+
* Called to write to a property on a specified target object.
88+
* Should only succeed if {@link #canWrite} also returns {@code true}.
89+
* @param context the evaluation context in which the access is being attempted
90+
* @param target the target object upon which the property is being accessed
91+
* @param index the index of the array being accessed
92+
* @param newValue the new value for the property
93+
* @throws AccessException if there is any problem writing to the property value
94+
*/
95+
void write(EvaluationContext context, @Nullable Object target, Object index, @Nullable Object newValue)
96+
throws AccessException;
97+
}

spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java

+66-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package org.springframework.expression.spel.ast;
1818

1919
import java.lang.reflect.Constructor;
20+
import java.util.ArrayList;
2021
import java.util.Collection;
2122
import java.util.List;
2223
import java.util.Map;
24+
import java.util.Optional;
2325
import java.util.function.Supplier;
2426

2527
import org.springframework.asm.Label;
@@ -28,6 +30,7 @@
2830
import org.springframework.expression.AccessException;
2931
import org.springframework.expression.EvaluationContext;
3032
import org.springframework.expression.EvaluationException;
33+
import org.springframework.expression.IndexAccessor;
3134
import org.springframework.expression.PropertyAccessor;
3235
import org.springframework.expression.TypeConverter;
3336
import org.springframework.expression.TypedValue;
@@ -246,11 +249,73 @@ else if (target instanceof Collection<?> collection) {
246249
return new PropertyIndexingValueRef(
247250
target, (String) index, state.getEvaluationContext(), targetDescriptor);
248251
}
249-
252+
Optional<ValueRef> optional = tryIndexAccessor(state, index);
253+
if (optional.isPresent()) {
254+
return optional.get();
255+
}
250256
throw new SpelEvaluationException(
251257
getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor);
252258
}
253259

260+
private Optional<ValueRef> tryIndexAccessor(ExpressionState state, Object index) {
261+
EvaluationContext context = state.getEvaluationContext();
262+
Object target = state.getActiveContextObject().getValue();
263+
if (context != null) {
264+
List<IndexAccessor> list = context.getIndexAccessors();
265+
if (list != null) {
266+
List<IndexAccessor> availableAccessors = getIndexAccessorsToTry(state.getActiveContextObject(), list);
267+
try {
268+
for (IndexAccessor indexAccessor : availableAccessors) {
269+
if (indexAccessor.canRead(context, target, index)) {
270+
ValueRef valueRef = indexAccessor.read(context, target, index);
271+
return Optional.of(valueRef);
272+
}
273+
}
274+
}
275+
catch (Exception ex) {
276+
}
277+
}
278+
}
279+
return Optional.empty();
280+
}
281+
282+
private List<IndexAccessor> getIndexAccessorsToTry(
283+
@Nullable Object contextObject, List<IndexAccessor> propertyAccessors) {
284+
285+
Class<?> targetType;
286+
if (contextObject instanceof TypedValue) {
287+
targetType = ((TypedValue) contextObject).getTypeDescriptor().getObjectType();
288+
}
289+
else {
290+
targetType = (contextObject != null ? contextObject.getClass() : null);
291+
}
292+
293+
List<IndexAccessor> specificAccessors = new ArrayList<>();
294+
List<IndexAccessor> generalAccessors = new ArrayList<>();
295+
for (IndexAccessor resolver : propertyAccessors) {
296+
Class<?>[] targets = resolver.getSpecificTargetClasses();
297+
if (targets == null) {
298+
// generic resolver that says it can be used for any type
299+
generalAccessors.add(resolver);
300+
}
301+
else if (targetType != null) {
302+
for (Class<?> clazz : targets) {
303+
if (clazz == targetType) {
304+
specificAccessors.add(resolver);
305+
break;
306+
}
307+
else if (clazz.isAssignableFrom(targetType)) {
308+
generalAccessors.add(resolver);
309+
}
310+
}
311+
}
312+
}
313+
List<IndexAccessor> resolvers = new ArrayList<>(specificAccessors);
314+
generalAccessors.removeAll(specificAccessors);
315+
resolvers.addAll(generalAccessors);
316+
return resolvers;
317+
}
318+
254319
@Override
255320
public boolean isCompilable() {
256321
if (this.indexedType == IndexedType.ARRAY) {

spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.expression.BeanResolver;
2929
import org.springframework.expression.ConstructorResolver;
3030
import org.springframework.expression.EvaluationContext;
31+
import org.springframework.expression.IndexAccessor;
3132
import org.springframework.expression.MethodResolver;
3233
import org.springframework.expression.OperatorOverloader;
3334
import org.springframework.expression.PropertyAccessor;
@@ -146,6 +147,11 @@ public List<PropertyAccessor> getPropertyAccessors() {
146147
return this.propertyAccessors;
147148
}
148149

150+
@Override
151+
public List<IndexAccessor> getIndexAccessors() {
152+
return null;
153+
}
154+
149155
/**
150156
* Return an empty list, always, since this context does not support the
151157
* use of type references.

spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java

+30
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.expression.BeanResolver;
2828
import org.springframework.expression.ConstructorResolver;
2929
import org.springframework.expression.EvaluationContext;
30+
import org.springframework.expression.IndexAccessor;
3031
import org.springframework.expression.MethodFilter;
3132
import org.springframework.expression.MethodResolver;
3233
import org.springframework.expression.OperatorOverloader;
@@ -83,6 +84,9 @@ public class StandardEvaluationContext implements EvaluationContext {
8384
@Nullable
8485
private volatile List<PropertyAccessor> propertyAccessors;
8586

87+
@Nullable
88+
private volatile List<IndexAccessor> indexAccessors;
89+
8690
@Nullable
8791
private volatile List<ConstructorResolver> constructorResolvers;
8892

@@ -142,6 +146,10 @@ public void setPropertyAccessors(List<PropertyAccessor> propertyAccessors) {
142146
this.propertyAccessors = propertyAccessors;
143147
}
144148

149+
public void setIndexAccessors(List<IndexAccessor>indexAccessors){
150+
this.indexAccessors=indexAccessors;
151+
}
152+
145153
@Override
146154
public List<PropertyAccessor> getPropertyAccessors() {
147155
return initPropertyAccessors();
@@ -155,6 +163,14 @@ public boolean removePropertyAccessor(PropertyAccessor accessor) {
155163
return initPropertyAccessors().remove(accessor);
156164
}
157165

166+
public void addIndexAccessor(IndexAccessor accessor){
167+
initIndexAccessors().add(accessor);
168+
}
169+
170+
public boolean removeIndexAccessor(IndexAccessor indexAccessor){
171+
return initIndexAccessors().remove(indexAccessor);
172+
}
173+
158174
public void setConstructorResolvers(List<ConstructorResolver> constructorResolvers) {
159175
this.constructorResolvers = constructorResolvers;
160176
}
@@ -404,6 +420,15 @@ private List<PropertyAccessor> initPropertyAccessors() {
404420
return accessors;
405421
}
406422

423+
private List<IndexAccessor>initIndexAccessors(){
424+
List<IndexAccessor> accessors = this.indexAccessors;
425+
if(accessors == null){
426+
accessors = new ArrayList<>(5);
427+
this.indexAccessors = accessors;
428+
}
429+
return accessors;
430+
}
431+
407432
private List<ConstructorResolver> initConstructorResolvers() {
408433
List<ConstructorResolver> resolvers = this.constructorResolvers;
409434
if (resolvers == null) {
@@ -429,4 +454,9 @@ private static <T> void addBeforeDefault(List<T> list, T element) {
429454
list.add(list.size() - 1, element);
430455
}
431456

457+
@Override
458+
public List<IndexAccessor> getIndexAccessors() {
459+
return initIndexAccessors();
460+
}
461+
432462
}

0 commit comments

Comments
 (0)