Initial review and polish of IndexAccessor support in SpEL
See gh-26409 See gh-26478
This commit is contained in:
@@ -28,13 +28,20 @@ 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.ast.ValueRef;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.testresources.Person;
|
||||
@@ -449,6 +456,135 @@ 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 indexReadWrite() {
|
||||
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 ValueRef read(EvaluationContext context, Object target, Object index) {
|
||||
return new ArrayNodeValueRef((ArrayNode) target, (Integer) index, this.objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, Object target, Object index) {
|
||||
return (target instanceof ArrayNode && index instanceof Integer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(EvaluationContext context, Object target, Object index, Object newValue) {
|
||||
if (!(target instanceof ArrayNode arrayNode)) {
|
||||
throw new IllegalStateException("target must be an ArrayNode: " + target.getClass().getName());
|
||||
}
|
||||
if (!(index instanceof Integer intIndex)) {
|
||||
throw new IllegalStateException("index must be an integer: " + target.getClass().getName());
|
||||
}
|
||||
|
||||
arrayNode.set(intIndex, this.objectMapper.convertValue(newValue, JsonNode.class));
|
||||
}
|
||||
|
||||
private record ArrayNodeValueRef(ArrayNode arrayNode, int index, ObjectMapper objectMapper) implements ValueRef {
|
||||
|
||||
@Override
|
||||
public TypedValue getValue() {
|
||||
return new TypedValue(this.arrayNode.get(this.index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object newValue) {
|
||||
// TODO throw new UnsupportedOperationException("setValue() is not supported");
|
||||
this.arrayNode.set(index, this.objectMapper.convertValue(newValue, JsonNode.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
||||
@@ -21,10 +21,6 @@ import java.util.Collections;
|
||||
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;
|
||||
@@ -32,10 +28,8 @@ import org.springframework.expression.AccessException;
|
||||
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;
|
||||
@@ -150,25 +144,6 @@ class PropertyAccessTests extends AbstractExpressionTests {
|
||||
assertThat(ctx.getPropertyAccessors()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddingRemovingIndexAccessors() {
|
||||
StandardEvaluationContext ctx = new StandardEvaluationContext();
|
||||
List<IndexAccessor> 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");
|
||||
@@ -248,24 +223,6 @@ class PropertyAccessTests extends AbstractExpressionTests {
|
||||
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() {
|
||||
@@ -406,67 +363,4 @@ class PropertyAccessTests extends AbstractExpressionTests {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user