Skip to content

Commit ae3dc0d

Browse files
committed
Merge SpEL IndexAccessor feature into main
This set of commits introduces a new IndexAccessor SPI for the Spring Expression Language (SpEL) which allows third parties to customize the SpEL Indexer. A custom IndexAccessor implementation can be registered in a StandardEvaluationContext. For an example, see the JacksonArrayNodeIndexAccessor in IndexingTests in the spring-expression module. See gh-26409 Closes gh-26478
2 parents d6e9562 + 1c9cff6 commit ae3dc0d

File tree

10 files changed

+878
-75
lines changed

10 files changed

+878
-75
lines changed

spring-expression/spring-expression.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ 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")
1112
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.expression;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021
import java.util.function.Supplier;
2122

@@ -56,6 +57,16 @@ public interface EvaluationContext {
5657
*/
5758
List<PropertyAccessor> getPropertyAccessors();
5859

60+
/**
61+
* Return a list of index accessors that will be asked in turn to access or
62+
* set an indexed value.
63+
* <p>The default implementation returns an empty list.
64+
* @since 6.2
65+
*/
66+
default List<IndexAccessor> getIndexAccessors() {
67+
return Collections.emptyList();
68+
}
69+
5970
/**
6071
* Return a list of resolvers that will be asked in turn to locate a constructor.
6172
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2002-2024 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.lang.Nullable;
20+
21+
/**
22+
* An index accessor is able to read from (and possibly write to) an indexed
23+
* structure of an object.
24+
*
25+
* <p>This interface places no restrictions on what constitutes an indexed
26+
* structure. Implementors are therefore free to access indexed values any way
27+
* they deem appropriate.
28+
*
29+
* <p>An index accessor can optionally specify an array of target classes for
30+
* which it should be called. However, if it returns {@code null} or an empty
31+
* array from {@link #getSpecificTargetClasses()}, it will be called for all
32+
* indexing operations and given a chance to determine if it can read from or
33+
* write to the indexed structure.
34+
*
35+
* <p>Index accessors are considered to be ordered, and each will be called in
36+
* turn. The only rule that affects the call order is that any index accessor
37+
* which specifies explicit support for the target class via
38+
* {@link #getSpecificTargetClasses()} will be called first, before other
39+
* generic index accessors.
40+
*
41+
* @author Jackmiking Lee
42+
* @author Sam Brannen
43+
* @since 6.2
44+
* @see PropertyAccessor
45+
*/
46+
public interface IndexAccessor extends TargetedAccessor {
47+
48+
/**
49+
* Get the set of classes for which this index accessor should be called.
50+
* <p>Returning {@code null} or an empty array indicates this is a generic
51+
* index accessor that can be called in an attempt to access an index on any
52+
* type.
53+
* @return an array of classes that this index accessor is suitable for
54+
* (or {@code null} or an empty array if a generic index accessor)
55+
*/
56+
@Override
57+
@Nullable
58+
Class<?>[] getSpecificTargetClasses();
59+
60+
/**
61+
* Determine if this index accessor is able to read a specified index on a
62+
* specified target object.
63+
* @param context the evaluation context in which the access is being attempted
64+
* @param target the target object upon which the index is being accessed
65+
* @param index the index being accessed
66+
* @return {@code true} if this index accessor is able to read the index
67+
* @throws AccessException if there is any problem determining whether the
68+
* index can be read
69+
*/
70+
boolean canRead(EvaluationContext context, Object target, Object index) throws AccessException;
71+
72+
/**
73+
* Read an index from a specified target object.
74+
* <p>Should only be invoked if {@link #canRead} returns {@code true} for the
75+
* same arguments.
76+
* @param context the evaluation context in which the access is being attempted
77+
* @param target the target object upon which the index is being accessed
78+
* @param index the index being accessed
79+
* @return a TypedValue object wrapping the index value read and a type
80+
* descriptor for the value
81+
* @throws AccessException if there is any problem reading the index
82+
*/
83+
TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException;
84+
85+
/**
86+
* Determine if this index accessor is able to write to a specified index on
87+
* a specified target object.
88+
* @param context the evaluation context in which the access is being attempted
89+
* @param target the target object upon which the index is being accessed
90+
* @param index the index being accessed
91+
* @return {@code true} if this index accessor is able to write to the index
92+
* @throws AccessException if there is any problem determining whether the
93+
* index can be written to
94+
*/
95+
boolean canWrite(EvaluationContext context, Object target, Object index) throws AccessException;
96+
97+
/**
98+
* Write to an index on a specified target object.
99+
* <p>Should only be invoked if {@link #canWrite} returns {@code true} for the
100+
* same arguments.
101+
* @param context the evaluation context in which the access is being attempted
102+
* @param target the target object upon which the index is being accessed
103+
* @param index the index being accessed
104+
* @param newValue the new value for the index
105+
* @throws AccessException if there is any problem writing to the index
106+
*/
107+
void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue)
108+
throws AccessException;
109+
110+
}

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

