Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Iterable in SpEL Indexer #26323

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -455,6 +469,16 @@ private <T> 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 {

Expand Down Expand Up @@ -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(
Expand All @@ -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");
}
}
}
Expand All @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -319,6 +320,55 @@ public void indexIntoGenericPropertyContainingGrowingList2() {

public List property2;

@Test
public void indexIntoGenericPropertyContainingIterable() {
List<String> 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<java.lang.String>");
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<String> 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<java.lang.String>");
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<String> parametizedIterable;

private final class ParametizedIterable <T>implements Iterable<T> {
private final List<T> property;

private ParametizedIterable(List<T> property) {
this.property = property;
}

@Override
public Iterator<T> iterator() {
return property.iterator();
}
}

@Test
public void indexIntoGenericPropertyContainingArray() {
String[] property = new String[] { "bar" };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -6256,4 +6279,18 @@ public void setValue2(Integer value) {
}
}

public class IterableItems implements Iterable<String> {

private List<String> items;

public IterableItems(List<String> items) {
this.items = items;
}

@Override
public Iterator<String> iterator() {
return items.iterator();
}
}

}