From 11c40b5c1ca2877dd2a7f00c709de94dc4ba1ff5 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:12:53 +0100 Subject: [PATCH] Restructure SpEL indexer compilation tests --- .../spel/CompilableMapAccessor.java | 117 +++ .../spel/SpelCompilationCoverageTests.java | 981 +++++++++--------- .../spel/standard/SpelCompilerTests.java | 1 + 3 files changed, 627 insertions(+), 472 deletions(-) create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java new file mode 100644 index 0000000000..3557dfa98d --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java @@ -0,0 +1,117 @@ +/* + * 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 org.springframework.expression.spel; + +import java.util.Map; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * This is a local COPY of {@link org.springframework.context.expression.MapAccessor}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 4.1 + */ +class CompilableMapAccessor implements CompilablePropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof Map map && map.containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof Map, "Target must be of type Map"); + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + Assert.state(target instanceof Map, "Target must be a Map"); + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { + if (descriptor == null) { + cf.loadTarget(mv); + } + CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(""); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 9765586a84..f36de5a0ca 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -27,13 +29,14 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.asm.MethodVisitor; -import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; @@ -47,6 +50,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.testdata.PersonInOtherPackage; +import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -55,11 +59,64 @@ import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN; import static org.springframework.expression.spel.standard.SpelExpressionTestUtils.assertIsCompiled; /** - * Checks SpelCompiler behavior. This should cover compilation all compiled node types. + * Checks {@link org.springframework.expression.spel.standard.SpelCompiler} behavior. + * + *

This should cover compilation of all compiled node types. + * + *

Compiled nodes: + * + * TypeReference + * OperatorInstanceOf + * StringLiteral + * NullLiteral + * RealLiteral + * IntLiteral + * LongLiteral + * BooleanLiteral + * FloatLiteral + * OpOr + * OpAnd + * OperatorNot + * Ternary + * Elvis + * VariableReference + * OpLt + * OpLe + * OpGt + * OpGe + * OpEq + * OpNe + * OpPlus + * OpMinus + * OpMultiply + * OpDivide + * MethodReference + * PropertyOrFieldReference + * Indexer + * CompoundExpression + * ConstructorReference + * FunctionReference + * InlineList + * OpModulus + * + *

Not yet compiled (some may never need to be): + * + * Assign + * BeanReference + * Identifier + * OpDec + * OpBetween + * OpMatches + * OpPower + * OpInc + * Projection + * QualifiedId + * Selection * * @author Andy Clement * @author Sam Brannen * @since 4.1 + * @see org.springframework.expression.spel.standard.SpelCompilerTests */ public class SpelCompilationCoverageTests extends AbstractExpressionTests { @@ -67,72 +124,463 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { * Further TODOs for compilation: * * - OpMinus with a single literal operand could be treated as a negative literal. Will save a - * pointless loading of 0 and then a subtract instruction in code gen. - * - allow other accessors/resolvers to participate in compilation and create their own code + * pointless loading of 0 and then a subtract instruction in code geneneration. + * + * - allow other accessors/resolvers to participate in compilation and create their own code. + * * - A TypeReference followed by (what ends up as) a static method invocation can really skip - * code gen for the TypeReference since once that is used to locate the method it is not + * code generation for the TypeReference since once that is used to locate the method it is not * used again. + * * - The opEq implementation is quite basic. It will compare numbers of the same type (allowing * them to be their boxed or unboxed variants) or compare object references. It does not * compile expressions where numbers are of different types or when objects implement * Comparable. - * - * Compiled nodes: - * - * TypeReference - * OperatorInstanceOf - * StringLiteral - * NullLiteral - * RealLiteral - * IntLiteral - * LongLiteral - * BooleanLiteral - * FloatLiteral - * OpOr - * OpAnd - * OperatorNot - * Ternary - * Elvis - * VariableReference - * OpLt - * OpLe - * OpGt - * OpGe - * OpEq - * OpNe - * OpPlus - * OpMinus - * OpMultiply - * OpDivide - * MethodReference - * PropertyOrFieldReference - * Indexer - * CompoundExpression - * ConstructorReference - * FunctionReference - * InlineList - * OpModulus - * - * Not yet compiled (some may never need to be): - * Assign - * BeanReference - * Identifier - * OpDec - * OpBetween - * OpMatches - * OpPower - * OpInc - * Projection - * QualifiedId - * Selection */ - private Expression expression; private SpelNodeImpl ast; + @Nested + class IndexingTests { + + @Test + void indexIntoPrimitiveShortArray() { + short[] shorts = { (short) 33, (short) 44, (short) 55 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(shorts)).isEqualTo((short) 55); + assertCanCompile(expression); + assertThat(expression.getValue(shorts)).isEqualTo((short) 55); + assertThat(getAst().getExitDescriptor()).isEqualTo("S"); + } + + @Test + void indexIntoPrimitiveByteArray() { + byte[] bytes = { (byte) 2, (byte) 3, (byte) 4 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(bytes)).isEqualTo((byte) 4); + assertCanCompile(expression); + assertThat(expression.getValue(bytes)).isEqualTo((byte) 4); + assertThat(getAst().getExitDescriptor()).isEqualTo("B"); + } + + @Test + void indexIntoPrimitiveIntArray() { + int[] ints = { 8, 9, 10 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(ints)).isEqualTo(10); + assertCanCompile(expression); + assertThat(expression.getValue(ints)).isEqualTo(10); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexIntoPrimitiveLongArray() { + long[] longs = { 2L, 3L, 4L }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(longs)).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue(longs)).isEqualTo(2L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + } + + @Test + void indexIntoPrimitiveFloatArray() { + float[] floats = { 6.0f, 7.0f, 8.0f }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(floats)).isEqualTo(6.0f); + assertCanCompile(expression); + assertThat(expression.getValue(floats)).isEqualTo(6.0f); + assertThat(getAst().getExitDescriptor()).isEqualTo("F"); + } + + @Test + void indexIntoPrimitiveDoubleArray() { + double[] doubles = { 3.0d, 4.0d, 5.0d }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(doubles)).isEqualTo(4.0d); + assertCanCompile(expression); + assertThat(expression.getValue(doubles)).isEqualTo(4.0d); + assertThat(getAst().getExitDescriptor()).isEqualTo("D"); + } + + @Test + void indexIntoPrimitiveCharArray() { + char[] chars = { 'a', 'b', 'c' }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(chars)).isEqualTo('b'); + assertCanCompile(expression); + assertThat(expression.getValue(chars)).isEqualTo('b'); + assertThat(getAst().getExitDescriptor()).isEqualTo("C"); + } + + @Test + void indexIntoStringArray() { + String[] strings = { "a", "b", "c" }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(strings)).isEqualTo("a"); + assertCanCompile(expression); + assertThat(expression.getValue(strings)).isEqualTo("a"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + void indexIntoNumberArray() { + Number[] numbers = { 2, 8, 9 }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(numbers)).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(numbers)).isEqualTo(8); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); + } + + @Test + void indexInto2DPrimitiveIntArray() { + int[][] array = new int[][] { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[I"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexInto2DStringArray() { + String[][] array = new String[][] { + { "a", "b", "c" }, + { "d", "e", "f" } + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + @SuppressWarnings("unchecked") + void indexIntoArrayOfListOfString() { + List[] array = new List[] { + List.of("a", "b", "c"), + List.of("d", "e", "f") + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/List"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + @SuppressWarnings("unchecked") + void indexIntoArrayOfMap() { + Map[] array = new Map[] { Map.of("key", "value1") }; + + expression = parser.parseExpression("[0]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + + expression = parser.parseExpression("[0]['key']"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfString() { + List list = List.of("aaa", "bbb", "ccc"); + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(list)).isEqualTo("bbb"); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo("bbb"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfInteger() { + List list = List.of(123, 456, 789); + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(list)).isEqualTo(789); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(789); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfStringArray() { + List list = List.of( + new String[] { "a", "b", "c" }, + new String[] { "d", "e", "f" } + ); + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][0]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("d"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + void indexIntoListOfIntegerArray() { + List list = List.of( + new Integer[] { 1, 2, 3 }, + new Integer[] { 4, 5, 6 } + ); + + expression = parser.parseExpression("[0]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[0][1]"); + + assertThat(expression.getValue(list)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(2); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + } + + @Test + void indexIntoListOfListOfString() { + List> list = List.of( + List.of("a", "b", "c"), + List.of("d", "e", "f") + ); + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMap() { + Map map = Map.of("aaa", 111); + + expression = parser.parseExpression("['aaa']"); + + assertThat(expression.getValue(map)).isEqualTo(111); + assertCanCompile(expression); + assertThat(expression.getValue(map)).isEqualTo(111); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMapOfListOfString() { + Map> map = Map.of("foo", List.of("a", "b", "c")); + + expression = parser.parseExpression("['foo']"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("a b c"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(map))).isEqualTo("a b c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("['foo'][2]"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("c"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(map))).isEqualTo("c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMapOfPrimitiveIntArray() { + Map map = Map.of("foo", new int[] { 1, 2, 3 }); + + // map key access + expression = parser.parseExpression("['foo']"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(map))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // map key access & array index + expression = parser.parseExpression("['foo'][1]"); + + assertThat(expression.getValue(map)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(map)).isEqualTo(2); + } + + @Test + void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.addPropertyAccessor(new CompilableMapAccessor()); + + // Map map = Map.of("foo", new int[] { 1, 2, 3 }); + Map map = new HashMap<>(); + map.put("foo", new int[] { 1, 2, 3 }); + + // map key access + expression = parser.parseExpression("['foo']"); + + assertThat(stringify(expression.getValue(context, map))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(context, map))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // custom CompilableMapAccessor via implicit #root & array index + expression = parser.parseExpression("foo[1]"); + + assertThat(expression.getValue(context, map)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(context, map)).isEqualTo(2); + + // custom CompilableMapAccessor via explicit #root & array index + expression = parser.parseExpression("#root.foo[1]"); + + assertThat(expression.getValue(context, map)).isEqualTo(2); + assertCanCompile(expression); + // TODO If map is created via Map.of(), the following fails with an IllegalAccessError. + // + // IllegalAccessError: failed to access class java.util.ImmutableCollections$Map1 from class + // spel.Ex2774 (java.util.ImmutableCollections$Map1 is in module java.base of loader 'bootstrap'; + // spel.Ex2774 is in unnamed module of loader + // org.springframework.expression.spel.standard.SpelCompiler$ChildClassLoader @359b650b) + assertThat(expression.getValue(context, map)).isEqualTo(2); + + // map key access & array index + expression = parser.parseExpression("['foo'][2]"); + + assertThat(stringify(expression.getValue(context, map))).isEqualTo("3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(context, map))).isEqualTo("3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexIntoObject() { + TestClass6 tc = new TestClass6(); + + // field access + expression = parser.parseExpression("['orange']"); + + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + // field access + expression = parser.parseExpression("['peach']"); + + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + + // property access (getter) + expression = parser.parseExpression("['banana']"); + + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + private String stringify(Object object) { + Stream stream; + if (object instanceof Collection collection) { + stream = collection.stream(); + } + else if (object instanceof Object[] objects) { + stream = Arrays.stream(objects); + } + else if (object instanceof int[] ints) { + stream = Arrays.stream(ints).mapToObj(Integer::valueOf); + } + else { + return String.valueOf(object); + } + return stream.map(Object::toString).collect(joining(" ")); + } + + } + @Test void typeReference() { expression = parse("T(String)"); @@ -4356,314 +4804,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { assertThat(expression.getValue(ctx)).isEqualTo("wibble"); } - @SuppressWarnings("unchecked") - @Test - void indexer() { - String[] sss = new String[] {"a","b","c"}; - Number[] ns = new Number[] {2,8,9}; - int[] is = new int[] {8,9,10}; - double[] ds = new double[] {3.0d,4.0d,5.0d}; - long[] ls = new long[] {2L,3L,4L}; - short[] ss = new short[] {(short)33,(short)44,(short)55}; - float[] fs = new float[] {6.0f,7.0f,8.0f}; - byte[] bs = new byte[] {(byte)2,(byte)3,(byte)4}; - char[] cs = new char[] {'a','b','c'}; - - // Access String (reference type) array - expression = parser.parseExpression("[0]"); - assertThat(expression.getValue(sss)).isEqualTo("a"); - assertCanCompile(expression); - assertThat(expression.getValue(sss)).isEqualTo("a"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); - - expression = parser.parseExpression("[1]"); - assertThat(expression.getValue(ns)).isEqualTo(8); - assertCanCompile(expression); - assertThat(expression.getValue(ns)).isEqualTo(8); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); - - // Access int array - expression = parser.parseExpression("[2]"); - assertThat(expression.getValue(is)).isEqualTo(10); - assertCanCompile(expression); - assertThat(expression.getValue(is)).isEqualTo(10); - assertThat(getAst().getExitDescriptor()).isEqualTo("I"); - - // Access double array - expression = parser.parseExpression("[1]"); - assertThat(expression.getValue(ds)).isEqualTo(4.0d); - assertCanCompile(expression); - assertThat(expression.getValue(ds)).isEqualTo(4.0d); - assertThat(getAst().getExitDescriptor()).isEqualTo("D"); - - // Access long array - expression = parser.parseExpression("[0]"); - assertThat(expression.getValue(ls)).isEqualTo(2L); - assertCanCompile(expression); - assertThat(expression.getValue(ls)).isEqualTo(2L); - assertThat(getAst().getExitDescriptor()).isEqualTo("J"); - - // Access short array - expression = parser.parseExpression("[2]"); - assertThat(expression.getValue(ss)).isEqualTo((short)55); - assertCanCompile(expression); - assertThat(expression.getValue(ss)).isEqualTo((short)55); - assertThat(getAst().getExitDescriptor()).isEqualTo("S"); - - // Access float array - expression = parser.parseExpression("[0]"); - assertThat(expression.getValue(fs)).isEqualTo(6.0f); - assertCanCompile(expression); - assertThat(expression.getValue(fs)).isEqualTo(6.0f); - assertThat(getAst().getExitDescriptor()).isEqualTo("F"); - - // Access byte array - expression = parser.parseExpression("[2]"); - assertThat(expression.getValue(bs)).isEqualTo((byte)4); - assertCanCompile(expression); - assertThat(expression.getValue(bs)).isEqualTo((byte)4); - assertThat(getAst().getExitDescriptor()).isEqualTo("B"); - - // Access char array - expression = parser.parseExpression("[1]"); - assertThat(expression.getValue(cs)).isEqualTo('b'); - assertCanCompile(expression); - assertThat(expression.getValue(cs)).isEqualTo('b'); - assertThat(getAst().getExitDescriptor()).isEqualTo("C"); - - // Collections - List strings = new ArrayList<>(); - strings.add("aaa"); - strings.add("bbb"); - strings.add("ccc"); - expression = parser.parseExpression("[1]"); - assertThat(expression.getValue(strings)).isEqualTo("bbb"); - assertCanCompile(expression); - assertThat(expression.getValue(strings)).isEqualTo("bbb"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - List ints = new ArrayList<>(); - ints.add(123); - ints.add(456); - ints.add(789); - expression = parser.parseExpression("[2]"); - assertThat(expression.getValue(ints)).isEqualTo(789); - assertCanCompile(expression); - assertThat(expression.getValue(ints)).isEqualTo(789); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - // Maps - Map map1 = new HashMap<>(); - map1.put("aaa", 111); - map1.put("bbb", 222); - map1.put("ccc", 333); - expression = parser.parseExpression("['aaa']"); - assertThat(expression.getValue(map1)).isEqualTo(111); - assertCanCompile(expression); - assertThat(expression.getValue(map1)).isEqualTo(111); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - // Object - TestClass6 tc = new TestClass6(); - expression = parser.parseExpression("['orange']"); - assertThat(expression.getValue(tc)).isEqualTo("value1"); - assertCanCompile(expression); - assertThat(expression.getValue(tc)).isEqualTo("value1"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); - - expression = parser.parseExpression("['peach']"); - assertThat(expression.getValue(tc)).isEqualTo(34L); - assertCanCompile(expression); - assertThat(expression.getValue(tc)).isEqualTo(34L); - assertThat(getAst().getExitDescriptor()).isEqualTo("J"); - - // getter - expression = parser.parseExpression("['banana']"); - assertThat(expression.getValue(tc)).isEqualTo("value3"); - assertCanCompile(expression); - assertThat(expression.getValue(tc)).isEqualTo("value3"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); - - // list of arrays - - List listOfStringArrays = new ArrayList<>(); - listOfStringArrays.add(new String[] {"a","b","c"}); - listOfStringArrays.add(new String[] {"d","e","f"}); - expression = parser.parseExpression("[1]"); - assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d e f"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d e f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - expression = parser.parseExpression("[1][0]"); - assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); - - List listOfIntegerArrays = new ArrayList<>(); - listOfIntegerArrays.add(new Integer[] {1,2,3}); - listOfIntegerArrays.add(new Integer[] {4,5,6}); - expression = parser.parseExpression("[0]"); - assertThat(stringify(expression.getValue(listOfIntegerArrays))).isEqualTo("1 2 3"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(listOfIntegerArrays))).isEqualTo("1 2 3"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - expression = parser.parseExpression("[0][1]"); - assertThat(expression.getValue(listOfIntegerArrays)).isEqualTo(2); - assertCanCompile(expression); - assertThat(expression.getValue(listOfIntegerArrays)).isEqualTo(2); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); - - // array of lists - List[] stringArrayOfLists = new ArrayList[2]; - stringArrayOfLists[0] = new ArrayList<>(); - stringArrayOfLists[0].add("a"); - stringArrayOfLists[0].add("b"); - stringArrayOfLists[0].add("c"); - stringArrayOfLists[1] = new ArrayList<>(); - stringArrayOfLists[1].add("d"); - stringArrayOfLists[1].add("e"); - stringArrayOfLists[1].add("f"); - expression = parser.parseExpression("[1]"); - assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("d e f"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("d e f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/ArrayList"); - - expression = parser.parseExpression("[1][2]"); - assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("f"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - // array of arrays - String[][] referenceTypeArrayOfArrays = new String[][] {new String[] {"a","b","c"},new String[] {"d","e","f"}}; - expression = parser.parseExpression("[1]"); - assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("d e f"); - assertCanCompile(expression); - assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); - assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("d e f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); - - expression = parser.parseExpression("[1][2]"); - assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("f"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); - - int[][] primitiveTypeArrayOfArrays = new int[][] {new int[] {1,2,3},new int[] {4,5,6}}; - expression = parser.parseExpression("[1]"); - assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("4 5 6"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("4 5 6"); - assertThat(getAst().getExitDescriptor()).isEqualTo("[I"); - - expression = parser.parseExpression("[1][2]"); - assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("6"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("6"); - assertThat(getAst().getExitDescriptor()).isEqualTo("I"); - - // list of lists of reference types - List> listOfListOfStrings = new ArrayList<>(); - List list = new ArrayList<>(); - list.add("a"); - list.add("b"); - list.add("c"); - listOfListOfStrings.add(list); - list = new ArrayList<>(); - list.add("d"); - list.add("e"); - list.add("f"); - listOfListOfStrings.add(list); - - expression = parser.parseExpression("[1]"); - assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("d e f"); - assertCanCompile(expression); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("d e f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - expression = parser.parseExpression("[1][2]"); - assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("f"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("f"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - // Map of lists - Map> mapToLists = new HashMap<>(); - list = new ArrayList<>(); - list.add("a"); - list.add("b"); - list.add("c"); - mapToLists.put("foo", list); - expression = parser.parseExpression("['foo']"); - assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("a b c"); - assertCanCompile(expression); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("a b c"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - expression = parser.parseExpression("['foo'][2]"); - assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("c"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("c"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - // Map to array - Map mapToIntArray = new HashMap<>(); - StandardEvaluationContext ctx = new StandardEvaluationContext(); - ctx.addPropertyAccessor(new CompilableMapAccessor()); - mapToIntArray.put("foo",new int[] {1,2,3}); - expression = parser.parseExpression("['foo']"); - assertThat(stringify(expression.getValue(mapToIntArray))).isEqualTo("1 2 3"); - assertCanCompile(expression); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - assertThat(stringify(expression.getValue(mapToIntArray))).isEqualTo("1 2 3"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - expression = parser.parseExpression("['foo'][1]"); - assertThat(expression.getValue(mapToIntArray)).isEqualTo(2); - assertCanCompile(expression); - assertThat(expression.getValue(mapToIntArray)).isEqualTo(2); - - expression = parser.parseExpression("foo"); - assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("1 2 3"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("1 2 3"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - - expression = parser.parseExpression("foo[1]"); - assertThat(expression.getValue(ctx, mapToIntArray)).isEqualTo(2); - assertCanCompile(expression); - assertThat(expression.getValue(ctx, mapToIntArray)).isEqualTo(2); - - expression = parser.parseExpression("['foo'][2]"); - assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("3"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("3"); - assertThat(getAst().getExitDescriptor()).isEqualTo("I"); - - // Map array - Map[] mapArray = new Map[1]; - mapArray[0] = new HashMap<>(); - mapArray[0].put("key", "value1"); - expression = parser.parseExpression("[0]"); - assertThat(stringify(expression.getValue(mapArray))).isEqualTo("{key=value1}"); - assertCanCompile(expression); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); - assertThat(stringify(expression.getValue(mapArray))).isEqualTo("{key=value1}"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); - - expression = parser.parseExpression("[0]['key']"); - assertThat(stringify(expression.getValue(mapArray))).isEqualTo("value1"); - assertCanCompile(expression); - assertThat(stringify(expression.getValue(mapArray))).isEqualTo("value1"); - assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); - } - @Test void plusNeedingCheckcast_SPR12426() { expression = parser.parseExpression("object + ' world'"); @@ -5261,32 +5401,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { return (SpelNodeImpl)ast; } - private String stringify(Object object) { - StringBuilder s = new StringBuilder(); - if (object instanceof List list) { - for (Object l: list) { - s.append(l); - s.append(' '); - } - } - else if (object instanceof Object[] objects) { - for (Object o: objects) { - s.append(o); - s.append(' '); - } - } - else if (object instanceof int[] ints) { - for (int i: ints) { - s.append(i); - s.append(' '); - } - } - else { - s.append(object); - } - return s.toString().trim(); - } - private void assertCanCompile(Expression expression) { assertThat(SpelCompiler.compile(expression)).isTrue(); } @@ -5447,84 +5561,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { } - static class CompilableMapAccessor implements CompilablePropertyAccessor { - - @Override - public boolean canRead(EvaluationContext context, Object target, String name) { - Map map = (Map) target; - return map.containsKey(name); - } - - @Override - public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { - Map map = (Map) target; - Object value = map.get(name); - if (value == null && !map.containsKey(name)) { - throw new MapAccessException(name); - } - return new TypedValue(value); - } - - @Override - public boolean canWrite(EvaluationContext context, Object target, String name) { - return true; - } - - @Override - @SuppressWarnings("unchecked") - public void write(EvaluationContext context, Object target, String name, Object newValue) { - Map map = (Map) target; - map.put(name, newValue); - } - - @Override - public Class[] getSpecificTargetClasses() { - return new Class[] {Map.class}; - } - - @Override - public boolean isCompilable() { - return true; - } - - @Override - public Class getPropertyType() { - return Object.class; - } - - @Override - public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { - String descriptor = cf.lastDescriptor(); - if (descriptor == null) { - cf.loadTarget(mv); - } - mv.visitLdcInsn(propertyName); - mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); - } - } - - - /** - * Exception thrown from {@code read} in order to reset a cached - * PropertyAccessor, allowing other accessors to have a try. - */ - @SuppressWarnings("serial") - private static class MapAccessException extends AccessException { - - private final String key; - - public MapAccessException(String key) { - super(null); - this.key = key; - } - - @Override - public String getMessage() { - return "Map does not contain a value for key '" + this.key + "'"; - } - } - - public static class Greeter { public String getWorld() { @@ -5986,21 +6022,22 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { public static class TestClass6 { - public String orange = "value1"; public static String apple = "value2"; + + public String orange = "value1"; public long peach = 34L; public String getBanana() { return "value3"; } - public static String getPlum() { - return "value4"; - } - public String strawberry() { return "value5"; } + + public static String getPlum() { + return "value4"; + } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java index 2346e01dc1..d0e05eb58d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java @@ -36,6 +36,7 @@ import static org.springframework.expression.spel.standard.SpelExpressionTestUti * @author Sam Brannen * @author Andy Clement * @since 5.1.14 + * @see org.springframework.expression.spel.SpelCompilationCoverageTests */ class SpelCompilerTests {