Skip to content

Commit 9b85c93

Browse files
committed
Polishing
1 parent 65f118b commit 9b85c93

File tree

3 files changed

+108
-48
lines changed

3 files changed

+108
-48
lines changed

spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import org.springframework.lang.Nullable;
2020

2121
/**
22-
* An index accessor is able to read from (and possibly write to) an indexed
23-
* structure of an object.
22+
* An index accessor is able to read from and possibly write to an indexed
23+
* structure of a target object.
2424
*
2525
* <p>This interface places no restrictions on what constitutes an indexed
2626
* structure. Implementors are therefore free to access indexed values any way
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package example;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
/**
23+
* Type that can be indexed by the {@link Color} enum (i.e., something other
24+
* than an int, Integer, or String) and whose indexed values are Strings.
25+
*/
26+
public class FruitMap {
27+
28+
private final Map<Color, String> map = new HashMap<>();
29+
30+
public FruitMap() {
31+
this.map.put(Color.RED, "cherry");
32+
this.map.put(Color.ORANGE, "orange");
33+
this.map.put(Color.YELLOW, "banana");
34+
this.map.put(Color.GREEN, "kiwi");
35+
this.map.put(Color.BLUE, "blueberry");
36+
// We don't map PURPLE so that we can test for an unsupported color.
37+
}
38+
39+
public String getFruit(Color color) {
40+
if (!this.map.containsKey(color)) {
41+
throw new IllegalArgumentException("No fruit for color " + color);
42+
}
43+
return this.map.get(color);
44+
}
45+
46+
public void setFruit(Color color, String fruit) {
47+
this.map.put(color, fruit);
48+
}
49+
50+
}

spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.stream.Stream;
3434

3535
import example.Color;
36+
import example.FruitMap;
3637
import org.junit.jupiter.api.Nested;
3738
import org.junit.jupiter.api.Test;
3839
import org.junit.jupiter.params.ParameterizedTest;
@@ -900,22 +901,22 @@ void indexWithReferenceIndexTypeAndPrimitiveValueType() {
900901
}
901902

902903
@ParameterizedTest(name = "{0}")
903-
@MethodSource("fruitsIndexAccessors")
904+
@MethodSource("fruitMapIndexAccessors")
904905
void indexWithReferenceIndexTypeAndReferenceValueType(IndexAccessor indexAccessor) {
905906
String exitTypeDescriptor = CodeFlow.toDescriptor(String.class);
906907

907908
StandardEvaluationContext context = new StandardEvaluationContext();
908909
context.addIndexAccessor(indexAccessor);
909-
context.setVariable("list", List.of(new Fruits()));
910+
context.setVariable("list", List.of(new FruitMap()));
910911

911912
expression = parser.parseExpression("#list.get(0)[T(example.Color).PURPLE]");
912913
assertCannotCompile(expression);
913914

914915
assertThatExceptionOfType(SpelEvaluationException.class)
915916
.isThrownBy(() -> expression.getValue(context))
916917
.withMessageEndingWith("A problem occurred while attempting to read index '%s' in '%s'",
917-
Color.PURPLE, Fruits.class.getName())
918-
.withCauseInstanceOf(IndexOutOfBoundsException.class)
918+
Color.PURPLE, FruitMap.class.getName())
919+
.withCauseInstanceOf(IllegalArgumentException.class)
919920
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
920921
assertCannotCompile(expression);
921922

@@ -943,12 +944,21 @@ void indexWithReferenceIndexTypeAndReferenceValueType(IndexAccessor indexAccesso
943944
assertCanCompile(expression);
944945
assertThat(expression.getValue(context)).isEqualTo("blueberry");
945946
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
947+
948+
// Set fruit for purple
949+
context.setVariable("color", Color.PURPLE);
950+
expression.setValue(context, "plum");
951+
assertCanCompile(expression);
952+
assertThat(expression.getValue(context)).isEqualTo("plum");
953+
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
946954
}
947955

