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
This commit is contained in:
@@ -8,4 +8,5 @@ dependencies {
|
||||
testImplementation(testFixtures(project(":spring-core")))
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-databind")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.expression;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -56,6 +57,16 @@ public interface EvaluationContext {
|
||||
*/
|
||||
List<PropertyAccessor> getPropertyAccessors();
|
||||
|
||||
/**
|
||||
* Return a list of index accessors that will be asked in turn to access or
|
||||
* set an indexed value.
|
||||
* <p>The default implementation returns an empty list.
|
||||
* @since 6.2
|
||||
*/
|
||||
default List<IndexAccessor> getIndexAccessors() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of resolvers that will be asked in turn to locate a constructor.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2002-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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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.expression;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* An index accessor is able to read from (and possibly write to) an indexed
|
||||
* structure of an object.
|
||||
*
|
||||
* <p>This interface places no restrictions on what constitutes an indexed
|
||||
* structure. Implementors are therefore free to access indexed values any way
|
||||
* they deem appropriate.
|
||||
*
|
||||
* <p>An index accessor can optionally specify an array of target classes for
|
||||
* which it should be called. However, if it returns {@code null} or an empty
|
||||
* array from {@link #getSpecificTargetClasses()}, it will be called for all
|
||||
* indexing operations and given a chance to determine if it can read from or
|
||||
* write to the indexed structure.
|
||||
*
|
||||
* <p>Index accessors are considered to be ordered, and each will be called in
|
||||
* turn. The only rule that affects the call order is that any index accessor
|
||||
* which specifies explicit support for the target class via
|
||||
* {@link #getSpecificTargetClasses()} will be called first, before other
|
||||
* generic index accessors.
|
||||
*
|
||||
* @author Jackmiking Lee
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see PropertyAccessor
|
||||
*/
|
||||
public interface IndexAccessor extends TargetedAccessor {
|
||||
|
||||
/**
|
||||
* Get the set of classes for which this index accessor should be called.
|
||||
* <p>Returning {@code null} or an empty array indicates this is a generic
|
||||
* index accessor that can be called in an attempt to access an index on any
|
||||
* type.
|
||||
* @return an array of classes that this index accessor is suitable for
|
||||
* (or {@code null} or an empty array if a generic index accessor)
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
Class<?>[] getSpecificTargetClasses();
|
||||
|
||||
/**
|
||||
* Determine if this index accessor is able to read a specified index on a
|
||||
* specified target object.
|
||||
* @param context the evaluation context in which the access is being attempted
|
||||
* @param target the target object upon which the index is being accessed
|
||||
* @param index the index being accessed
|
||||
* @return {@code true} if this index accessor is able to read the index
|
||||
* @throws AccessException if there is any problem determining whether the
|
||||
* index can be read
|
||||
*/
|
||||
boolean canRead(EvaluationContext context, Object target, Object index) throws AccessException;
|
||||
|
||||
/**
|
||||
* Read an index from a specified target object.
|
||||
* <p>Should only be invoked if {@link #canRead} returns {@code true} for the
|
||||
* same arguments.
|
||||
* @param context the evaluation context in which the access is being attempted
|
||||
* @param target the target object upon which the index is being accessed
|
||||
* @param index the index being accessed
|
||||
* @return a TypedValue object wrapping the index value read and a type
|
||||
* descriptor for the value
|
||||
* @throws AccessException if there is any problem reading the index
|
||||
*/
|
||||
TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException;
|
||||
|
||||
/**
|
||||
* Determine if this index accessor is able to write to a specified index on
|
||||
* a specified target object.
|
||||
* @param context the evaluation context in which the access is being attempted
|
||||
* @param target the target object upon which the index is being accessed
|
||||
* @param index the index being accessed
|
||||
* @return {@code true} if this index accessor is able to write to the index
|
||||
* @throws AccessException if there is any problem determining whether the
|
||||
* index can be written to
|
||||
*/
|
||||
boolean canWrite(EvaluationContext context, Object target, Object index) throws AccessException;
|
||||
|
||||
/**
|
||||
* Write to an index on a specified target object.
|
||||
* <p>Should only be invoked if {@link #canWrite} returns {@code true} for the
|
||||
* same arguments.
|
||||
* @param context the evaluation context in which the access is being attempted
|
||||
* @param target the target object upon which the index is being accessed
|
||||
* @param index the index being accessed
|
||||
* @param newValue the new value for the index
|
||||
* @throws AccessException if there is any problem writing to the index
|
||||
*/
|
||||
void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue)
|
||||
throws AccessException;
|
||||
|
||||
}
|
||||
@@ -34,21 +34,24 @@ import org.springframework.lang.Nullable;
|
||||
* <p>Property accessors are considered to be ordered, and each will be called in
|
||||
* turn. The only rule that affects the call order is that any property accessor
|
||||
* which specifies explicit support for the target class via
|
||||
* {@link #getSpecificTargetClasses()} will be called first, before the general
|
||||
* {@link #getSpecificTargetClasses()} will be called first, before the generic
|
||||
* property accessors.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @since 3.0
|
||||
* @see IndexAccessor
|
||||
*/
|
||||
public interface PropertyAccessor {
|
||||
public interface PropertyAccessor extends TargetedAccessor {
|
||||
|
||||
/**
|
||||
* Return an array of classes for which this property accessor should be called.
|
||||
* <p>Returning {@code null} indicates this is a general property accessor that
|
||||
* can be called in an attempt to access a property on any type.
|
||||
* Get the set of classes for which this property accessor should be called.
|
||||
* <p>Returning {@code null} or an empty array indicates this is a generic
|
||||
* property accessor that can be called in an attempt to access a property on
|
||||
* any type.
|
||||
* @return an array of classes that this property accessor is suitable for
|
||||
* (or {@code null} if a general property accessor)
|
||||
* (or {@code null} if a generic property accessor)
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
Class<?>[] getSpecificTargetClasses();
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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.expression;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Strategy for types that access elements of specific target classes.
|
||||
*
|
||||
* <p>This interface places no restrictions on what constitutes an element.
|
||||
*
|
||||
* <p>A targeted accessor can specify a set of target classes for which it should
|
||||
* be called. However, if it returns {@code null} or an empty array from
|
||||
* {@link #getSpecificTargetClasses()}, it will typically be called for all
|
||||
* access operations and given a chance to determine if it supports a concrete
|
||||
* access attempt.
|
||||
*
|
||||
* <p>Targeted accessors are considered to be ordered, and each will be called
|
||||
* in turn. The only rule that affects the call order is that any accessor which
|
||||
* specifies explicit support for a given target class via
|
||||
* {@link #getSpecificTargetClasses()} will be called first, before other generic
|
||||
* accessors that do not specify explicit support for the given target class.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see PropertyAccessor
|
||||
* @see IndexAccessor
|
||||
*/
|
||||
public interface TargetedAccessor {
|
||||
|
||||
/**
|
||||
* Get the set of classes for which this accessor should be called.
|
||||
* <p>Returning {@code null} or an empty array indicates this is a generic
|
||||
* accessor that can be called in an attempt to access an element on any
|
||||
* type.
|
||||
* @return an array of classes that this accessor is suitable for
|
||||
* (or {@code null} or an empty array if a generic accessor)
|
||||
*/
|
||||
@Nullable
|
||||
Class<?>[] getSpecificTargetClasses();
|
||||
|
||||
}
|
||||
@@ -291,7 +291,16 @@ public enum SpelMessage {
|
||||
|
||||
/** @since 6.0.13 */
|
||||
NEGATIVE_REPEATED_TEXT_COUNT(Kind.ERROR, 1081,
|
||||
"Repeat count ''{0}'' must not be negative");
|
||||
"Repeat count ''{0}'' must not be negative"),
|
||||
|
||||
/** @since 6.2 */
|
||||
EXCEPTION_DURING_INDEX_READ(Kind.ERROR, 1082,
|
||||
"A problem occurred while attempting to read index ''{0}'' in ''{1}''"),
|
||||
|
||||
/** @since 6.2 */
|
||||
EXCEPTION_DURING_INDEX_WRITE(Kind.ERROR, 1083,
|
||||
"A problem occurred while attempting to write index ''{0}'' in ''{1}''");
|
||||
|
||||
|
||||
|
||||
private final Kind kind;
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
package org.springframework.expression.spel.ast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.expression.PropertyAccessor;
|
||||
import org.springframework.expression.TargetedAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@@ -27,16 +29,76 @@ import org.springframework.util.ObjectUtils;
|
||||
* Utility methods for use in the AST classes.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Sam Brannen
|
||||
* @since 3.0.2
|
||||
*/
|
||||
public abstract class AstUtils {
|
||||
|
||||
/**
|
||||
* Determine the set of accessors that should be used to try to access an
|
||||
* element on the specified target type.
|
||||
* <p>The accessors are considered to be in an ordered list; however, in the
|
||||
* returned list any accessors that are exact matches for the input target
|
||||
* type (as opposed to 'generic' accessors that could work for any type) are
|
||||
* placed at the start of the list. In addition, if there are specific
|
||||
* accessors that exactly name the class in question and accessors that name
|
||||
* a specific class which is a supertype of the class in question, the latter
|
||||
* are put at the end of the specific accessors set and will be tried after
|
||||
* exactly matching accessors but before generic accessors.
|
||||
* @param targetType the type upon which element access is being attempted
|
||||
* @param accessors the list of element accessors to process
|
||||
* @return a list of accessors that should be tried in order to access the
|
||||
* element on the specified target type, or an empty list if no suitable
|
||||
* accessor could be found
|
||||
* @since 6.2
|
||||
*/
|
||||
public static <T extends TargetedAccessor> List<T> getAccessorsToTry(
|
||||
@Nullable Class<?> targetType, List<T> accessors) {
|
||||
|
||||
if (accessors.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<T> exactMatches = new ArrayList<>();
|
||||
List<T> inexactMatches = new ArrayList<>();
|
||||
List<T> genericMatches = new ArrayList<>();
|
||||
for (T accessor : accessors) {
|
||||
Class<?>[] targets = accessor.getSpecificTargetClasses();
|
||||
if (ObjectUtils.isEmpty(targets)) {
|
||||
// generic accessor that says it can be used for any type
|
||||
genericMatches.add(accessor);
|
||||
}
|
||||
else if (targetType != null) {
|
||||
for (Class<?> clazz : targets) {
|
||||
if (clazz == targetType) {
|
||||
exactMatches.add(accessor);
|
||||
}
|
||||
else if (clazz.isAssignableFrom(targetType)) {
|
||||
inexactMatches.add(accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int size = exactMatches.size() + inexactMatches.size() + genericMatches.size();
|
||||
if (size == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
else {
|
||||
List<T> result = new ArrayList<>(size);
|
||||
result.addAll(exactMatches);
|
||||
result.addAll(inexactMatches);
|
||||
result.addAll(genericMatches);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the set of property accessors that should be used to try to
|
||||
* access a property on the specified target type.
|
||||
* <p>The accessors are considered to be in an ordered list; however, in the
|
||||
* returned list any accessors that are exact matches for the input target
|
||||
* type (as opposed to 'general' accessors that could work for any type) are
|
||||
* type (as opposed to 'generic' accessors that could work for any type) are
|
||||
* placed at the start of the list. In addition, if there are specific
|
||||
* accessors that exactly name the class in question and accessors that name
|
||||
* a specific class which is a supertype of the class in question, the latter
|
||||
@@ -44,36 +106,15 @@ public abstract class AstUtils {
|
||||
* exactly matching accessors but before generic accessors.
|
||||
* @param targetType the type upon which property access is being attempted
|
||||
* @param propertyAccessors the list of property accessors to process
|
||||
* @return a list of accessors that should be tried in order to access the property
|
||||
* @return a list of accessors that should be tried in order to access the
|
||||
* property on the specified target type, or an empty list if no suitable
|
||||
* accessor could be found
|
||||
* @see #getAccessorsToTry(Class, List)
|
||||
*/
|
||||
public static List<PropertyAccessor> getPropertyAccessorsToTry(
|
||||
@Nullable Class<?> targetType, List<PropertyAccessor> propertyAccessors) {
|
||||
|
||||
List<PropertyAccessor> specificAccessors = new ArrayList<>();
|
||||
List<PropertyAccessor> generalAccessors = new ArrayList<>();
|
||||
for (PropertyAccessor accessor : propertyAccessors) {
|
||||
Class<?>[] targets = accessor.getSpecificTargetClasses();
|
||||
if (ObjectUtils.isEmpty(targets)) {
|
||||
// generic accessor that says it can be used for any type
|
||||
generalAccessors.add(accessor);
|
||||
}
|
||||
else if (targetType != null) {
|
||||
for (Class<?> clazz : targets) {
|
||||
if (clazz == targetType) {
|
||||
// add exact matches to the specificAccessors list
|
||||
specificAccessors.add(accessor);
|
||||
}
|
||||
else if (clazz.isAssignableFrom(targetType)) {
|
||||
// add supertype matches to the front of the generalAccessors list
|
||||
generalAccessors.add(0, accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<PropertyAccessor> accessors = new ArrayList<>(specificAccessors.size() + generalAccessors.size());
|
||||
accessors.addAll(specificAccessors);
|
||||
accessors.addAll(generalAccessors);
|
||||
return accessors;
|
||||
return getAccessorsToTry(targetType, propertyAccessors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.expression.AccessException;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.IndexAccessor;
|
||||
import org.springframework.expression.PropertyAccessor;
|
||||
import org.springframework.expression.TypeConverter;
|
||||
import org.springframework.expression.TypedValue;
|
||||
@@ -73,7 +74,24 @@ import org.springframework.util.ReflectionUtils;
|
||||
*/
|
||||
public class Indexer extends SpelNodeImpl {
|
||||
|
||||
private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}
|
||||
private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT, CUSTOM}
|
||||
|
||||
private enum AccessMode {
|
||||
|
||||
READ(true, false),
|
||||
|
||||
WRITE(false, true),
|
||||
|
||||
READ_WRITE(true, true);
|
||||
|
||||
private final boolean supportsReads;
|
||||
private final boolean supportsWrites;
|
||||
|
||||
private AccessMode(boolean supportsReads, boolean supportsWrites) {
|
||||
this.supportsReads = supportsReads;
|
||||
this.supportsWrites = supportsWrites;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final boolean nullSafe;
|
||||
@@ -87,33 +105,81 @@ public class Indexer extends SpelNodeImpl {
|
||||
@Nullable
|
||||
private volatile String arrayTypeDescriptor;
|
||||
|
||||
// These fields are used when the indexer is being used as a property read accessor.
|
||||
// If the name and target type match these cached values then the cachedReadAccessor
|
||||
// is used to read the property. If they do not match, the correct accessor is
|
||||
// discovered and then cached for later use.
|
||||
// These fields are used when the Indexer is being used as PropertyAccessor
|
||||
// for reading. If the name and target type match these cached values, then
|
||||
// the cachedPropertyReadAccessor is used to read the property. If they do
|
||||
// not match, a suitable accessor is discovered and then cached for later use.
|
||||
|
||||
@Nullable
|
||||
private String cachedReadName;
|
||||
private String cachedPropertyReadName;
|
||||
|
||||
@Nullable
|
||||
private Class<?> cachedReadTargetType;
|
||||
private Class<?> cachedPropertyReadTargetType;
|
||||
|
||||
@Nullable
|
||||
private PropertyAccessor cachedReadAccessor;
|
||||
private PropertyAccessor cachedPropertyReadAccessor;
|
||||
|
||||
// These fields are used when the indexer is being used as a property write accessor.
|
||||
// If the name and target type match these cached values then the cachedWriteAccessor
|
||||
// is used to write the property. If they do not match, the correct accessor is
|
||||
// discovered and then cached for later use.
|
||||
// These fields are used when the Indexer is being used as a PropertyAccessor
|
||||
// for writing. If the name and target type match these cached values, then
|
||||
// the cachedPropertyWriteAccessor is used to write the property. If they do
|
||||
// not match, a suitable accessor is discovered and then cached for later use.
|
||||
|
||||
@Nullable
|
||||
private String cachedWriteName;
|
||||
private String cachedPropertyWriteName;
|
||||
|
||||
@Nullable
|
||||
private Class<?> cachedWriteTargetType;
|
||||
private Class<?> cachedPropertyWriteTargetType;
|
||||
|
||||
@Nullable
|
||||
private PropertyAccessor cachedWriteAccessor;
|
||||
private PropertyAccessor cachedPropertyWriteAccessor;
|
||||
|
||||
// These fields are used when the Indexer is being used as an IndexAccessor
|
||||
// for reading. If the index value and target type match these cached values,
|
||||
// then the cachedIndexReadAccessor is used to read the index. If they do not
|
||||
// match, a suitable accessor is discovered and then cached for later use.
|
||||
|
||||
/**
|
||||
* The index value: the value inside the square brackets, such as the
|
||||
* Integer 0 in [0], the String "name" in ['name'], etc.
|
||||
*/
|
||||
@Nullable
|
||||
private Object cachedIndexReadIndex;
|
||||
|
||||
/**
|
||||
* The target type on which the index is being read.
|
||||
*/
|
||||
@Nullable
|
||||
private Class<?> cachedIndexReadTargetType;
|
||||
|
||||
/**
|
||||
* Cached {@link IndexAccessor} for reading.
|
||||
*/
|
||||
@Nullable
|
||||
private IndexAccessor cachedIndexReadAccessor;
|
||||
|
||||
// These fields are used when the Indexer is being used as an IndexAccessor
|
||||
// for writing. If the name and target type match these cached values, then
|
||||
// the cachedIndexWriteAccessor is used to read the property. If they do not
|
||||
// match, a suitable accessor is discovered and then cached for later use.
|
||||
|
||||
/**
|
||||
* The index value: the value inside the square brackets, such as the
|
||||
* Integer 0 in [0], the String "name" in ['name'], etc.
|
||||
*/
|
||||
@Nullable
|
||||
private Object cachedIndexWriteIndex;
|
||||
|
||||
/**
|
||||
* The target type on which the index is being written.
|
||||
*/
|
||||
@Nullable
|
||||
private Class<?> cachedIndexWriteTargetType;
|
||||
|
||||
/**
|
||||
* Cached {@link IndexAccessor} for writing.
|
||||
*/
|
||||
@Nullable
|
||||
private IndexAccessor cachedIndexWriteAccessor;
|
||||
|
||||
|
||||
/**
|
||||
@@ -149,7 +215,7 @@ public class Indexer extends SpelNodeImpl {
|
||||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
return getValueRef(state).getValue();
|
||||
return getValueRef(state, AccessMode.READ).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,18 +223,21 @@ public class Indexer extends SpelNodeImpl {
|
||||
throws EvaluationException {
|
||||
|
||||
TypedValue typedValue = valueSupplier.get();
|
||||
getValueRef(state).setValue(typedValue.getValue());
|
||||
getValueRef(state, AccessMode.WRITE).setValue(typedValue.getValue());
|
||||
return typedValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException {
|
||||
return true;
|
||||
return getValueRef(expressionState, AccessMode.WRITE).isWritable();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
|
||||
return getValueRef(state, AccessMode.READ_WRITE);
|
||||
}
|
||||
|
||||
private ValueRef getValueRef(ExpressionState state, AccessMode accessMode) throws EvaluationException {
|
||||
TypedValue context = state.getActiveContextObject();
|
||||
Object target = context.getValue();
|
||||
|
||||
@@ -239,14 +308,47 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// Try and treat the index value as a property of the context object
|
||||
// Try to treat the index value as a property of the context object.
|
||||
TypeDescriptor valueType = indexValue.getTypeDescriptor();
|
||||
if (valueType != null && String.class == valueType.getType()) {
|
||||
this.indexedType = IndexedType.OBJECT;
|
||||
return new PropertyIndexingValueRef(
|
||||
return new PropertyAccessorValueRef(
|
||||
target, (String) index, state.getEvaluationContext(), targetDescriptor);
|
||||
}
|
||||
|
||||
EvaluationContext evalContext = state.getEvaluationContext();
|
||||
List<IndexAccessor> accessorsToTry = getIndexAccessorsToTry(target, evalContext.getIndexAccessors());
|
||||
if (accessMode.supportsReads) {
|
||||
try {
|
||||
for (IndexAccessor indexAccessor : accessorsToTry) {
|
||||
if (indexAccessor.canRead(evalContext, target, index)) {
|
||||
this.indexedType = IndexedType.CUSTOM;
|
||||
return new IndexAccessorValueRef(target, index, evalContext, targetDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new SpelEvaluationException(
|
||||
getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_INDEX_READ,
|
||||
index, target.getClass().getTypeName());
|
||||
}
|
||||
}
|
||||
if (accessMode.supportsWrites) {
|
||||
try {
|
||||
for (IndexAccessor indexAccessor : accessorsToTry) {
|
||||
if (indexAccessor.canWrite(evalContext, target, index)) {
|
||||
this.indexedType = IndexedType.CUSTOM;
|
||||
return new IndexAccessorValueRef(target, index, evalContext, targetDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new SpelEvaluationException(
|
||||
getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_INDEX_WRITE,
|
||||
index, target.getClass().getTypeName());
|
||||
}
|
||||
}
|
||||
|
||||
throw new SpelEvaluationException(
|
||||
getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor);
|
||||
}
|
||||
@@ -267,8 +369,8 @@ public class Indexer extends SpelNodeImpl {
|
||||
// If the string name is changing, the accessor is clearly going to change.
|
||||
// So compilation is only possible if the index expression is a StringLiteral.
|
||||
return (index instanceof StringLiteral &&
|
||||
this.cachedReadAccessor instanceof CompilablePropertyAccessor compilablePropertyAccessor &&
|
||||
compilablePropertyAccessor.isCompilable());
|
||||
this.cachedPropertyReadAccessor instanceof CompilablePropertyAccessor cpa &&
|
||||
cpa.isCompilable());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -347,7 +449,8 @@ public class Indexer extends SpelNodeImpl {
|
||||
throw new IllegalStateException(
|
||||
"Index expression must be a StringLiteral, but was: " + index.getClass().getName());
|
||||
}
|
||||
CompilablePropertyAccessor compilablePropertyAccessor = (CompilablePropertyAccessor) this.cachedReadAccessor;
|
||||
CompilablePropertyAccessor compilablePropertyAccessor =
|
||||
(CompilablePropertyAccessor) this.cachedPropertyReadAccessor;
|
||||
Assert.state(compilablePropertyAccessor != null, "No cached read accessor");
|
||||
String propertyName = (String) stringLiteral.getLiteralValue().getValue();
|
||||
Assert.state(propertyName != null, "No property name");
|
||||
@@ -386,6 +489,50 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePropertyReadState(@Nullable PropertyAccessor propertyAccessor, @Nullable String name,
|
||||
@Nullable Class<?> targetType) {
|
||||
this.cachedPropertyReadAccessor = propertyAccessor;
|
||||
this.cachedPropertyReadName = name;
|
||||
this.cachedPropertyReadTargetType = targetType;
|
||||
}
|
||||
|
||||
private void updatePropertyWriteState(@Nullable PropertyAccessor propertyAccessor, @Nullable String name,
|
||||
@Nullable Class<?> targetType) {
|
||||
this.cachedPropertyWriteAccessor = propertyAccessor;
|
||||
this.cachedPropertyWriteName = name;
|
||||
this.cachedPropertyWriteTargetType = targetType;
|
||||
}
|
||||
|
||||
private void updateIndexReadState(@Nullable IndexAccessor indexAccessor, @Nullable Object index,
|
||||
@Nullable Class<?> targetType) {
|
||||
this.cachedIndexReadAccessor = indexAccessor;
|
||||
this.cachedIndexReadIndex = index;
|
||||
this.cachedIndexReadTargetType = targetType;
|
||||
}
|
||||
|
||||
private void updateIndexWriteState(@Nullable IndexAccessor indexAccessor, @Nullable Object index,
|
||||
@Nullable Class<?> targetType) {
|
||||
this.cachedIndexWriteAccessor = indexAccessor;
|
||||
this.cachedIndexWriteIndex = index;
|
||||
this.cachedIndexWriteTargetType = targetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the set of index accessors that should be used to try to access
|
||||
* an index on the specified context object.
|
||||
* <p>Delegates to {@link AstUtils#getAccessorsToTry(Class, List)}.
|
||||
* @param targetObject the object upon which index access is being attempted
|
||||
* @param indexAccessors the list of index accessors to process
|
||||
* @return a list of accessors that should be tried in order to access the
|
||||
* index, or an empty list if no suitable accessor could be found
|
||||
*/
|
||||
private static List<IndexAccessor> getIndexAccessorsToTry(
|
||||
@Nullable Object targetObject, List<IndexAccessor> indexAccessors) {
|
||||
|
||||
Class<?> targetType = (targetObject != null ? targetObject.getClass() : null);
|
||||
return AstUtils.getAccessorsToTry(targetType, indexAccessors);
|
||||
}
|
||||
|
||||
|
||||
private class ArrayIndexingValueRef implements ValueRef {
|
||||
|
||||
@@ -604,7 +751,7 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
|
||||
|
||||
private class PropertyIndexingValueRef implements ValueRef {
|
||||
private class PropertyAccessorValueRef implements ValueRef {
|
||||
|
||||
private final Object targetObject;
|
||||
|
||||
@@ -614,7 +761,7 @@ public class Indexer extends SpelNodeImpl {
|
||||
|
||||
private final TypeDescriptor targetObjectTypeDescriptor;
|
||||
|
||||
public PropertyIndexingValueRef(Object targetObject, String value,
|
||||
public PropertyAccessorValueRef(Object targetObject, String value,
|
||||
EvaluationContext evaluationContext, TypeDescriptor targetObjectTypeDescriptor) {
|
||||
|
||||
this.targetObject = targetObject;
|
||||
@@ -624,29 +771,27 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway")
|
||||
public TypedValue getValue() {
|
||||
Class<?> targetObjectRuntimeClass = getObjectClass(this.targetObject);
|
||||
Class<?> targetType = getObjectClass(this.targetObject);
|
||||
try {
|
||||
if (Indexer.this.cachedReadName != null && Indexer.this.cachedReadName.equals(this.name) &&
|
||||
Indexer.this.cachedReadTargetType != null &&
|
||||
Indexer.this.cachedReadTargetType.equals(targetObjectRuntimeClass)) {
|
||||
String cachedPropertyReadName = Indexer.this.cachedPropertyReadName;
|
||||
Class<?> cachedPropertyReadTargetType = Indexer.this.cachedPropertyReadTargetType;
|
||||
if (cachedPropertyReadName != null && cachedPropertyReadName.equals(this.name) &&
|
||||
cachedPropertyReadTargetType != null && cachedPropertyReadTargetType.equals(targetType)) {
|
||||
// It is OK to use the cached accessor
|
||||
PropertyAccessor accessor = Indexer.this.cachedReadAccessor;
|
||||
Assert.state(accessor != null, "No cached read accessor");
|
||||
PropertyAccessor accessor = Indexer.this.cachedPropertyReadAccessor;
|
||||
Assert.state(accessor != null, "No cached PropertyAccessor for reading");
|
||||
return accessor.read(this.evaluationContext, this.targetObject, this.name);
|
||||
}
|
||||
List<PropertyAccessor> accessorsToTry = AstUtils.getPropertyAccessorsToTry(
|
||||
targetObjectRuntimeClass, this.evaluationContext.getPropertyAccessors());
|
||||
targetType, this.evaluationContext.getPropertyAccessors());
|
||||
for (PropertyAccessor accessor : accessorsToTry) {
|
||||
if (accessor.canRead(this.evaluationContext, this.targetObject, this.name)) {
|
||||
if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) {
|
||||
accessor = reflectivePropertyAccessor.createOptimalAccessor(
|
||||
this.evaluationContext, this.targetObject, this.name);
|
||||
}
|
||||
Indexer.this.cachedReadAccessor = accessor;
|
||||
Indexer.this.cachedReadName = this.name;
|
||||
Indexer.this.cachedReadTargetType = targetObjectRuntimeClass;
|
||||
updatePropertyReadState(accessor, this.name, targetType);
|
||||
if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
|
||||
setExitTypeDescriptor(CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()));
|
||||
}
|
||||
@@ -663,26 +808,24 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway")
|
||||
public void setValue(@Nullable Object newValue) {
|
||||
Class<?> contextObjectClass = getObjectClass(this.targetObject);
|
||||
Class<?> targetType = getObjectClass(this.targetObject);
|
||||
try {
|
||||
if (Indexer.this.cachedWriteName != null && Indexer.this.cachedWriteName.equals(this.name) &&
|
||||
Indexer.this.cachedWriteTargetType != null &&
|
||||
Indexer.this.cachedWriteTargetType.equals(contextObjectClass)) {
|
||||
String cachedPropertyWriteName = Indexer.this.cachedPropertyWriteName;
|
||||
Class<?> cachedPropertyWriteTargetType = Indexer.this.cachedPropertyWriteTargetType;
|
||||
if (cachedPropertyWriteName != null && cachedPropertyWriteName.equals(this.name) &&
|
||||
cachedPropertyWriteTargetType != null && cachedPropertyWriteTargetType.equals(targetType)) {
|
||||
// It is OK to use the cached accessor
|
||||
PropertyAccessor accessor = Indexer.this.cachedWriteAccessor;
|
||||
Assert.state(accessor != null, "No cached write accessor");
|
||||
PropertyAccessor accessor = Indexer.this.cachedPropertyWriteAccessor;
|
||||
Assert.state(accessor != null, "No cached PropertyAccessor for writing");
|
||||
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
|
||||
return;
|
||||
}
|
||||
List<PropertyAccessor> accessorsToTry = AstUtils.getPropertyAccessorsToTry(
|
||||
contextObjectClass, this.evaluationContext.getPropertyAccessors());
|
||||
targetType, this.evaluationContext.getPropertyAccessors());
|
||||
for (PropertyAccessor accessor : accessorsToTry) {
|
||||
if (accessor.canWrite(this.evaluationContext, this.targetObject, this.name)) {
|
||||
Indexer.this.cachedWriteName = this.name;
|
||||
Indexer.this.cachedWriteTargetType = contextObjectClass;
|
||||
Indexer.this.cachedWriteAccessor = accessor;
|
||||
updatePropertyWriteState(accessor, this.name, targetType);
|
||||
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
|
||||
return;
|
||||
}
|
||||
@@ -842,4 +985,132 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class IndexAccessorValueRef implements ValueRef {
|
||||
|
||||
private final Object target;
|
||||
|
||||
private final Object index;
|
||||
|
||||
private final EvaluationContext evaluationContext;
|
||||
|
||||
private final TypeDescriptor typeDescriptor;
|
||||
|
||||
|
||||
IndexAccessorValueRef(Object target, Object index, EvaluationContext evaluationContext,
|
||||
TypeDescriptor typeDescriptor) {
|
||||
|
||||
this.target = target;
|
||||
this.index = index;
|
||||
this.evaluationContext = evaluationContext;
|
||||
this.typeDescriptor = typeDescriptor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TypedValue getValue() {
|
||||
Class<?> targetType = getObjectClass(this.target);
|
||||
Exception exception = null;
|
||||
try {
|
||||
Object cachedIndexReadIndex = Indexer.this.cachedIndexReadIndex;
|
||||
Class<?> cachedIndexReadTargetType = Indexer.this.cachedIndexReadTargetType;
|
||||
// Is it OK to use the cached IndexAccessor?
|
||||
if (cachedIndexReadIndex != null && cachedIndexReadIndex.equals(this.index) &&
|
||||
cachedIndexReadTargetType != null && cachedIndexReadTargetType.equals(targetType)) {
|
||||
IndexAccessor accessor = Indexer.this.cachedIndexReadAccessor;
|
||||
Assert.state(accessor != null, "No cached IndexAccessor for reading");
|
||||
if (this.evaluationContext.getIndexAccessors().contains(accessor)) {
|
||||
try {
|
||||
return accessor.read(this.evaluationContext, this.target, this.index);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// This is OK: it may have gone stale due to a class change.
|
||||
// So, we track the exception and try to find a new accessor
|
||||
// before giving up...
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
// If the above code block did not use a cached accessor,
|
||||
// we need to reset our cached state.
|
||||
updateIndexReadState(null, null, null);
|
||||
}
|
||||
List<IndexAccessor> accessorsToTry =
|
||||
getIndexAccessorsToTry(this.target, this.evaluationContext.getIndexAccessors());
|
||||
for (IndexAccessor indexAccessor : accessorsToTry) {
|
||||
if (indexAccessor.canRead(this.evaluationContext, this.target, this.index)) {
|
||||
updateIndexReadState(indexAccessor, this.index, targetType);
|
||||
return indexAccessor.read(this.evaluationContext, this.target, this.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new SpelEvaluationException(
|
||||
getStartPosition(), exception, SpelMessage.EXCEPTION_DURING_INDEX_READ, this.index,
|
||||
this.typeDescriptor.toString());
|
||||
}
|
||||
throw new SpelEvaluationException(getStartPosition(),
|
||||
SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.typeDescriptor.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(@Nullable Object newValue) {
|
||||
Class<?> targetType = getObjectClass(this.target);
|
||||
Exception exception = null;
|
||||
try {
|
||||
Object cachedIndexWriteIndex = Indexer.this.cachedIndexWriteIndex;
|
||||
Class<?> cachedIndexWriteTargetType = Indexer.this.cachedIndexWriteTargetType;
|
||||
// Is it OK to use the cached IndexAccessor?
|
||||
if (cachedIndexWriteIndex != null && cachedIndexWriteIndex.equals(this.index) &&
|
||||
cachedIndexWriteTargetType != null && cachedIndexWriteTargetType.equals(targetType)) {
|
||||
IndexAccessor accessor = Indexer.this.cachedIndexWriteAccessor;
|
||||
Assert.state(accessor != null, "No cached IndexAccessor for writing");
|
||||
if (this.evaluationContext.getIndexAccessors().contains(accessor)) {
|
||||
try {
|
||||
accessor.write(this.evaluationContext, this.target, this.index, newValue);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// This is OK: it may have gone stale due to a class change.
|
||||
// So, we track the exception and try to find a new accessor
|
||||
// before giving up...
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
// If the above code block did not use a cached accessor,
|
||||
// we need to reset our cached state.
|
||||
updateIndexWriteState(null, null, null);
|
||||
}
|
||||
List<IndexAccessor> accessorsToTry =
|
||||
getIndexAccessorsToTry(this.target, this.evaluationContext.getIndexAccessors());
|
||||
for (IndexAccessor indexAccessor : accessorsToTry) {
|
||||
if (indexAccessor.canWrite(this.evaluationContext, this.target, this.index)) {
|
||||
updateIndexWriteState(indexAccessor, this.index, targetType);
|
||||
indexAccessor.write(this.evaluationContext, this.target, this.index, newValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new SpelEvaluationException(
|
||||
getStartPosition(), exception, SpelMessage.EXCEPTION_DURING_INDEX_WRITE, this.index,
|
||||
this.typeDescriptor.toString());
|
||||
}
|
||||
throw new SpelEvaluationException(getStartPosition(),
|
||||
SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.typeDescriptor.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.expression.ConstructorResolver;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.IndexAccessor;
|
||||
import org.springframework.expression.MethodFilter;
|
||||
import org.springframework.expression.MethodResolver;
|
||||
import org.springframework.expression.OperatorOverloader;
|
||||
@@ -83,6 +84,9 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
@Nullable
|
||||
private volatile List<PropertyAccessor> propertyAccessors;
|
||||
|
||||
@Nullable
|
||||
private volatile List<IndexAccessor> indexAccessors;
|
||||
|
||||
@Nullable
|
||||
private volatile List<ConstructorResolver> constructorResolvers;
|
||||
|
||||
@@ -155,6 +159,56 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
return initPropertyAccessors().remove(accessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of index accessors to use in this evaluation context.
|
||||
* <p>Replaces any previously configured index accessors.
|
||||
* @since 6.2
|
||||
* @see #getIndexAccessors()
|
||||
* @see #addIndexAccessor(IndexAccessor)
|
||||
* @see #removeIndexAccessor(IndexAccessor)
|
||||
*/
|
||||
public void setIndexAccessors(List<IndexAccessor> indexAccessors) {
|
||||
this.indexAccessors = indexAccessors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of index accessors configured in this evaluation context.
|
||||
* @since 6.2
|
||||
* @see #setIndexAccessors(List)
|
||||
* @see #addIndexAccessor(IndexAccessor)
|
||||
* @see #removeIndexAccessor(IndexAccessor)
|
||||
*/
|
||||
@Override
|
||||
public List<IndexAccessor> getIndexAccessors() {
|
||||
return initIndexAccessors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the supplied index accessor to this evaluation context.
|
||||
* @param indexAccessor the index accessor to add
|
||||
* @since 6.2
|
||||
* @see #getIndexAccessors()
|
||||
* @see #setIndexAccessors(List)
|
||||
* @see #removeIndexAccessor(IndexAccessor)
|
||||
*/
|
||||
public void addIndexAccessor(IndexAccessor indexAccessor) {
|
||||
initIndexAccessors().add(indexAccessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the supplied index accessor from this evaluation context.
|
||||
* @param indexAccessor the index accessor to remove
|
||||
* @return {@code true} if the index accessor was removed, {@code false} if
|
||||
* the index accessor was not configured in this evaluation context
|
||||
* @since 6.2
|
||||
* @see #getIndexAccessors()
|
||||
* @see #setIndexAccessors(List)
|
||||
* @see #addIndexAccessor(IndexAccessor)
|
||||
*/
|
||||
public boolean removeIndexAccessor(IndexAccessor indexAccessor) {
|
||||
return initIndexAccessors().remove(indexAccessor);
|
||||
}
|
||||
|
||||
public void setConstructorResolvers(List<ConstructorResolver> constructorResolvers) {
|
||||
this.constructorResolvers = constructorResolvers;
|
||||
}
|
||||
@@ -384,6 +438,7 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
evaluationContext.setConstructorResolvers(new ArrayList<>(getConstructorResolvers()));
|
||||
evaluationContext.setMethodResolvers(new ArrayList<>(getMethodResolvers()));
|
||||
evaluationContext.setPropertyAccessors(new ArrayList<>(getPropertyAccessors()));
|
||||
evaluationContext.setIndexAccessors(new ArrayList<>(getIndexAccessors()));
|
||||
evaluationContext.setTypeLocator(getTypeLocator());
|
||||
evaluationContext.setTypeConverter(getTypeConverter());
|
||||
|
||||
@@ -404,6 +459,15 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
return accessors;
|
||||
}
|
||||
|
||||
private List<IndexAccessor> initIndexAccessors() {
|
||||
List<IndexAccessor> accessors = this.indexAccessors;
|
||||
if (accessors == null) {
|
||||
accessors = new ArrayList<>(5);
|
||||
this.indexAccessors = accessors;
|
||||
}
|
||||
return accessors;
|
||||
}
|
||||
|
||||
private List<ConstructorResolver> initConstructorResolvers() {
|
||||
List<ConstructorResolver> resolvers = this.constructorResolvers;
|
||||
if (resolvers == null) {
|
||||
|
||||
@@ -28,19 +28,36 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.NullNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.IndexAccessor;
|
||||
import org.springframework.expression.PropertyAccessor;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.testresources.Person;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.doThrow;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.springframework.expression.spel.SpelMessage.EXCEPTION_DURING_INDEX_READ;
|
||||
import static org.springframework.expression.spel.SpelMessage.EXCEPTION_DURING_INDEX_WRITE;
|
||||
import static org.springframework.expression.spel.SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE;
|
||||
import static org.springframework.expression.spel.SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE;
|
||||
|
||||
@@ -449,6 +466,226 @@ class IndexingTests {
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class IndexAccessorTests { // gh-26478
|
||||
|
||||
@Test
|
||||
void addingAndRemovingIndexAccessors() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
IndexAccessor accessor1 = new JacksonArrayNodeIndexAccessor(objectMapper);
|
||||
IndexAccessor accessor2 = new JacksonArrayNodeIndexAccessor(objectMapper);
|
||||
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
List<IndexAccessor> indexAccessors = context.getIndexAccessors();
|
||||
assertThat(indexAccessors).isEmpty();
|
||||
|
||||
context.addIndexAccessor(accessor1);
|
||||
assertThat(indexAccessors).containsExactly(accessor1);
|
||||
|
||||
context.addIndexAccessor(accessor2);
|
||||
assertThat(indexAccessors).containsExactly(accessor1, accessor2);
|
||||
|
||||
List<IndexAccessor> copy = new ArrayList<>(indexAccessors);
|
||||
assertThat(context.removeIndexAccessor(accessor1)).isTrue();
|
||||
assertThat(context.removeIndexAccessor(accessor1)).isFalse();
|
||||
assertThat(indexAccessors).containsExactly(accessor2);
|
||||
|
||||
context.setIndexAccessors(copy);
|
||||
assertThat(context.getIndexAccessors()).containsExactly(accessor1, accessor2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSuitableIndexAccessorResultsInException() {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
assertThat(context.getIndexAccessors()).isEmpty();
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expr = parser.parseExpression("[0]");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr.getValue(context, this))
|
||||
.withMessageEndingWith("Indexing into type '%s' is not supported", getClass().getName())
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(INDEXING_NOT_SUPPORTED_FOR_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canReadThrowsException() throws Exception {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
RuntimeException exception = new RuntimeException("Boom!");
|
||||
|
||||
IndexAccessor mock = mock();
|
||||
given(mock.getSpecificTargetClasses()).willReturn(null);
|
||||
given(mock.canRead(any(), eq(this), any())).willThrow(exception);
|
||||
context.addIndexAccessor(mock);
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expr = parser.parseExpression("[0]");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr.getValue(context, this))
|
||||
.withMessageEndingWith("A problem occurred while attempting to read index '%d' in '%s'",
|
||||
0, getClass().getName())
|
||||
.withCause(exception)
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
|
||||
|
||||
verify(mock, times(1)).getSpecificTargetClasses();
|
||||
verify(mock, times(1)).canRead(any(), any(), any());
|
||||
verifyNoMoreInteractions(mock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readThrowsException() throws Exception {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
RuntimeException exception = new RuntimeException("Boom!");
|
||||
|
||||
IndexAccessor mock = mock();
|
||||
given(mock.getSpecificTargetClasses()).willReturn(null);
|
||||
given(mock.canRead(any(), eq(this), any())).willReturn(true);
|
||||
given(mock.read(any(), eq(this), any())).willThrow(exception);
|
||||
context.addIndexAccessor(mock);
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expr = parser.parseExpression("[0]");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr.getValue(context, this))
|
||||
.withMessageEndingWith("A problem occurred while attempting to read index '%d' in '%s'",
|
||||
0, getClass().getName())
|
||||
.withCause(exception)
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
|
||||
|
||||
verify(mock, times(2)).getSpecificTargetClasses();
|
||||
verify(mock, times(2)).canRead(any(), any(), any());
|
||||
verify(mock, times(1)).read(any(), any(), any());
|
||||
verifyNoMoreInteractions(mock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canWriteThrowsException() throws Exception {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
RuntimeException exception = new RuntimeException("Boom!");
|
||||
|
||||
IndexAccessor mock = mock();
|
||||
given(mock.getSpecificTargetClasses()).willReturn(null);
|
||||
given(mock.canWrite(eq(context), eq(this), eq(0))).willThrow(exception);
|
||||
context.addIndexAccessor(mock);
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expr = parser.parseExpression("[0]");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr.setValue(context, this, 999))
|
||||
.withMessageEndingWith("A problem occurred while attempting to write index '%d' in '%s'",
|
||||
0, getClass().getName())
|
||||
.withCause(exception)
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_WRITE);
|
||||
|
||||
verify(mock, times(1)).getSpecificTargetClasses();
|
||||
verify(mock, times(1)).canWrite(any(), any(), any());
|
||||
verifyNoMoreInteractions(mock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeThrowsException() throws Exception {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
RuntimeException exception = new RuntimeException("Boom!");
|
||||
|
||||
IndexAccessor mock = mock();
|
||||
given(mock.getSpecificTargetClasses()).willReturn(null);
|
||||
given(mock.canWrite(eq(context), eq(this), eq(0))).willReturn(true);
|
||||
doThrow(exception).when(mock).write(any(), any(), any(), any());
|
||||
context.addIndexAccessor(mock);
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expr = parser.parseExpression("[0]");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr.setValue(context, this, 999))
|
||||
.withMessageEndingWith("A problem occurred while attempting to write index '%d' in '%s'",
|
||||
0, getClass().getName())
|
||||
.withCause(exception)
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_WRITE);
|
||||
|
||||
verify(mock, times(2)).getSpecificTargetClasses();
|
||||
verify(mock, times(2)).canWrite(any(), any(), any());
|
||||
verify(mock, times(1)).write(any(), any(), any(), any());
|
||||
verifyNoMoreInteractions(mock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAndWriteIndex() {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
context.addIndexAccessor(new JacksonArrayNodeIndexAccessor(objectMapper));
|
||||
|
||||
TextNode node0 = new TextNode("node0");
|
||||
TextNode node1 = new TextNode("node1");
|
||||
ArrayNode arrayNode = objectMapper.createArrayNode();
|
||||
arrayNode.addAll(List.of(node0, node1));
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expr = parser.parseExpression("[0]");
|
||||
assertThat(expr.getValue(context, arrayNode)).isSameAs(node0);
|
||||
|
||||
TextNode nodeX = new TextNode("nodeX");
|
||||
expr.setValue(context, arrayNode, nodeX);
|
||||
// We use isEqualTo() instead of isSameAs(), since ObjectMapper.convertValue()
|
||||
// converts the supplied TextNode to an equivalent JsonNode.
|
||||
assertThat(expr.getValue(context, arrayNode)).isEqualTo(nodeX);
|
||||
|
||||
NullNode nullNode = NullNode.getInstance();
|
||||
expr.setValue(context, arrayNode, nullNode);
|
||||
assertThat(expr.getValue(context, arrayNode)).isSameAs(nullNode);
|
||||
|
||||
expr = parser.parseExpression("[1]");
|
||||
assertThat(expr.getValue(context, arrayNode)).isSameAs(node1);
|
||||
|
||||
expr = parser.parseExpression("[-1]");
|
||||
// Jackson's ArrayNode returns null for a non-existent index instead
|
||||
// of throwing an ArrayIndexOutOfBoundsException or similar.
|
||||
assertThat(expr.getValue(context, arrayNode)).isNull();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link IndexAccessor} that knows how to read and write indexes in a
|
||||
* Jackson {@link ArrayNode}.
|
||||
*/
|
||||
private static class JacksonArrayNodeIndexAccessor implements IndexAccessor {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
JacksonArrayNodeIndexAccessor(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return new Class[] { ArrayNode.class };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, Object target, Object index) {
|
||||
return (target instanceof ArrayNode && index instanceof Integer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypedValue read(EvaluationContext context, Object target, Object index) {
|
||||
ArrayNode arrayNode = (ArrayNode) target;
|
||||
Integer intIndex = (Integer) index;
|
||||
return new TypedValue(arrayNode.get(intIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, Object target, Object index) {
|
||||
return canRead(context, target, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
|
||||
ArrayNode arrayNode = (ArrayNode) target;
|
||||
Integer intIndex = (Integer) index;
|
||||
arrayNode.set(intIndex, this.objectMapper.convertValue(newValue, JsonNode.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
||||
Reference in New Issue
Block a user