From bdd8c109fc67f01fdb0de89f0753f3f782edf442 Mon Sep 17 00:00:00 2001 From: Pierre Lakreb Date: Sat, 24 Oct 2020 16:04:41 +0200 Subject: [PATCH] Add the ability for SpEL AST Indexer to interpret Iterable object * make object implementing java.lang.Iterable accessible with SpEL indexing * only read access is possible, as we cannot write an iterable object * make compiler compatible --- .../expression/spel/SpelMessage.java | 4 +- .../expression/spel/ast/Indexer.java | 77 +++++++++++++++++-- .../expression/spel/EvaluationTests.java | 2 +- .../expression/spel/IndexingTests.java | 50 ++++++++++++ .../spel/SpelCompilationCoverageTests.java | 37 +++++++++ 5 files changed, 162 insertions(+), 8 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 8998cb298889..67f8e0cefb30 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -189,8 +189,8 @@ public enum SpelMessage { RUN_OUT_OF_ARGUMENTS(Kind.ERROR, 1051, "Unexpectedly ran out of arguments"), - UNABLE_TO_GROW_COLLECTION(Kind.ERROR, 1052, - "Unable to grow collection"), + UNABLE_TO_GROW(Kind.ERROR, 1052, + "Unable to grow {0}"), UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE(Kind.ERROR, 1053, "Unable to grow collection: unable to determine list element type"), diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 04b502816169..9c0added1b73 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -56,7 +56,7 @@ // TODO support correct syntax for multidimensional [][][] and not [,,,] public class Indexer extends SpelNodeImpl { - private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} + private enum IndexedType {ARRAY, LIST, ITERABLE, MAP, STRING, OBJECT} // These fields are used when the indexer is being used as a property read accessor. @@ -159,7 +159,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException // If the object is something that looks indexable by an integer, // attempt to treat the index value as a number - if (target.getClass().isArray() || target instanceof Collection || target instanceof String) { + if (target.getClass().isArray() || target instanceof Iterable || target instanceof String) { int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); if (target.getClass().isArray()) { this.indexedType = IndexedType.ARRAY; @@ -173,6 +173,10 @@ else if (target instanceof Collection) { state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(), state.getConfiguration().getMaximumAutoGrowSize()); } + else if (target instanceof Iterable) { + this.indexedType = IndexedType.ITERABLE; + return new IterableIndexingValueRef((Iterable) target, idx, targetDescriptor); + } else { this.indexedType = IndexedType.STRING; return new StringIndexingLValue((String) target, idx, targetDescriptor); @@ -197,7 +201,7 @@ public boolean isCompilable() { if (this.indexedType == IndexedType.ARRAY) { return (this.exitTypeDescriptor != null); } - else if (this.indexedType == IndexedType.LIST) { + else if (this.indexedType == IndexedType.LIST || this.indexedType == IndexedType.ITERABLE) { return this.children[0].isCompilable(); } else if (this.indexedType == IndexedType.MAP) { @@ -271,6 +275,16 @@ else if (this.indexedType == IndexedType.LIST) { mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); } + else if (this.indexedType == IndexedType.ITERABLE) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Iterable"); + + cf.enterCompilationScope(); + this.children[0].generateCode(mv, cf); + cf.exitCompilationScope(); + + mv.visitMethodInsn(INVOKESTATIC, "org/springframework/expression/spel/ast/Indexer", "getFromIterableIdx", "(Ljava/lang/Iterable;I)Ljava/lang/Object;", false); + } + else if (this.indexedType == IndexedType.MAP) { mv.visitTypeInsn(CHECKCAST, "java/util/Map"); // Special case when the key is an unquoted string literal that will be parsed as @@ -455,6 +469,16 @@ private T convertValue(TypeConverter converter, @Nullable Object value, Clas return result; } + public static Object getFromIterableIdx(Iterable it, int idx) { + int pos = 0; + for (Object o : it) { + if (pos == idx) { + return o; + } + pos++; + } + throw new IllegalStateException("Failed to find indexed element " + idx + ": " + it); + } private class ArrayIndexingValueRef implements ValueRef { @@ -704,7 +728,7 @@ private void growCollectionIfNecessary() { this.collection.size(), this.index); } if (this.index >= this.maximumSize) { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION); + throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW, "collection"); } if (this.collectionEntryDescriptor.getElementTypeDescriptor() == null) { throw new SpelEvaluationException( @@ -721,7 +745,7 @@ private void growCollectionIfNecessary() { } } catch (Throwable ex) { - throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW_COLLECTION); + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW, "collection"); } } } @@ -742,6 +766,49 @@ public boolean isWritable() { } } + @SuppressWarnings({"rawtypes"}) + private class IterableIndexingValueRef implements ValueRef { + + private final Iterable iterable; + + private final int index; + + private final TypeDescriptor targetDescriptor; + + public IterableIndexingValueRef(Iterable iterable, int index, TypeDescriptor targetDescriptor) { + this.iterable = iterable; + this.index = index; + this.targetDescriptor = targetDescriptor; + } + + @Override + public TypedValue getValue() { + exitTypeDescriptor = CodeFlow.toDescriptor(Object.class); + Object o = getFromIterableIdx(this.iterable, this.index); + return new TypedValue(o, this.targetDescriptor.elementTypeDescriptor(o)); + } + + @Override + public void setValue(@Nullable Object newValue) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW, "'Iterable', not growable"); + } + + @Nullable + private Constructor getDefaultConstructor(Class type) { + try { + return ReflectionUtils.accessibleConstructor(type); + } + catch (Throwable ex) { + return null; + } + } + + @Override + public boolean isWritable() { + return false; + } + } + private class StringIndexingLValue implements ValueRef { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index 98b9704ea336..f7db8350d9a1 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -671,7 +671,7 @@ public void limitCollectionGrowing() { e.setValue(ctx, "3"); } catch (SpelEvaluationException see) { - assertThat(see.getMessageCode()).isEqualTo(SpelMessage.UNABLE_TO_GROW_COLLECTION); + assertThat(see.getMessageCode()).isEqualTo(SpelMessage.UNABLE_TO_GROW); assertThat(instance.getFoo().size()).isEqualTo(3); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index 922ec4a51a02..56879e0ca60c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -319,6 +320,55 @@ public void indexIntoGenericPropertyContainingGrowingList2() { public List property2; + @Test + public void indexIntoGenericPropertyContainingIterable() { + List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + parametizedIterable = new ParametizedIterable<>(list); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("parametizedIterable"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("org.springframework.expression.spel.IndexingTests$ParametizedIterable"); + assertThat(expression.getValue(this)).isEqualTo(parametizedIterable); + expression = parser.parseExpression("parametizedIterable[0]"); + assertThat(expression.getValue(this)).isEqualTo("foo"); + expression = parser.parseExpression("parametizedIterable[1]"); + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + + @Test + public void indexIntoGenericPropertyContainingGrowingIterable() { + List list = new ArrayList<>(); + parametizedIterable = new ParametizedIterable<>(list); + SpelParserConfiguration configuration = new SpelParserConfiguration(true, true); + SpelExpressionParser parser = new SpelExpressionParser(configuration); + Expression expression = parser.parseExpression("parametizedIterable"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("org.springframework.expression.spel.IndexingTests$ParametizedIterable"); + assertThat(expression.getValue(this)).isEqualTo(parametizedIterable); + expression = parser.parseExpression("parametizedIterable[0]"); + try { + expression.setValue(this, "bar"); + } + catch (EvaluationException ex) { + assertThat(ex.getMessage()).startsWith("EL1052E"); + } + } + + public ParametizedIterable parametizedIterable; + + private final class ParametizedIterable implements Iterable { + private final List property; + + private ParametizedIterable(List property) { + this.property = property; + } + + @Override + public Iterator iterator() { + return property.iterator(); + } + } + @Test public void indexIntoGenericPropertyContainingArray() { String[] property = new String[] { "bar" }; 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 993a6450f814..826aa3baccbe 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 @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -949,6 +950,28 @@ public void compiledExpressionShouldWorkWhenUsingCustomFunctionWithVarargs() thr assertCanCompile(expression); assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + expression = parser.parseExpression("#doFormat([0], 'there')"); + context = new StandardEvaluationContext(new IterableItems(List.of("hey %s"))); + context.registerFunction("doFormat", + DelegatingStringFormat.class.getDeclaredMethod("format", String.class, Object[].class)); + ((SpelExpression) expression).setEvaluationContext(context); + + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + assertThat(((SpelNodeImpl) ((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + + expression = parser.parseExpression("#doFormat([1], 'there')"); + context = new StandardEvaluationContext(new IterableItems(List.of("hey %s", "there %s"))); + context.registerFunction("doFormat", + DelegatingStringFormat.class.getDeclaredMethod("format", String.class, Object[].class)); + ((SpelExpression) expression).setEvaluationContext(context); + + assertThat(expression.getValue(String.class)).isEqualTo("there there"); + assertThat(((SpelNodeImpl) ((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("there there"); + expression = parser.parseExpression("#doFormat([0], #arg)"); context = new StandardEvaluationContext(new Object[] {"hey %s"}); context.registerFunction("doFormat", @@ -6256,4 +6279,18 @@ public void setValue2(Integer value) { } } + public class IterableItems implements Iterable { + + private List items; + + public IterableItems(List items) { + this.items = items; + } + + @Override + public Iterator iterator() { + return items.iterator(); + } + } + }