Polishing
This commit is contained in:
parent
65f118bdec
commit
9b85c93b6b
|
@ -19,8 +19,8 @@ package org.springframework.expression;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An index accessor is able to read from (and possibly write to) an indexed
|
* An index accessor is able to read from and possibly write to an indexed
|
||||||
* structure of an object.
|
* structure of a target object.
|
||||||
*
|
*
|
||||||
* <p>This interface places no restrictions on what constitutes an indexed
|
* <p>This interface places no restrictions on what constitutes an indexed
|
||||||
* structure. Implementors are therefore free to access indexed values any way
|
* structure. Implementors are therefore free to access indexed values any way
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 example;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type that can be indexed by the {@link Color} enum (i.e., something other
|
||||||
|
* than an int, Integer, or String) and whose indexed values are Strings.
|
||||||
|
*/
|
||||||
|
public class FruitMap {
|
||||||
|
|
||||||
|
private final Map<Color, String> map = new HashMap<>();
|
||||||
|
|
||||||
|
public FruitMap() {
|
||||||
|
this.map.put(Color.RED, "cherry");
|
||||||
|
this.map.put(Color.ORANGE, "orange");
|
||||||
|
this.map.put(Color.YELLOW, "banana");
|
||||||
|
this.map.put(Color.GREEN, "kiwi");
|
||||||
|
this.map.put(Color.BLUE, "blueberry");
|
||||||
|
// We don't map PURPLE so that we can test for an unsupported color.
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFruit(Color color) {
|
||||||
|
if (!this.map.containsKey(color)) {
|
||||||
|
throw new IllegalArgumentException("No fruit for color " + color);
|
||||||
|
}
|
||||||
|
return this.map.get(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFruit(Color color, String fruit) {
|
||||||
|
this.map.put(color, fruit);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ import java.util.StringTokenizer;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import example.Color;
|
import example.Color;
|
||||||
|
import example.FruitMap;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
@ -900,13 +901,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0}")
|
@ParameterizedTest(name = "{0}")
|
||||||
@MethodSource("fruitsIndexAccessors")
|
@MethodSource("fruitMapIndexAccessors")
|
||||||
void indexWithReferenceIndexTypeAndReferenceValueType(IndexAccessor indexAccessor) {
|
void indexWithReferenceIndexTypeAndReferenceValueType(IndexAccessor indexAccessor) {
|
||||||
String exitTypeDescriptor = CodeFlow.toDescriptor(String.class);
|
String exitTypeDescriptor = CodeFlow.toDescriptor(String.class);
|
||||||
|
|
||||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||||
context.addIndexAccessor(indexAccessor);
|
context.addIndexAccessor(indexAccessor);
|
||||||
context.setVariable("list", List.of(new Fruits()));
|
context.setVariable("list", List.of(new FruitMap()));
|
||||||
|
|
||||||
expression = parser.parseExpression("#list.get(0)[T(example.Color).PURPLE]");
|
expression = parser.parseExpression("#list.get(0)[T(example.Color).PURPLE]");
|
||||||
assertCannotCompile(expression);
|
assertCannotCompile(expression);
|
||||||
|
@ -914,8 +915,8 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||||
.isThrownBy(() -> expression.getValue(context))
|
.isThrownBy(() -> expression.getValue(context))
|
||||||
.withMessageEndingWith("A problem occurred while attempting to read index '%s' in '%s'",
|
.withMessageEndingWith("A problem occurred while attempting to read index '%s' in '%s'",
|
||||||
Color.PURPLE, Fruits.class.getName())
|
Color.PURPLE, FruitMap.class.getName())
|
||||||
.withCauseInstanceOf(IndexOutOfBoundsException.class)
|
.withCauseInstanceOf(IllegalArgumentException.class)
|
||||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
|
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
|
||||||
assertCannotCompile(expression);
|
assertCannotCompile(expression);
|
||||||
|
|
||||||
|
@ -943,12 +944,21 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
assertCanCompile(expression);
|
assertCanCompile(expression);
|
||||||
assertThat(expression.getValue(context)).isEqualTo("blueberry");
|
assertThat(expression.getValue(context)).isEqualTo("blueberry");
|
||||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
|
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
|
||||||
|
|
||||||
|
// Set fruit for purple
|
||||||
|
context.setVariable("color", Color.PURPLE);
|
||||||
|
expression.setValue(context, "plum");
|
||||||
|
assertCanCompile(expression);
|
||||||
|
assertThat(expression.getValue(context)).isEqualTo("plum");
|
||||||
|
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<Arguments> fruitsIndexAccessors() {
|
static Stream<Arguments> fruitMapIndexAccessors() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
arguments(named("FruitsIndexAccessor", new FruitsIndexAccessor())),
|
arguments(named("FruitMapIndexAccessor",
|
||||||
arguments(named("ReflectiveIndexAccessor", new ReflectiveIndexAccessor(Fruits.class, Color.class, "get")))
|
new FruitMapIndexAccessor())),
|
||||||
|
arguments(named("ReflectiveIndexAccessor",
|
||||||
|
new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1185,10 +1195,10 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
String exitTypeDescriptor = CodeFlow.toDescriptor(String.class);
|
String exitTypeDescriptor = CodeFlow.toDescriptor(String.class);
|
||||||
|
|
||||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||||
context.addIndexAccessor(new FruitsIndexAccessor());
|
context.addIndexAccessor(new FruitMapIndexAccessor());
|
||||||
context.setVariable("color", Color.RED);
|
context.setVariable("color", Color.RED);
|
||||||
|
|
||||||
expression = parser.parseExpression("#fruits?.[#color]");
|
expression = parser.parseExpression("#fruitMap?.[#color]");
|
||||||
|
|
||||||
// Cannot compile before the indexed value type is known.
|
// Cannot compile before the indexed value type is known.
|
||||||
assertThat(expression.getValue(context)).isNull();
|
assertThat(expression.getValue(context)).isNull();
|
||||||
|
@ -1196,7 +1206,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
assertThat(expression.getValue(context)).isNull();
|
assertThat(expression.getValue(context)).isNull();
|
||||||
assertThat(getAst().getExitDescriptor()).isNull();
|
assertThat(getAst().getExitDescriptor()).isNull();
|
||||||
|
|
||||||
context.setVariable("fruits", new Fruits());
|
context.setVariable("fruitMap", new FruitMap());
|
||||||
|
|
||||||
assertThat(expression.getValue(context)).isEqualTo("cherry");
|
assertThat(expression.getValue(context)).isEqualTo("cherry");
|
||||||
assertCanCompile(expression);
|
assertCanCompile(expression);
|
||||||
|
@ -1204,7 +1214,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
|
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
|
||||||
|
|
||||||
// Null-safe support should have been compiled once the indexed value type is known.
|
// Null-safe support should have been compiled once the indexed value type is known.
|
||||||
context.setVariable("fruits", null);
|
context.setVariable("fruitMap", null);
|
||||||
assertThat(expression.getValue(context)).isNull();
|
assertThat(expression.getValue(context)).isNull();
|
||||||
assertCanCompile(expression);
|
assertCanCompile(expression);
|
||||||
assertThat(expression.getValue(context)).isNull();
|
assertThat(expression.getValue(context)).isNull();
|
||||||
|
@ -7249,18 +7259,34 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
|
|
||||||
private final Method readMethodToInvoke;
|
private final Method readMethodToInvoke;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Method writeMethodToInvoke;
|
||||||
|
|
||||||
private final String targetTypeDesc;
|
private final String targetTypeDesc;
|
||||||
|
|
||||||
private final String methodDescr;
|
private final String methodDescr;
|
||||||
|
|
||||||
|
|
||||||
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName) {
|
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName,
|
||||||
|
@Nullable String writeMethodName) {
|
||||||
|
|
||||||
this.targetType = targetType;
|
this.targetType = targetType;
|
||||||
this.indexType = indexType;
|
this.indexType = indexType;
|
||||||
this.readMethod = ReflectionUtils.findMethod(targetType, readMethodName, indexType);
|
this.readMethod = ReflectionUtils.findMethod(targetType, readMethodName, indexType);
|
||||||
Assert.notNull(this.readMethod, () -> "Failed to find method '%s(%s)' in class '%s'."
|
Assert.notNull(this.readMethod, () -> "Failed to find read-method '%s(%s)' in class '%s'."
|
||||||
.formatted(readMethodName, indexType.getTypeName(), targetType.getTypeName()));
|
.formatted(readMethodName, indexType.getTypeName(), targetType.getTypeName()));
|
||||||
this.readMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(this.readMethod, targetType);
|
this.readMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(this.readMethod, targetType);
|
||||||
|
if (writeMethodName != null) {
|
||||||
|
Class<?> indexedValueType = this.readMethod.getReturnType();
|
||||||
|
Method writeMethod = ReflectionUtils.findMethod(targetType, writeMethodName, indexType, indexedValueType);
|
||||||
|
Assert.notNull(writeMethod, () -> "Failed to find write-method '%s(%s, %s)' in class '%s'."
|
||||||
|
.formatted(writeMethodName, indexType.getTypeName(), indexedValueType.getTypeName(),
|
||||||
|
targetType.getTypeName()));
|
||||||
|
this.writeMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(writeMethod, targetType);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.writeMethodToInvoke = null;
|
||||||
|
}
|
||||||
this.targetTypeDesc = CodeFlow.toDescriptor(targetType);
|
this.targetTypeDesc = CodeFlow.toDescriptor(targetType);
|
||||||
this.methodDescr = CodeFlow.createSignatureDescriptor(this.readMethod);
|
this.methodDescr = CodeFlow.createSignatureDescriptor(this.readMethod);
|
||||||
}
|
}
|
||||||
|
@ -7286,12 +7312,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canWrite(EvaluationContext context, Object target, Object index) {
|
public boolean canWrite(EvaluationContext context, Object target, Object index) {
|
||||||
return false;
|
return (this.writeMethodToInvoke != null && canRead(context, target, index));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
|
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
|
||||||
throw new UnsupportedOperationException();
|
Assert.state(this.writeMethodToInvoke != null, "Write-method cannot be null");
|
||||||
|
ReflectionUtils.invokeMethod(this.writeMethodToInvoke, target, index, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -7355,7 +7382,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
private static class ColorsIndexAccessor extends ReflectiveIndexAccessor {
|
private static class ColorsIndexAccessor extends ReflectiveIndexAccessor {
|
||||||
|
|
||||||
ColorsIndexAccessor() {
|
ColorsIndexAccessor() {
|
||||||
super(Colors.class, int.class, "get");
|
super(Colors.class, int.class, "get", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7376,41 +7403,21 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
private static class ColorOrdinalsIndexAccessor extends ReflectiveIndexAccessor {
|
private static class ColorOrdinalsIndexAccessor extends ReflectiveIndexAccessor {
|
||||||
|
|
||||||
ColorOrdinalsIndexAccessor() {
|
ColorOrdinalsIndexAccessor() {
|
||||||
super(ColorOrdinals.class, Color.class, "get");
|
super(ColorOrdinals.class, Color.class, "get", null);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type that can be indexed by the {@link Color} enum (i.e., something other
|
|
||||||
* than an int, Integer, or String) and whose indexed values are Strings.
|
|
||||||
*/
|
|
||||||
public static class Fruits {
|
|
||||||
|
|
||||||
public String get(Color color) {
|
|
||||||
return switch (color) {
|
|
||||||
case RED -> "cherry";
|
|
||||||
case ORANGE -> "orange";
|
|
||||||
case YELLOW -> "banana";
|
|
||||||
case GREEN -> "kiwi";
|
|
||||||
case BLUE -> "blueberry";
|
|
||||||
// We don't map PURPLE so that we can test for IndexOutOfBoundsException.
|
|
||||||
// case PURPLE -> "plum";
|
|
||||||
default -> throw new IndexOutOfBoundsException("color " + color + " is not supported");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually implemented {@link CompilableIndexAccessor} that knows how to
|
* Manually implemented {@link CompilableIndexAccessor} that knows how to
|
||||||
* index into {@link Fruits}.
|
* index into {@link FruitMap}.
|
||||||
*/
|
*/
|
||||||
private static class FruitsIndexAccessor implements CompilableIndexAccessor {
|
private static class FruitMapIndexAccessor implements CompilableIndexAccessor {
|
||||||
|
|
||||||
private final Class<?> targetType = Fruits.class;
|
private final Class<?> targetType = FruitMap.class;
|
||||||
|
|
||||||
private final Class<?> indexType = Color.class;
|
private final Class<?> indexType = Color.class;
|
||||||
|
|
||||||
private final Method method = ReflectionUtils.findMethod(this.targetType, "get", this.indexType);
|
private final Method method = ReflectionUtils.findMethod(this.targetType, "getFruit", this.indexType);
|
||||||
|
|
||||||
private final String targetTypeDesc = CodeFlow.toDescriptor(this.targetType);
|
private final String targetTypeDesc = CodeFlow.toDescriptor(this.targetType);
|
||||||
|
|
||||||
|
@ -7431,19 +7438,22 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TypedValue read(EvaluationContext context, Object target, Object index) {
|
public TypedValue read(EvaluationContext context, Object target, Object index) {
|
||||||
Fruits fruits = (Fruits) target;
|
FruitMap fruitMap = (FruitMap) target;
|
||||||
Color color = (Color) index;
|
Color color = (Color) index;
|
||||||
return new TypedValue(fruits.get(color));
|
return new TypedValue(fruitMap.getFruit(color));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canWrite(EvaluationContext context, Object target, Object index) {
|
public boolean canWrite(EvaluationContext context, Object target, Object index) {
|
||||||
return false;
|
return canRead(context, target, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
|
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
|
||||||
throw new UnsupportedOperationException();
|
FruitMap fruitMap = (FruitMap) target;
|
||||||
|
Color color = (Color) index;
|
||||||
|
String fruit = String.valueOf(newValue);
|
||||||
|
fruitMap.setFruit(color, fruit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -7465,7 +7475,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||||
}
|
}
|
||||||
// Push the index onto the stack.
|
// Push the index onto the stack.
|
||||||
cf.generateCodeForArgument(mv, index, Color.class);
|
cf.generateCodeForArgument(mv, index, Color.class);
|
||||||
// Invoke the read-index method.
|
// Invoke the read-method.
|
||||||
mv.visitMethodInsn(INVOKEVIRTUAL, this.classDesc, this.method.getName(), this.methodDescr, false);
|
mv.visitMethodInsn(INVOKEVIRTUAL, this.classDesc, this.method.getName(), this.methodDescr, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue