33
33
import java .util .stream .Stream ;
34
34
35
35
import example .Color ;
36
+ import example .FruitMap ;
36
37
import org .junit .jupiter .api .Nested ;
37
38
import org .junit .jupiter .api .Test ;
38
39
import org .junit .jupiter .params .ParameterizedTest ;
@@ -900,22 +901,22 @@ void indexWithReferenceIndexTypeAndPrimitiveValueType() {
900
901
}
901
902
902
903
@ ParameterizedTest (name = "{0}" )
903
- @ MethodSource ("fruitsIndexAccessors " )
904
+ @ MethodSource ("fruitMapIndexAccessors " )
904
905
void indexWithReferenceIndexTypeAndReferenceValueType (IndexAccessor indexAccessor ) {
905
906
String exitTypeDescriptor = CodeFlow .toDescriptor (String .class );
906
907
907
908
StandardEvaluationContext context = new StandardEvaluationContext ();
908
909
context .addIndexAccessor (indexAccessor );
909
- context .setVariable ("list" , List .of (new Fruits ()));
910
+ context .setVariable ("list" , List .of (new FruitMap ()));
910
911
911
912
expression = parser .parseExpression ("#list.get(0)[T(example.Color).PURPLE]" );
912
913
assertCannotCompile (expression );
913
914
914
915
assertThatExceptionOfType (SpelEvaluationException .class )
915
916
.isThrownBy (() -> expression .getValue (context ))
916
917
.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 )
919
920
.extracting (SpelEvaluationException ::getMessageCode ).isEqualTo (EXCEPTION_DURING_INDEX_READ );
920
921
assertCannotCompile (expression );
921
922
@@ -943,12 +944,21 @@ void indexWithReferenceIndexTypeAndReferenceValueType(IndexAccessor indexAccesso
943
944
assertCanCompile (expression );
944
945
assertThat (expression .getValue (context )).isEqualTo ("blueberry" );
945
946
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 );
946
954
}
947
955
948
- static Stream <Arguments > fruitsIndexAccessors () {
956
+ static Stream <Arguments > fruitMapIndexAccessors () {
949
957
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" )))
952
962
);
953
963
}
954
964
}
@@ -1185,26 +1195,26 @@ void nullSafeIndexWithReferenceIndexTypeAndReferenceValueType() {
1185
1195
String exitTypeDescriptor = CodeFlow .toDescriptor (String .class );
1186
1196
1187
1197
StandardEvaluationContext context = new StandardEvaluationContext ();
1188
- context .addIndexAccessor (new FruitsIndexAccessor ());
1198
+ context .addIndexAccessor (new FruitMapIndexAccessor ());
1189
1199
context .setVariable ("color" , Color .RED );
1190
1200
1191
- expression = parser .parseExpression ("#fruits ?.[#color]" );
1201
+ expression = parser .parseExpression ("#fruitMap ?.[#color]" );
1192
1202
1193
1203
// Cannot compile before the indexed value type is known.
1194
1204
assertThat (expression .getValue (context )).isNull ();
1195
1205
assertCannotCompile (expression );
1196
1206
assertThat (expression .getValue (context )).isNull ();
1197
1207
assertThat (getAst ().getExitDescriptor ()).isNull ();
1198
1208
1199
- context .setVariable ("fruits " , new Fruits ());
1209
+ context .setVariable ("fruitMap " , new FruitMap ());
1200
1210
1201
1211
assertThat (expression .getValue (context )).isEqualTo ("cherry" );
1202
1212
assertCanCompile (expression );
1203
1213
assertThat (expression .getValue (context )).isEqualTo ("cherry" );
1204
1214
assertThat (getAst ().getExitDescriptor ()).isEqualTo (exitTypeDescriptor );
1205
1215
1206
1216
// Null-safe support should have been compiled once the indexed value type is known.
1207
- context .setVariable ("fruits " , null );
1217
+ context .setVariable ("fruitMap " , null );
1208
1218
assertThat (expression .getValue (context )).isNull ();
1209
1219
assertCanCompile (expression );
1210
1220
assertThat (expression .getValue (context )).isNull ();
@@ -7249,18 +7259,34 @@ static class ReflectiveIndexAccessor implements CompilableIndexAccessor {
7249
7259
7250
7260
private final Method readMethodToInvoke ;
7251
7261
7262
+ @ Nullable
7263
+ private final Method writeMethodToInvoke ;
7264
+
7252
7265
private final String targetTypeDesc ;
7253
7266
7254
7267
private final String methodDescr ;
7255
7268
7256
7269
7257
- public ReflectiveIndexAccessor (Class <?> targetType , Class <?> indexType , String readMethodName ) {
7270
+ public ReflectiveIndexAccessor (Class <?> targetType , Class <?> indexType , String readMethodName ,
7271
+ @ Nullable String writeMethodName ) {
7272
+
7258
7273
this .targetType = targetType ;
7259
7274
this .indexType = indexType ;
7260
7275
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'."
7262
7277
.formatted (readMethodName , indexType .getTypeName (), targetType .getTypeName ()));
7263
7278
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
+ }
7264
7290
this .targetTypeDesc = CodeFlow .toDescriptor (targetType );
7265
7291
this .methodDescr = CodeFlow .createSignatureDescriptor (this .readMethod );
7266
7292
}
@@ -7286,12 +7312,13 @@ public TypedValue read(EvaluationContext context, Object target, Object index) {
7286
7312
7287
7313
@ Override
7288
7314
public boolean canWrite (EvaluationContext context , Object target , Object index ) {
7289
- return false ;
7315
+ return ( this . writeMethodToInvoke != null && canRead ( context , target , index )) ;
7290
7316
}
7291
7317
7292
7318
@ Override
7293
7319
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 );
7295
7322
}
7296
7323
7297
7324
@ Override
@@ -7355,7 +7382,7 @@ public Color get(int index) {
7355
7382
private static class ColorsIndexAccessor extends ReflectiveIndexAccessor {
7356
7383
7357
7384
ColorsIndexAccessor () {
7358
- super (Colors .class , int .class , "get" );
7385
+ super (Colors .class , int .class , "get" , null );
7359
7386
}
7360
7387
}
7361
7388
@@ -7376,41 +7403,21 @@ public int get(Color color) {
7376
7403
private static class ColorOrdinalsIndexAccessor extends ReflectiveIndexAccessor {
7377
7404
7378
7405
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 );
7400
7407
}
7401
7408
}
7402
7409
7403
7410
/**
7404
7411
* Manually implemented {@link CompilableIndexAccessor} that knows how to
7405
- * index into {@link Fruits }.
7412
+ * index into {@link FruitMap }.
7406
7413
*/
7407
- private static class FruitsIndexAccessor implements CompilableIndexAccessor {
7414
+ private static class FruitMapIndexAccessor implements CompilableIndexAccessor {
7408
7415
7409
- private final Class <?> targetType = Fruits .class ;
7416
+ private final Class <?> targetType = FruitMap .class ;
7410
7417
7411
7418
private final Class <?> indexType = Color .class ;
7412
7419
7413
- private final Method method = ReflectionUtils .findMethod (this .targetType , "get " , this .indexType );
7420
+ private final Method method = ReflectionUtils .findMethod (this .targetType , "getFruit " , this .indexType );
7414
7421
7415
7422
private final String targetTypeDesc = CodeFlow .toDescriptor (this .targetType );
7416
7423
@@ -7431,19 +7438,22 @@ public boolean canRead(EvaluationContext context, Object target, Object index) {
7431
7438
7432
7439
@ Override
7433
7440
public TypedValue read (EvaluationContext context , Object target , Object index ) {
7434
- Fruits fruits = (Fruits ) target ;
7441
+ FruitMap fruitMap = (FruitMap ) target ;
7435
7442
Color color = (Color ) index ;
7436
- return new TypedValue (fruits . get (color ));
7443
+ return new TypedValue (fruitMap . getFruit (color ));
7437
7444
}
7438
7445
7439
7446
@ Override
7440
7447
public boolean canWrite (EvaluationContext context , Object target , Object index ) {
7441
- return false ;
7448
+ return canRead ( context , target , index ) ;
7442
7449
}
7443
7450
7444
7451
@ Override
7445
7452
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 );
7447
7457
}
7448
7458
7449
7459
@ Override
@@ -7465,7 +7475,7 @@ public void generateCode(SpelNode index, MethodVisitor mv, CodeFlow cf) {
7465
7475
}
7466
7476
// Push the index onto the stack.
7467
7477
cf .generateCodeForArgument (mv , index , Color .class );
7468
- // Invoke the read-index method.
7478
+ // Invoke the read-method.
7469
7479
mv .visitMethodInsn (INVOKEVIRTUAL , this .classDesc , this .method .getName (), this .methodDescr , false );
7470
7480
}
7471
7481
0 commit comments