948-
static Stream<Arguments> fruitsIndexAccessors() {
956+
static Stream<Arguments> fruitMapIndexAccessors() {
949957
return Stream.of(
950-
arguments(named("FruitsIndexAccessor", new FruitsIndexAccessor())),
951-
arguments(named("ReflectiveIndexAccessor", new ReflectiveIndexAccessor(Fruits.class, Color.class, "get")))
958+
arguments(named("FruitMapIndexAccessor",
959+
new FruitMapIndexAccessor())),
960+
arguments(named("ReflectiveIndexAccessor",
961+
new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")))
952962
);
953963
}
954964
}
@@ -1185,26 +1195,26 @@ void nullSafeIndexWithReferenceIndexTypeAndReferenceValueType() {
11851195
String exitTypeDescriptor = CodeFlow.toDescriptor(String.class);
11861196

11871197
StandardEvaluationContext context = new StandardEvaluationContext();
1188-
context.addIndexAccessor(new FruitsIndexAccessor());
1198+
context.addIndexAccessor(new FruitMapIndexAccessor());
11891199
context.setVariable("color", Color.RED);
11901200

1191-
expression = parser.parseExpression("#fruits?.[#color]");
1201+
expression = parser.parseExpression("#fruitMap?.[#color]");
11921202

11931203
// Cannot compile before the indexed value type is known.
11941204
assertThat(expression.getValue(context)).isNull();
11951205
assertCannotCompile(expression);
11961206
assertThat(expression.getValue(context)).isNull();
11971207
assertThat(getAst().getExitDescriptor()).isNull();
11981208

1199-
context.setVariable("fruits", new Fruits());
1209+
context.setVariable("fruitMap", new FruitMap());
12001210

12011211
assertThat(expression.getValue(context)).isEqualTo("cherry");
12021212
assertCanCompile(expression);
12031213
assertThat(expression.getValue(context)).isEqualTo("cherry");
12041214
assertThat(getAst().getExitDescriptor()).isEqualTo(exitTypeDescriptor);
12051215

12061216
// Null-safe support should have been compiled once the indexed value type is known.
1207-
context.setVariable("fruits", null);
1217+
context.setVariable("fruitMap", null);
12081218
assertThat(expression.getValue(context)).isNull();
12091219
assertCanCompile(expression);
12101220
assertThat(expression.getValue(context)).isNull();
@@ -7249,18 +7259,34 @@ static class ReflectiveIndexAccessor implements CompilableIndexAccessor {
72497259

72507260
private final Method readMethodToInvoke;
72517261

7262+
@Nullable
7263+
private final Method writeMethodToInvoke;
7264+
72527265
private final String targetTypeDesc;
72537266

72547267
private final String methodDescr;
72557268

72567269

7257-
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName) {
7270+
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName,
7271+
@Nullable String writeMethodName) {
7272+
72587273
this.targetType = targetType;
72597274
this.indexType = indexType;
72607275
this.readMethod = ReflectionUtils.findMethod(targetType, readMethodName, indexType);
7261-
Assert.notNull(this.readMethod, () -> "Failed to find method '%s(%s)' in class '%s'."
7276+
Assert.notNull(this.readMethod, () -> "Failed to find read-method '%s(%s)' in class '%s'."
72627277
.formatted(readMethodName, indexType.getTypeName(), targetType.getTypeName()));
72637278
this.readMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(this.readMethod, targetType);
7279+
if (writeMethodName != null) {
7280+
Class<?> indexedValueType = this.readMethod.getReturnType();
7281+
Method writeMethod = ReflectionUtils.findMethod(targetType, writeMethodName, indexType, indexedValueType);
7282+
Assert.notNull(writeMethod, () -> "Failed to find write-method '%s(%s, %s)' in class '%s'."
7283+
.formatted(writeMethodName, indexType.getTypeName(), indexedValueType.getTypeName(),
7284+
targetType.getTypeName()));
7285+
this.writeMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(writeMethod, targetType);
7286+
}
7287+
else {
7288+
this.writeMethodToInvoke = null;
7289+
}
72647290
this.targetTypeDesc = CodeFlow.toDescriptor(targetType);
72657291
this.methodDescr = CodeFlow.createSignatureDescriptor(this.readMethod);
72667292
}
@@ -7286,12 +7312,13 @@ public TypedValue read(EvaluationContext context, Object target, Object index) {
72867312

72877313
@Override
72887314
public boolean canWrite(EvaluationContext context, Object target, Object index) {
7289-
return false;
7315+
return (this.writeMethodToInvoke != null && canRead(context, target, index));
72907316
}
72917317

72927318
@Override
72937319
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
7294-
throw new UnsupportedOperationException();
7320+
Assert.state(this.writeMethodToInvoke != null, "Write-method cannot be null");
7321+
ReflectionUtils.invokeMethod(this.writeMethodToInvoke, target, index, newValue);
72957322
}
72967323

72977324
@Override
@@ -7355,7 +7382,7 @@ public Color get(int index) {
73557382
private static class ColorsIndexAccessor extends ReflectiveIndexAccessor {
73567383

73577384
ColorsIndexAccessor() {
7358-
super(Colors.class, int.class, "get");
7385+
super(Colors.class, int.class, "get", null);
73597386
}
73607387
}
73617388

@@ -7376,41 +7403,21 @@ public int get(Color color) {
73767403
private static class ColorOrdinalsIndexAccessor extends ReflectiveIndexAccessor {
73777404

73787405
ColorOrdinalsIndexAccessor() {
7379-
super(ColorOrdinals.class, Color.class, "get");
7380-
}
7381-
}
7382-
7383-
/**
7384-
* Type that can be indexed by the {@link Color} enum (i.e., something other
7385-
* than an int, Integer, or String) and whose indexed values are Strings.
7386-
*/
7387-
public static class Fruits {
7388-
7389-
public String get(Color color) {
7390-
return switch (color) {
7391-
case RED -> "cherry";
7392-
case ORANGE -> "orange";
7393-
case YELLOW -> "banana";
7394-
case GREEN -> "kiwi";
7395-
case BLUE -> "blueberry";
7396-
// We don't map PURPLE so that we can test for IndexOutOfBoundsException.
7397-
// case PURPLE -> "plum";
7398-
default -> throw new IndexOutOfBoundsException("color " + color + " is not supported");
7399-
};
7406+
super(ColorOrdinals.class, Color.class, "get", null);
74007407
}
74017408
}
74027409

74037410
/**
74047411
* Manually implemented {@link CompilableIndexAccessor} that knows how to
7405-
* index into {@link Fruits}.
7412+
* index into {@link FruitMap}.
74067413
*/
7407-
private static class FruitsIndexAccessor implements CompilableIndexAccessor {
7414+
private static class FruitMapIndexAccessor implements CompilableIndexAccessor {
74087415

7409-
private final Class<?> targetType = Fruits.class;
7416+
private final Class<?> targetType = FruitMap.class;
74107417

74117418
private final Class<?> indexType = Color.class;
74127419

7413-
private final Method method = ReflectionUtils.findMethod(this.targetType, "get", this.indexType);
7420+
private final Method method = ReflectionUtils.findMethod(this.targetType, "getFruit", this.indexType);
74147421

74157422
private final String targetTypeDesc = CodeFlow.toDescriptor(this.targetType);
74167423

@@ -7431,19 +7438,22 @@ public boolean canRead(EvaluationContext context, Object target, Object index) {
74317438

74327439
@Override
74337440
public TypedValue read(EvaluationContext context, Object target, Object index) {
7434-
Fruits fruits = (Fruits) target;
7441+
FruitMap fruitMap = (FruitMap) target;
74357442
Color color = (Color) index;
7436-
return new TypedValue(fruits.get(color));
7443+
return new TypedValue(fruitMap.getFruit(color));
74377444
}
74387445

74397446
@Override
74407447
public boolean canWrite(EvaluationContext context, Object target, Object index) {
7441-
return false;
7448+
return canRead(context, target, index);
74427449
}
74437450

74447451
@Override
74457452
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
7446-
throw new UnsupportedOperationException();
7453+
FruitMap fruitMap = (FruitMap) target;
7454+
Color color = (Color) index;
7455+
String fruit = String.valueOf(newValue);
7456+
fruitMap.setFruit(color, fruit);
74477457
}
74487458

74497459
@Override
@@ -7465,7 +7475,7 @@ public void generateCode(SpelNode index, MethodVisitor mv, CodeFlow cf) {
74657475
}
74667476
// Push the index onto the stack.
74677477
cf.generateCodeForArgument(mv, index, Color.class);
7468-
// Invoke the read-index method.
7478+
// Invoke the read-method.
74697479
mv.visitMethodInsn(INVOKEVIRTUAL, this.classDesc, this.method.getName(), this.methodDescr, false);
74707480
}
74717481

0 commit comments

Comments
 (0)