+9-6
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,24 @@
3434
* <p>Property accessors are considered to be ordered, and each will be called in
3535
* turn. The only rule that affects the call order is that any property accessor
3636
* which specifies explicit support for the target class via
37-
* {@link #getSpecificTargetClasses()} will be called first, before the general
37+
* {@link #getSpecificTargetClasses()} will be called first, before the generic
3838
* property accessors.
3939
*
4040
* @author Andy Clement
4141
* @since 3.0
42+
* @see IndexAccessor
4243
*/
43-
public interface PropertyAccessor {
44+
public interface PropertyAccessor extends TargetedAccessor {
4445

4546
/**
46-
* Return an array of classes for which this property accessor should be called.
47-
* <p>Returning {@code null} indicates this is a general property accessor that
48-
* can be called in an attempt to access a property on any type.
47+
* Get the set of classes for which this property accessor should be called.
48+
* <p>Returning {@code null} or an empty array indicates this is a generic
49+
* property accessor that can be called in an attempt to access a property on
50+
* any type.
4951
* @return an array of classes that this property accessor is suitable for
50-
* (or {@code null} if a general property accessor)
52+
* (or {@code null} if a generic property accessor)
5153
*/
54+
@Override
5255
@Nullable
5356
Class<?>[] getSpecificTargetClasses();
5457

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2002-2024 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.lang.Nullable;
20+
21+
/**
22+
* Strategy for types that access elements of specific target classes.
23+
*
24+
* <p>This interface places no restrictions on what constitutes an element.
25+
*
26+
* <p>A targeted accessor can specify a set of target classes for which it should
27+
* be called. However, if it returns {@code null} or an empty array from
28+
* {@link #getSpecificTargetClasses()}, it will typically be called for all
29+
* access operations and given a chance to determine if it supports a concrete
30+
* access attempt.
31+
*
32+
* <p>Targeted accessors are considered to be ordered, and each will be called
33+
* in turn. The only rule that affects the call order is that any accessor which
34+
* specifies explicit support for a given target class via
35+
* {@link #getSpecificTargetClasses()} will be called first, before other generic
36+
* accessors that do not specify explicit support for the given target class.
37+
*
38+
* @author Sam Brannen
39+
* @since 6.2
40+
* @see PropertyAccessor
41+
* @see IndexAccessor
42+
*/
43+
public interface TargetedAccessor {
44+
45+
/**
46+
* Get the set of classes for which this accessor should be called.
47+
* <p>Returning {@code null} or an empty array indicates this is a generic
48+
* accessor that can be called in an attempt to access an element on any
49+
* type.
50+
* @return an array of classes that this accessor is suitable for
51+
* (or {@code null} or an empty array if a generic accessor)
52+
*/
53+
@Nullable
54+
Class<?>[] getSpecificTargetClasses();
55+
56+
}

spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,16 @@ public enum SpelMessage {
291291

292292
/** @since 6.0.13 */
293293
NEGATIVE_REPEATED_TEXT_COUNT(Kind.ERROR, 1081,
294-
"Repeat count ''{0}'' must not be negative");
294+
"Repeat count ''{0}'' must not be negative"),
295+
296+
/** @since 6.2 */
297+
EXCEPTION_DURING_INDEX_READ(Kind.ERROR, 1082,
298+
"A problem occurred while attempting to read index ''{0}'' in ''{1}''"),
299+
300+
/** @since 6.2 */
301+
EXCEPTION_DURING_INDEX_WRITE(Kind.ERROR, 1083,
302+
"A problem occurred while attempting to write index ''{0}'' in ''{1}''");
303+
295304

296305

297306
private final Kind kind;

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

+61-20
Original file line numberDiff line numberDiff line change
@@ -17,63 +17,104 @@
1717
package org.springframework.expression.spel.ast;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.List;
2122

2223
import org.springframework.expression.PropertyAccessor;
24+
import org.springframework.expression.TargetedAccessor;
2325
import org.springframework.lang.Nullable;
2426
import org.springframework.util.ObjectUtils;
2527

2628
/**
2729
* Utility methods for use in the AST classes.
2830
*
2931
* @author Andy Clement
32+
* @author Sam Brannen
3033
* @since 3.0.2
3134
*/
3235
public abstract class AstUtils {
3336

3437
/**
35-
* Determine the set of property accessors that should be used to try to
36-
* access a property on the specified target type.
38+
* Determine the set of accessors that should be used to try to access an
39+
* element on the specified target type.
3740
* <p>The accessors are considered to be in an ordered list; however, in the
3841
* returned list any accessors that are exact matches for the input target
39-
* type (as opposed to 'general' accessors that could work for any type) are
42+
* type (as opposed to 'generic' accessors that could work for any type) are
4043
* placed at the start of the list. In addition, if there are specific
4144
* accessors that exactly name the class in question and accessors that name
4245
* a specific class which is a supertype of the class in question, the latter
4346
* are put at the end of the specific accessors set and will be tried after
4447
* exactly matching accessors but before generic accessors.
45-
* @param targetType the type upon which property access is being attempted
46-
* @param propertyAccessors the list of property accessors to process
47-
* @return a list of accessors that should be tried in order to access the property
48+
* @param targetType the type upon which element access is being attempted
49+
* @param accessors the list of element accessors to process
50+
* @return a list of accessors that should be tried in order to access the
51+
* element on the specified target type, or an empty list if no suitable
52+
* accessor could be found
53+
* @since 6.2
4854
*/
49-
public static List<PropertyAccessor> getPropertyAccessorsToTry(
50-
@Nullable Class<?> targetType, List<PropertyAccessor> propertyAccessors) {
55+
public static <T extends TargetedAccessor> List<T> getAccessorsToTry(
56+
@Nullable Class<?> targetType, List<T> accessors) {
57+
58+
if (accessors.isEmpty()) {
59+
return Collections.emptyList();
60+
}
5161

52-
List<PropertyAccessor> specificAccessors = new ArrayList<>();
53-
List<PropertyAccessor> generalAccessors = new ArrayList<>();
54-
for (PropertyAccessor accessor : propertyAccessors) {
62+
List<T> exactMatches = new ArrayList<>();
63+
List<T> inexactMatches = new ArrayList<>();
64+
List<T> genericMatches = new ArrayList<>();
65+
for (T accessor : accessors) {
5566
Class<?>[] targets = accessor.getSpecificTargetClasses();
5667
if (ObjectUtils.isEmpty(targets)) {
5768
// generic accessor that says it can be used for any type
58-
generalAccessors.add(accessor);
69+
genericMatches.add(accessor);
5970
}
6071
else if (targetType != null) {
6172
for (Class<?> clazz : targets) {
6273
if (clazz == targetType) {
63-
// add exact matches to the specificAccessors list
64-
specificAccessors.add(accessor);
74+
exactMatches.add(accessor);
6575
}
6676
else if (clazz.isAssignableFrom(targetType)) {
67-
// add supertype matches to the front of the generalAccessors list
68-
generalAccessors.add(0, accessor);
77+
inexactMatches.add(accessor);
6978
}
7079
}
7180
}
7281
}
73-
List<PropertyAccessor> accessors = new ArrayList<>(specificAccessors.size() + generalAccessors.size());
74-
accessors.addAll(specificAccessors);
75-
accessors.addAll(generalAccessors);
76-
return accessors;
82+
83+
int size = exactMatches.size() + inexactMatches.size() + genericMatches.size();
84+
if (size == 0) {
85+
return Collections.emptyList();
86+
}
87+
else {
88+
List<T> result = new ArrayList<>(size);
89+
result.addAll(exactMatches);
90+
result.addAll(inexactMatches);
91+
result.addAll(genericMatches);
92+
return result;
93+
}
94+
}
95+
96+
/**
97+
* Determine the set of property accessors that should be used to try to
98+
* access a property on the specified target type.
99+
* <p>The accessors are considered to be in an ordered list; however, in the
100+
* returned list any accessors that are exact matches for the input target
101+
* type (as opposed to 'generic' accessors that could work for any type) are
102+
* placed at the start of the list. In addition, if there are specific
103+
* accessors that exactly name the class in question and accessors that name
104+
* a specific class which is a supertype of the class in question, the latter
105+
* are put at the end of the specific accessors set and will be tried after
106+
* exactly matching accessors but before generic accessors.
107+
* @param targetType the type upon which property access is being attempted
108+
* @param propertyAccessors the list of property accessors to process
109+
* @return a list of accessors that should be tried in order to access the
110+
* property on the specified target type, or an empty list if no suitable
111+
* accessor could be found
112+
* @see #getAccessorsToTry(Class, List)
113+
*/
114+
public static List<PropertyAccessor> getPropertyAccessorsToTry(
115+
@Nullable Class<?> targetType, List<PropertyAccessor> propertyAccessors) {
116+
117+
return getAccessorsToTry(targetType, propertyAccessors);
77118
}
78119

79120
}

0 commit comments

Comments
 (0)