diff --git a/spring-expression/spring-expression.gradle b/spring-expression/spring-expression.gradle index 4b31d5b6f5b8..5bc6aadf2ee2 100644 --- a/spring-expression/spring-expression.gradle +++ b/spring-expression/spring-expression.gradle @@ -8,4 +8,6 @@ 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") + testImplementation("com.fasterxml.jackson.core:jackson-core") } diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index 9dd23361cd0a..217b84fb1b24 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -56,6 +56,11 @@ public interface EvaluationContext { */ List getPropertyAccessors(); + /** + * Return a list of index accessors that will be asked in turn to read/write a property. + */ + List getIndexAccessors(); + /** * Return a list of resolvers that will be asked in turn to locate a constructor. */ diff --git a/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java b/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java new file mode 100644 index 000000000000..a5a76a9c769b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2019 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.expression.spel.ast.ValueRef; +import org.springframework.lang.Nullable; + +/** + * A index accessor is able to read from (and possibly write to) an array's elements. + * + *

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. + * + *

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. + * + *

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. + * + * @author jackmiking lee + * @since 3.0 + */ +public interface IndexAccessor { + /** + * Return an array of classes for which this resolver should be called. + *

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) + */ + @Nullable + Class[] getSpecificTargetClasses(); + + /** + * Called to determine if a resolver instance is able to access a specified property + * 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 + */ + 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}. + * @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 + */ + 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. + * @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 + * @throws AccessException if there is any problem determining whether the + * property 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}. + * @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 + */ + void write(EvaluationContext context, @Nullable Object target, Object index, @Nullable Object newValue) + throws AccessException; +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 0e888782af5a..5a1c6f527dc1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -17,9 +17,11 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import org.springframework.asm.Label; @@ -28,6 +30,7 @@ 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; @@ -246,11 +249,73 @@ else if (target instanceof Collection collection) { return new PropertyIndexingValueRef( target, (String) index, state.getEvaluationContext(), targetDescriptor); } - + Optional optional = tryIndexAccessor(state, index); + if (optional.isPresent()) { + return optional.get(); + } throw new SpelEvaluationException( getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor); } + private Optional tryIndexAccessor(ExpressionState state, Object index) { + EvaluationContext context = state.getEvaluationContext(); + Object target = state.getActiveContextObject().getValue(); + if (context != null) { + List list = context.getIndexAccessors(); + if (list != null) { + List availableAccessors = getIndexAccessorsToTry(state.getActiveContextObject(), list); + try { + for (IndexAccessor indexAccessor : availableAccessors) { + if (indexAccessor.canRead(context, target, index)) { + ValueRef valueRef = indexAccessor.read(context, target, index); + return Optional.of(valueRef); + } + } + } + catch (Exception ex) { + } + } + } + return Optional.empty(); + } + + private List getIndexAccessorsToTry( + @Nullable Object contextObject, List propertyAccessors) { + + Class targetType; + if (contextObject instanceof TypedValue) { + targetType = ((TypedValue) contextObject).getTypeDescriptor().getObjectType(); + } + else { + targetType = (contextObject != null ? contextObject.getClass() : null); + } + + List specificAccessors = new ArrayList<>(); + List generalAccessors = new ArrayList<>(); + for (IndexAccessor resolver : propertyAccessors) { + Class[] targets = resolver.getSpecificTargetClasses(); + if (targets == null) { + // generic resolver that says it can be used for any type + generalAccessors.add(resolver); + } + else if (targetType != null) { + for (Class clazz : targets) { + if (clazz == targetType) { + specificAccessors.add(resolver); + break; + } + else if (clazz.isAssignableFrom(targetType)) { + generalAccessors.add(resolver); + } + } + } + } + List resolvers = new ArrayList<>(specificAccessors); + generalAccessors.removeAll(specificAccessors); + resolvers.addAll(generalAccessors); + return resolvers; + } + @Override public boolean isCompilable() { if (this.indexedType == IndexedType.ARRAY) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index f8cd8a1cce5a..24ab07b9ca71 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -28,6 +28,7 @@ import org.springframework.expression.BeanResolver; import org.springframework.expression.ConstructorResolver; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.IndexAccessor; import org.springframework.expression.MethodResolver; import org.springframework.expression.OperatorOverloader; import org.springframework.expression.PropertyAccessor; @@ -146,6 +147,11 @@ public List getPropertyAccessors() { return this.propertyAccessors; } + @Override + public List getIndexAccessors() { + return null; + } + /** * Return an empty list, always, since this context does not support the * use of type references. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 864395a8871b..ebe22a449a86 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -27,6 +27,7 @@ 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 propertyAccessors; + @Nullable + private volatile List indexAccessors; + @Nullable private volatile List constructorResolvers; @@ -142,6 +146,10 @@ public void setPropertyAccessors(List propertyAccessors) { this.propertyAccessors = propertyAccessors; } + public void setIndexAccessors(ListindexAccessors){ + this.indexAccessors=indexAccessors; + } + @Override public List getPropertyAccessors() { return initPropertyAccessors(); @@ -155,6 +163,14 @@ public boolean removePropertyAccessor(PropertyAccessor accessor) { return initPropertyAccessors().remove(accessor); } + public void addIndexAccessor(IndexAccessor accessor){ + initIndexAccessors().add(accessor); + } + + public boolean removeIndexAccessor(IndexAccessor indexAccessor){ + return initIndexAccessors().remove(indexAccessor); + } + public void setConstructorResolvers(List constructorResolvers) { this.constructorResolvers = constructorResolvers; } @@ -404,6 +420,15 @@ private List initPropertyAccessors() { return accessors; } + private ListinitIndexAccessors(){ + List accessors = this.indexAccessors; + if(accessors == null){ + accessors = new ArrayList<>(5); + this.indexAccessors = accessors; + } + return accessors; + } + private List initConstructorResolvers() { List resolvers = this.constructorResolvers; if (resolvers == null) { @@ -429,4 +454,9 @@ private static void addBeforeDefault(List list, T element) { list.add(list.size() - 1, element); } + @Override + public List getIndexAccessors() { + return initIndexAccessors(); + } + } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index 287644f84538..0557ab64cfc7 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -21,6 +21,10 @@ import java.util.List; import java.util.Map; +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.TextNode; import org.junit.jupiter.api.Test; import org.springframework.core.convert.TypeDescriptor; @@ -28,8 +32,10 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; 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.ast.ValueRef; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; @@ -144,6 +150,25 @@ void addingAndRemovingAccessors() { assertThat(ctx.getPropertyAccessors()).hasSize(2); } + @Test + public void testAddingRemovingIndexAccessors() { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + List indexAccessors = ctx.getIndexAccessors(); + assertThat(indexAccessors.size()).isEqualTo(0); + JsonIndexAccessor jsonIndexAccessor=new JsonIndexAccessor(); + ctx.addIndexAccessor(jsonIndexAccessor); + assertThat(indexAccessors.size()).isEqualTo(1); + JsonIndexAccessor jsonIndexAccessor1=new JsonIndexAccessor(); + ctx.addIndexAccessor(jsonIndexAccessor1); + assertThat(indexAccessors.size()).isEqualTo(2); + List copy=new ArrayList(indexAccessors); + assertThat(ctx.removeIndexAccessor(jsonIndexAccessor)).isTrue(); + assertThat(ctx.removeIndexAccessor(jsonIndexAccessor)).isFalse(); + assertThat(indexAccessors.size()).isEqualTo(1); + ctx.setIndexAccessors(copy); + assertThat(ctx.getIndexAccessors().size()).isEqualTo(2); + } + @Test void accessingPropertyOfClass() { Expression expression = parser.parseExpression("name"); @@ -223,6 +248,24 @@ void propertyReadWrite() { assertThat(target.getName()).isEqualTo("p4"); assertThat(expr.getValue(context, target)).isEqualTo("p4"); } + @Test + public void indexReadWrite(){ + StandardEvaluationContext context=new StandardEvaluationContext(); + JsonIndexAccessor indexAccessor=new JsonIndexAccessor(); + context.addIndexAccessor(indexAccessor); + ArrayNode arrayNode=indexAccessor.objectMapper.createArrayNode(); + arrayNode.add(new TextNode("node0")); + arrayNode.add(new TextNode("node1")); + Expression expr=parser.parseExpression("[0]"); + assertThat(new TextNode("node0").equals(expr.getValue(context,arrayNode))).isTrue(); + expr.setValue(context,arrayNode,new TextNode("nodeUpdate")); + assertThat(new TextNode("nodeUpdate").equals(expr.getValue(context,arrayNode))).isTrue(); + Expression expr1=parser.parseExpression("[1]"); + assertThat(new TextNode("node1").equals(expr1.getValue(context,arrayNode))).isTrue(); + + + } + @Test void propertyReadWriteWithRootObject() { @@ -363,4 +406,67 @@ public void write(EvaluationContext context, Object target, String name, Object } } + private static class JsonIndexAccessor implements IndexAccessor { + ObjectMapper objectMapper=new ObjectMapper(); + public class ArrayValueRef implements ValueRef { + ArrayNode arrayNode; + Integer index; + + @Override + public TypedValue getValue() { + return new TypedValue(arrayNode.get(index)); + } + + @Override + public void setValue(Object newValue) { + arrayNode.set(index,objectMapper.convertValue(newValue, JsonNode.class)); + } + public void setArrayNode(ArrayNode arrayNode){ + this.arrayNode=arrayNode; + } + + public void setIndex(Object index) { + if (index instanceof Integer) { + this.index = (Integer) index; + } + } + + @Override + public boolean isWritable() { + return false; + } + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[]{ + ArrayNode.class + }; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, Object index) throws AccessException { + return true; + } + + @Override + public ValueRef read(EvaluationContext context, Object target, Object index) throws AccessException { + ArrayValueRef valueRef = new ArrayValueRef(); + valueRef.setArrayNode((ArrayNode) target); + valueRef.setIndex(index); + return valueRef; + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, Object index) throws AccessException { + return true; + } + + @Override + public void write(EvaluationContext context, Object target, Object index, Object newValue) throws AccessException { + ValueRef valueRef=read(context,target,index); + valueRef.setValue(newValue); + } + } + }