From 3346752cfdccf80d993e4d3c90cca5431ec4ffaf Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Wed, 28 Oct 2009 03:42:36 +0000 Subject: [PATCH] INT-6051 SpEL selection and projection may now be applied to arrays. --- .../expression/spel/ast/Projection.java | 53 +++- .../expression/spel/ast/Selection.java | 22 +- .../spel/SelectionAndProjectionTests.java | 260 ++++++++++++++++++ 3 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 org.springframework.expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index bb8d4a56568..c8c2b517298 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -16,7 +16,9 @@ package org.springframework.expression.spel.ast; +import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -27,6 +29,8 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; /** * Represents projection, where a given operation is performed on all elements in some input sequence, returning @@ -34,7 +38,8 @@ import org.springframework.expression.spel.SpelMessage; * "{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}" returns "[n, y, n, y, n, y, n, y, n, y]" * * @author Andy Clement - * + * @author Mark Fisher + * @since 3.0 */ public class Projection extends SpelNodeImpl { @@ -50,6 +55,7 @@ public class Projection extends SpelNodeImpl { TypedValue op = state.getActiveContextObject(); Object operand = op.getValue(); + boolean operandIsArray = ObjectUtils.isArray(operand); // TypeDescriptor operandTypeDescriptor = op.getTypeDescriptor(); // When the input is a map, we push a special context object on the stack @@ -69,22 +75,36 @@ public class Projection extends SpelNodeImpl { } } return new TypedValue(result,TypeDescriptor.valueOf(List.class)); // TODO unable to build correct type descriptor - } else if (operand instanceof List) { + } else if (operand instanceof List || operandIsArray) { List data = new ArrayList(); - data.addAll((Collection) operand); + Collection c = (operand instanceof List) ? (Collection) operand : Arrays.asList(ObjectUtils.toObjectArray(operand)); + data.addAll(c); List result = new ArrayList(); int idx = 0; + Class arrayElementType = null; for (Object element : data) { try { state.pushActiveContextObject(new TypedValue(element,TypeDescriptor.valueOf(op.getTypeDescriptor().getType()))); state.enterScope("index", idx); - result.add(children[0].getValueInternal(state).getValue()); + Object value = children[0].getValueInternal(state).getValue(); + if (value != null && operandIsArray) { + arrayElementType = this.determineCommonType(arrayElementType, value.getClass()); + } + result.add(value); } finally { state.exitScope(); state.popActiveContextObject(); } idx++; } + if (operandIsArray) { + if (arrayElementType == null) { + arrayElementType = Object.class; + } + Object resultArray = Array.newInstance(arrayElementType, result.size()); + System.arraycopy(result.toArray(), 0, resultArray, 0, result.size()); + return new TypedValue(resultArray, op.getTypeDescriptor()); + } return new TypedValue(result,op.getTypeDescriptor()); } else { if (operand==null) { @@ -104,5 +124,28 @@ public class Projection extends SpelNodeImpl { StringBuilder sb = new StringBuilder(); return sb.append("![").append(getChild(0).toStringAST()).append("]").toString(); } - + + private Class determineCommonType(Class oldType, Class newType) { + if (oldType == null) { + return newType; + } + if (oldType.isAssignableFrom(newType)) { + return oldType; + } + Class nextType = newType; + while (nextType != Object.class) { + if (nextType.isAssignableFrom(oldType)) { + return nextType; + } + nextType = nextType.getSuperclass(); + } + Class[] interfaces = ClassUtils.getAllInterfacesForClass(newType); + for (Class nextInterface : interfaces) { + if (nextInterface.isAssignableFrom(oldType)) { + return nextInterface; + } + } + return Object.class; + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index fff0d945d79..3d5f9053e24 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -16,7 +16,9 @@ package org.springframework.expression.spel.ast; +import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -28,6 +30,8 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; /** * Represents selection over a map or collection. For example: {1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'} returns @@ -37,6 +41,8 @@ import org.springframework.expression.spel.SpelMessage; * criteria. * * @author Andy Clement + * @author Mark Fisher + * @since 3.0 */ public class Selection extends SpelNodeImpl { @@ -97,9 +103,11 @@ public class Selection extends SpelNodeImpl { return new TypedValue(resultMap,TypeDescriptor.valueOf(Map.class)); } return new TypedValue(result,op.getTypeDescriptor()); - } else if (operand instanceof Collection) { + } else if ((operand instanceof Collection) || ObjectUtils.isArray(operand)) { List data = new ArrayList(); - data.addAll((Collection) operand); + Collection c = (operand instanceof Collection) ? + (Collection) operand : Arrays.asList(ObjectUtils.toObjectArray(operand)); + data.addAll(c); List result = new ArrayList(); int idx = 0; for (Object element : data) { @@ -130,7 +138,15 @@ public class Selection extends SpelNodeImpl { if (variant == LAST) { return new TypedValue(result.get(result.size() - 1),TypeDescriptor.valueOf(op.getTypeDescriptor().getElementType())); } - return new TypedValue(result,op.getTypeDescriptor()); + if (operand instanceof Collection) { + return new TypedValue(result,op.getTypeDescriptor()); + } + else { + Class elementType = ClassUtils.resolvePrimitiveIfNecessary(op.getTypeDescriptor().getElementType()); + Object resultArray = Array.newInstance(elementType, result.size()); + System.arraycopy(result.toArray(), 0, resultArray, 0, result.size()); + return new TypedValue(resultArray, op.getTypeDescriptor()); + } } else { if (operand==null) { if (nullSafe) { diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java new file mode 100644 index 00000000000..9c3ab7a8b6e --- /dev/null +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -0,0 +1,260 @@ +/* + * Copyright 2002-2009 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 + * + * http://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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * @author Mark Fisher + * @since 3.0 + */ +public class SelectionAndProjectionTests { + + @Test + public void selectionWithList() throws Exception { + Expression expression = new SpelExpressionParser().parse("integers.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof List); + List list = (List) value; + assertEquals(5, list.size()); + assertEquals(0, list.get(0)); + assertEquals(1, list.get(1)); + assertEquals(2, list.get(2)); + assertEquals(3, list.get(3)); + assertEquals(4, list.get(4)); + } + + @Test + public void selectFirstItemInList() throws Exception { + Expression expression = new SpelExpressionParser().parse("integers.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof Integer); + assertEquals(0, value); + } + + @Test + public void selectLastItemInList() throws Exception { + Expression expression = new SpelExpressionParser().parse("integers.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof Integer); + assertEquals(4, value); + } + + @Test + public void selectionWithArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("integers.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertTrue(value.getClass().isArray()); + TypedValue typedValue = new TypedValue(value); + assertEquals(Integer.class, typedValue.getTypeDescriptor().getElementType()); + Integer[] array = (Integer[]) value; + assertEquals(5, array.length); + assertEquals(new Integer(0), array[0]); + assertEquals(new Integer(1), array[1]); + assertEquals(new Integer(2), array[2]); + assertEquals(new Integer(3), array[3]); + assertEquals(new Integer(4), array[4]); + } + + @Test + public void selectFirstItemInArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("integers.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof Integer); + assertEquals(0, value); + } + + @Test + public void selectLastItemInArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("integers.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof Integer); + assertEquals(4, value); + } + + @Test + public void selectionWithPrimitiveArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("ints.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertTrue(value.getClass().isArray()); + TypedValue typedValue = new TypedValue(value); + assertEquals(Integer.class, typedValue.getTypeDescriptor().getElementType()); + Integer[] array = (Integer[]) value; + assertEquals(5, array.length); + assertEquals(new Integer(0), array[0]); + assertEquals(new Integer(1), array[1]); + assertEquals(new Integer(2), array[2]); + assertEquals(new Integer(3), array[3]); + assertEquals(new Integer(4), array[4]); + } + + @Test + public void selectFirstItemInPrimitiveArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("ints.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof Integer); + assertEquals(0, value); + } + + @Test + public void selectLastItemInPrimitiveArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("ints.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertTrue(value instanceof Integer); + assertEquals(4, value); + } + + @Test + public void projectionWithList() throws Exception { + Expression expression = new SpelExpressionParser().parse("#testList.![wrapper.value]"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("testList", IntegerTestBean.createList()); + Object value = expression.getValue(context); + assertTrue(value instanceof List); + List list = (List) value; + assertEquals(3, list.size()); + assertEquals(5, list.get(0)); + assertEquals(6, list.get(1)); + assertEquals(7, list.get(2)); + } + + @Test + public void projectionWithArray() throws Exception { + Expression expression = new SpelExpressionParser().parse("#testArray.![wrapper.value]"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("testArray", IntegerTestBean.createArray()); + Object value = expression.getValue(context); + assertTrue(value.getClass().isArray()); + TypedValue typedValue = new TypedValue(value); + assertEquals(Number.class, typedValue.getTypeDescriptor().getElementType()); + Number[] array = (Number[]) value; + assertEquals(3, array.length); + assertEquals(new Integer(5), array[0]); + assertEquals(5.9f, array[1]); + assertEquals(new Integer(7), array[2]); + } + + + static class ListTestBean { + + private final List integers = new ArrayList(); + + ListTestBean() { + for (int i = 0; i < 10; i++) { + integers.add(i); + } + } + + public List getIntegers() { + return integers; + } + } + + + static class ArrayTestBean { + + private final int[] ints = new int[10]; + + private final Integer[] integers = new Integer[10]; + + ArrayTestBean() { + for (int i = 0; i < 10; i++) { + ints[i] = i; + integers[i] = i; + } + } + + public int[] getInts() { + return ints; + } + + public Integer[] getIntegers() { + return integers; + } + } + + + static class IntegerTestBean { + + private final IntegerWrapper wrapper; + + IntegerTestBean(Number value) { + this.wrapper = new IntegerWrapper(value); + } + + public IntegerWrapper getWrapper() { + return this.wrapper; + } + + static List createList() { + List list = new ArrayList(); + for (int i = 0; i < 3; i++) { + list.add(new IntegerTestBean(i + 5)); + } + return list; + } + + static IntegerTestBean[] createArray() { + IntegerTestBean[] array = new IntegerTestBean[3]; + for (int i = 0; i < 3; i++) { + if (i == 1) { + array[i] = new IntegerTestBean(5.9f); + } + else { + array[i] = new IntegerTestBean(i + 5); + } + } + return array; + } + } + + + static class IntegerWrapper { + + private final Number value; + + IntegerWrapper(Number value) { + this.value = value; + } + + public Number getValue() { + return this.value; + } + } + +}