Skip to content

Commit

Permalink
Initial review and polish of IndexAccessor support in SpEL
Browse files Browse the repository at this point in the history
  • Loading branch information
sbrannen committed Apr 10, 2024
1 parent 50a0000 commit d912770
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 262 deletions.
1 change: 0 additions & 1 deletion spring-expression/spring-expression.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.jetbrains.kotlin:kotlin-stdlib")
testImplementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("com.fasterxml.jackson.core:jackson-core")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.expression;

import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

Expand Down Expand Up @@ -57,9 +58,14 @@ public interface EvaluationContext {
List<PropertyAccessor> getPropertyAccessors();

/**
* Return a list of index accessors that will be asked in turn to read/write a property.
* 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
*/
List<IndexAccessor> getIndexAccessors();
default List<IndexAccessor> getIndexAccessors() {
return Collections.emptyList();
}

/**
* Return a list of resolvers that will be asked in turn to locate a constructor.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
Expand All @@ -20,78 +20,91 @@
import org.springframework.lang.Nullable;

/**
* A index accessor is able to read from (and possibly write to) an array's elements.
* An index accessor is able to read from (and possibly write to) an indexed
* structure of an object.
*
* <p>This interface places no restrictions, and so implementors are free to access elements
* directly as fields or through getters or in any other way they see as appropriate.
* <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>A resolver can optionally specify an array of target classes for which it should be
* called. However, if it returns {@code null} from {@link #getSpecificTargetClasses()},
* it will be called for all property references and given a chance to determine if it
* can read or write them.
* <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>Property resolvers are considered to be ordered, and each will be called in turn.
* The only rule that affects the call order is that any resolver naming the target
* class directly in {@link #getSpecificTargetClasses()} will be called first, before
* the general resolvers.
* <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
* @since 3.0
* @author Jackmiking Lee
* @author Sam Brannen
* @since 6.2
* @see PropertyAccessor
*/
public interface IndexAccessor {
public interface IndexAccessor extends TargetedAccessor {

/**
* Return an array of classes for which this resolver should be called.
* <p>Returning {@code null} indicates this is a general resolver that
* can be called in an attempt to resolve a property on any type.
* @return an array of classes that this resolver is suitable for
* (or {@code null} if a general resolver)
* 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();

/**
* Called to determine if a resolver instance is able to access a specified property
* on a specified target object.
* Called to 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 property is being accessed
* @param index the index of the array being accessed
* @return true if this resolver is able to read the property
* @throws AccessException if there is any problem determining whether the property can be read
* @param target the target object upon which the index is being accessed
* @param index the index being accessed
* @return 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, @Nullable Object target, Object index) throws AccessException;

/**
* Called to read a property from a specified target object.
* Should only succeed if {@link #canRead} also returns {@code true}.
* Called to read an index from a specified target object.
* <p>Should only succeed if {@link #canRead} also returns {@code true}.
* @param context the evaluation context in which the access is being attempted
* @param target the target object upon which the property is being accessed
* @param index the index of the array being accessed
* @return a TypedValue object wrapping the property value read and a type descriptor for it
* @throws AccessException if there is any problem accessing the property value
* @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 it
* @throws AccessException if there is any problem reading the index value
*/
ValueRef read(EvaluationContext context, @Nullable Object target,Object index) throws AccessException;
// TODO Change return type to TypedValue to avoid package cycle.
ValueRef read(EvaluationContext context, @Nullable Object target, Object index) throws AccessException;

/**
* Called to determine if a resolver instance is able to write to a specified
* property on a specified target object.
* Called to 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 property is being accessed
* @param index the index of the array being accessed
* @return true if this resolver is able to write to the property
* @param target the target object upon which the index is being accessed
* @param index the index being accessed
* @return true if this index accessor is able to write to the index
* @throws AccessException if there is any problem determining whether the
* property can be written to
* index can be written to
*/
boolean canWrite(EvaluationContext context, @Nullable Object target, Object index) throws AccessException;

/**
* Called to write to a property on a specified target object.
* Should only succeed if {@link #canWrite} also returns {@code true}.
* Called to write to an index on a specified target object.
* <p>Should only succeed if {@link #canWrite} also returns {@code true}.
* @param context the evaluation context in which the access is being attempted
* @param target the target object upon which the property is being accessed
* @param index the index of the array being accessed
* @param newValue the new value for the property
* @throws AccessException if there is any problem writing to the property value
* @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 value
*/
void write(EvaluationContext context, @Nullable Object target, Object index, @Nullable Object newValue)
throws AccessException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,24 @@
* <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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,63 +17,104 @@
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;

/**
* 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 property accessors that should be used to try to
* access a property on the specified target type.
* 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 '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
* 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 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
* @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 List<PropertyAccessor> getPropertyAccessorsToTry(
@Nullable Class<?> targetType, List<PropertyAccessor> propertyAccessors) {
public static <T extends TargetedAccessor> List<T> getAccessorsToTry(
@Nullable Class<?> targetType, List<T> accessors) {

if (accessors.isEmpty()) {
return Collections.emptyList();
}

List<PropertyAccessor> specificAccessors = new ArrayList<>();
List<PropertyAccessor> generalAccessors = new ArrayList<>();
for (PropertyAccessor accessor : propertyAccessors) {
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
generalAccessors.add(accessor);
genericMatches.add(accessor);
}
else if (targetType != null) {
for (Class<?> clazz : targets) {
if (clazz == targetType) {
// add exact matches to the specificAccessors list
specificAccessors.add(accessor);
exactMatches.add(accessor);
}
else if (clazz.isAssignableFrom(targetType)) {
// add supertype matches to the front of the generalAccessors list
generalAccessors.add(0, accessor);
inexactMatches.add(accessor);
}
}
}
}
List<PropertyAccessor> accessors = new ArrayList<>(specificAccessors.size() + generalAccessors.size());
accessors.addAll(specificAccessors);
accessors.addAll(generalAccessors);
return accessors;

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 '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 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 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) {

return getAccessorsToTry(targetType, propertyAccessors);
}

}
Loading

0 comments on commit d912770

Please sign in to comment.