diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java b/org.springframework.expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java index d3dc07d1242..a1cbfccb41a 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java @@ -19,6 +19,7 @@ package org.springframework.expression.common; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; import org.springframework.expression.TypedValue; import org.springframework.util.ClassUtils; @@ -70,4 +71,68 @@ public abstract class ExpressionUtils { throw new EvaluationException("Cannot convert value '" + value + "' to type '" + targetType.getName() + "'"); } + /** + * Attempt to convert a typed value to an int using the supplied type converter. + */ + public static int toInt(TypeConverter typeConverter, TypedValue typedValue) { + return (Integer) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), + TypeDescriptor.valueOf(Integer.class)); + } + + /** + * Attempt to convert a typed value to a boolean using the supplied type converter. + */ + public static boolean toBoolean(TypeConverter typeConverter, TypedValue typedValue) { + return (Boolean) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), + TypeDescriptor.valueOf(Boolean.class)); + } + + /** + * Attempt to convert a typed value to a double using the supplied type converter. + */ + public static double toDouble(TypeConverter typeConverter, TypedValue typedValue) { + return (Double) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), + TypeDescriptor.valueOf(Double.class)); + } + + /** + * Attempt to convert a typed value to a long using the supplied type converter. + */ + public static long toLong(TypeConverter typeConverter, TypedValue typedValue) { + return (Long) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), TypeDescriptor + .valueOf(Long.class)); + } + + /** + * Attempt to convert a typed value to a char using the supplied type converter. + */ + public static char toChar(TypeConverter typeConverter, TypedValue typedValue) { + return (Character) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), + TypeDescriptor.valueOf(Character.class)); + } + + /** + * Attempt to convert a typed value to a short using the supplied type converter. + */ + public static short toShort(TypeConverter typeConverter, TypedValue typedValue) { + return (Short) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), TypeDescriptor + .valueOf(Short.class)); + } + + /** + * Attempt to convert a typed value to a float using the supplied type converter. + */ + public static float toFloat(TypeConverter typeConverter, TypedValue typedValue) { + return (Float) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), TypeDescriptor + .valueOf(Float.class)); + } + + /** + * Attempt to convert a typed value to a byte using the supplied type converter. + */ + public static byte toByte(TypeConverter typeConverter, TypedValue typedValue) { + return (Byte) typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), TypeDescriptor + .valueOf(Byte.class)); + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 0f109d8dbcb..459bed1ea4c 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -95,6 +95,15 @@ public enum SpelMessage { NO_BEAN_RESOLVER_REGISTERED(Kind.ERROR,1057,"No bean resolver registered in the context to resolve access to bean ''{0}''"),// EXCEPTION_DURING_BEAN_RESOLUTION(Kind.ERROR, 1058, "A problem occurred when trying to resolve bean ''{0}'':''{1}''"), // INVALID_BEAN_REFERENCE(Kind.ERROR,1059,"@ can only be followed by an identifier or a quoted name"),// + TYPE_NAME_EXPECTED_FOR_ARRAY_CONSTRUCTION(Kind.ERROR, 1060, + "Expected the type of the new array to be specified as a String but found ''{0}''"), // + INCORRECT_ELEMENT_TYPE_FOR_ARRAY(Kind.ERROR, 1061, + "The array of type ''{0}'' cannot have an element of type ''{1}'' inserted"), // + MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED(Kind.ERROR, 1062, + "Using an initializer to build a multi-dimensional array is not currently supported"), // + MISSING_ARRAY_DIMENSION(Kind.ERROR, 1063, "A required array dimension has not been specified"), // + INITIALIZER_LENGTH_INCORRECT( + Kind.ERROR, 1064, "array initializer size does not match array dimensions"), // ; private Kind kind; diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index 4bff9417230..66711702d81 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -16,20 +16,23 @@ package org.springframework.expression.spel.ast; +import java.lang.reflect.Array; import java.util.List; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; import org.springframework.expression.ConstructorExecutor; import org.springframework.expression.ConstructorResolver; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; import org.springframework.expression.TypedValue; +import org.springframework.expression.common.ExpressionUtils; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelNode; -// TODO asc array constructor call logic has been removed for now -// TODO make this like the method referencing one /** * Represents the invocation of a constructor. Either a constructor on a regular type or construction of an array. When * an array is constructed, an initializer can be specified. @@ -42,7 +45,7 @@ import org.springframework.expression.spel.SpelMessage; * @author Andy Clement * @author Juergen Hoeller * @since 3.0 - */ + */ public class ConstructorReference extends SpelNodeImpl { // TODO is this caching safe - passing the expression around will mean this executor is also being passed around @@ -51,13 +54,26 @@ public class ConstructorReference extends SpelNodeImpl { */ private volatile ConstructorExecutor cachedExecutor; - + private boolean isArrayConstructor = false; + private SpelNodeImpl[] dimensions; + /** - * Create a constructor reference. The first argument is the type, the rest are the parameters to the - * constructor call + * Create a constructor reference. The first argument is the type, the rest are the parameters to the constructor + * call */ public ConstructorReference(int pos, SpelNodeImpl... arguments) { - super(pos,arguments); + super(pos, arguments); + this.isArrayConstructor = false; + } + + /** + * Create a constructor reference. The first argument is the type, the rest are the parameters to the constructor + * call + */ + public ConstructorReference(int pos, SpelNodeImpl[] dimensions, SpelNodeImpl... arguments) { + super(pos, arguments); + this.isArrayConstructor = true; + this.dimensions = dimensions; } /** @@ -65,11 +81,16 @@ public class ConstructorReference extends SpelNodeImpl { */ @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - return createNewInstance(state); + if (isArrayConstructor) { + return createArray(state); + } else { + return createNewInstance(state); + } } /** * Create a new ordinary object and return it. + * * @param state the expression state within which this expression is being evaluated * @return the new object * @throws EvaluationException if there is a problem creating the object @@ -81,40 +102,40 @@ public class ConstructorReference extends SpelNodeImpl { TypedValue childValue = children[i + 1].getValueInternal(state); Object value = childValue.getValue(); arguments[i] = value; - argumentTypes[i] = (value==null?null:value.getClass()); + argumentTypes[i] = (value == null ? null : value.getClass()); } ConstructorExecutor executorToUse = this.cachedExecutor; if (executorToUse != null) { try { return executorToUse.execute(state.getEvaluationContext(), arguments); - } - catch (AccessException ae) { + } catch (AccessException ae) { // Two reasons this can occur: // 1. the method invoked actually threw a real exception // 2. the method invoked was not passed the arguments it expected and has become 'stale' - - // In the first case we should not retry, in the second case we should see if there is a + + // In the first case we should not retry, in the second case we should see if there is a // better suited method. - + // To determine which situation it is, the AccessException will contain a cause - this - // will be the exception thrown by the reflective invocation. Inside this exception there - // may or may not be a root cause. If there is a root cause it is a user created exception. + // will be the exception thrown by the reflective invocation. Inside this exception there + // may or may not be a root cause. If there is a root cause it is a user created exception. // If there is no root cause it was a reflective invocation problem. - + Throwable causeOfAccessException = ae.getCause(); - Throwable rootCause = (causeOfAccessException==null?null:causeOfAccessException.getCause()); - if (rootCause!=null) { + Throwable rootCause = (causeOfAccessException == null ? null : causeOfAccessException.getCause()); + if (rootCause != null) { // User exception was the root cause - exit now if (rootCause instanceof RuntimeException) { - throw (RuntimeException)rootCause; + throw (RuntimeException) rootCause; } else { String typename = (String) children[0].getValueInternal(state).getValue(); - throw new SpelEvaluationException(getStartPosition(), rootCause, SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, - typename,FormatHelper.formatMethodForMessage("", argumentTypes)); + throw new SpelEvaluationException(getStartPosition(), rootCause, + SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typename, FormatHelper + .formatMethodForMessage("", argumentTypes)); } } - + // at this point we know it wasn't a user problem so worth a retry if a better candidate can be found this.cachedExecutor = null; } @@ -128,8 +149,8 @@ public class ConstructorReference extends SpelNodeImpl { TypedValue result = executorToUse.execute(state.getEvaluationContext(), arguments); return result; } catch (AccessException ae) { - throw new SpelEvaluationException(getStartPosition(), ae, SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typename, - FormatHelper.formatMethodForMessage("", argumentTypes)); + throw new SpelEvaluationException(getStartPosition(), ae, SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, + typename, FormatHelper.formatMethodForMessage("", argumentTypes)); } } @@ -137,14 +158,15 @@ public class ConstructorReference extends SpelNodeImpl { /** * Go through the list of registered constructor resolvers and see if any can find a constructor that takes the * specified set of arguments. + * * @param typename the type trying to be constructed * @param argumentTypes the types of the arguments supplied that the constructor must take * @param state the current state of the expression * @return a reusable ConstructorExecutor that can be invoked to run the constructor or null * @throws SpelEvaluationException if there is a problem locating the constructor */ - private ConstructorExecutor findExecutorForConstructor( - String typename, Class[] argumentTypes, ExpressionState state) throws SpelEvaluationException { + private ConstructorExecutor findExecutorForConstructor(String typename, Class[] argumentTypes, + ExpressionState state) throws SpelEvaluationException { EvaluationContext eContext = state.getEvaluationContext(); List cResolvers = eContext.getConstructorResolvers(); @@ -157,13 +179,14 @@ public class ConstructorReference extends SpelNodeImpl { return cEx; } } catch (AccessException ex) { - throw new SpelEvaluationException(getStartPosition(),ex, SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typename, - FormatHelper.formatMethodForMessage("", argumentTypes)); + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typename, FormatHelper.formatMethodForMessage( + "", argumentTypes)); } } } - throw new SpelEvaluationException(getStartPosition(),SpelMessage.CONSTRUCTOR_NOT_FOUND, typename, FormatHelper.formatMethodForMessage("", - argumentTypes)); + throw new SpelEvaluationException(getStartPosition(), SpelMessage.CONSTRUCTOR_NOT_FOUND, typename, FormatHelper + .formatMethodForMessage("", argumentTypes)); } @Override @@ -173,15 +196,201 @@ public class ConstructorReference extends SpelNodeImpl { int index = 0; sb.append(getChild(index++).toStringAST()); - - sb.append("("); - for (int i = index; i < getChildCount(); i++) { - if (i > index) - sb.append(","); - sb.append(getChild(i).toStringAST()); - } - sb.append(")"); + sb.append("("); + for (int i = index; i < getChildCount(); i++) { + if (i > index) + sb.append(","); + sb.append(getChild(i).toStringAST()); + } + sb.append(")"); return sb.toString(); } + /** + * Create an array and return it. + * + * @param state the expression state within which this expression is being evaluated + * @return the new array + * @throws EvaluationException if there is a problem creating the array + */ + private TypedValue createArray(ExpressionState state) throws EvaluationException { + + // First child gives us the array type which will either be a primitive or reference type + Object intendedArrayType = getChild(0).getValue(state); + if (!(intendedArrayType instanceof String)) { + throw new SpelEvaluationException(getChild(0).getStartPosition(), + SpelMessage.TYPE_NAME_EXPECTED_FOR_ARRAY_CONSTRUCTION, FormatHelper + .formatClassNameForMessage(intendedArrayType.getClass())); + } + String type = (String) intendedArrayType; + Class componentType = null; + TypeCode arrayTypeCode = TypeCode.forName(type); + if (arrayTypeCode == TypeCode.OBJECT) { + componentType = state.findType(type); + } else { + componentType = arrayTypeCode.getType(); + } + + TypeDescriptor td = TypeDescriptor.valueOf(componentType); + + Object newArray = null; + + if (!hasInitializer()) { + // Confirm all dimensions were specified (for example [3][][5] is missing the 2nd dimension) + for (int i = 0; i < dimensions.length; i++) { + if (dimensions[i] == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.MISSING_ARRAY_DIMENSION); + } + } + TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter(); + + // Shortcut for 1 dimensional + if (dimensions.length == 1) { + TypedValue o = dimensions[0].getTypedValue(state); + int arraySize = ExpressionUtils.toInt(typeConverter, o); + newArray = Array.newInstance(componentType, arraySize); + } else { + // Multi-dimensional - hold onto your hat! + int[] dims = new int[dimensions.length]; + for (int d = 0; d < dimensions.length; d++) { + TypedValue o = dimensions[d].getTypedValue(state); + dims[d] = ExpressionUtils.toInt(typeConverter, o); + } + newArray = Array.newInstance(componentType, dims); + } + } else { + // There is an initializer + if (dimensions.length > 1) { + // There is an initializer but this is a multi-dimensional array (e.g. new int[][]{{1,2},{3,4}}) - this + // is not currently supported + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); + } + TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter(); + InlineList initializer = (InlineList) getChild(1); + // If a dimension was specified, check it matches the initializer length + if (dimensions[0] != null) { + TypedValue dValue = dimensions[0].getTypedValue(state); + int i = ExpressionUtils.toInt(typeConverter, dValue); + if (i != initializer.getChildCount()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INITIALIZER_LENGTH_INCORRECT); + } + } + // Build the array and populate it + int arraySize = initializer.getChildCount(); + newArray = Array.newInstance(componentType, arraySize); + if (arrayTypeCode == TypeCode.OBJECT) { + populateReferenceTypeArray(state, newArray, typeConverter, initializer, componentType); + } else if (arrayTypeCode == TypeCode.INT) { + populateIntArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.BOOLEAN) { + populateBooleanArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.CHAR) { + populateCharArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.LONG) { + populateLongArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.SHORT) { + populateShortArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.DOUBLE) { + populateDoubleArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.FLOAT) { + populateFloatArray(state, newArray, typeConverter, initializer); + } else if (arrayTypeCode == TypeCode.BYTE) { + populateByteArray(state, newArray, typeConverter, initializer); + } else { + throw new IllegalStateException(arrayTypeCode.name()); + } + } + + return new TypedValue(newArray, td); + } + + private void populateReferenceTypeArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer, Class componentType) { + TypeDescriptor toTypeDescriptor = TypeDescriptor.valueOf(componentType); + Object[] newObjectArray = (Object[]) newArray; + for (int i = 0; i < newObjectArray.length; i++) { + SpelNode elementNode = initializer.getChild(i); + Object arrayEntry = elementNode.getValue(state); + newObjectArray[i] = typeConverter.convertValue(arrayEntry, TypeDescriptor.forObject(arrayEntry), + toTypeDescriptor); + } + } + + private void populateByteArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + byte[] newByteArray = (byte[]) newArray; + for (int i = 0; i < newByteArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newByteArray[i] = ExpressionUtils.toByte(typeConverter, typedValue); + } + } + + private void populateFloatArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + float[] newFloatArray = (float[]) newArray; + for (int i = 0; i < newFloatArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newFloatArray[i] = ExpressionUtils.toFloat(typeConverter, typedValue); + } + } + + private void populateDoubleArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + double[] newDoubleArray = (double[]) newArray; + for (int i = 0; i < newDoubleArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newDoubleArray[i] = ExpressionUtils.toDouble(typeConverter, typedValue); + } + } + + private void populateShortArray(ExpressionState state, Object newArray, + TypeConverter typeConverter, InlineList initializer) { + short[] newShortArray = (short[]) newArray; + for (int i = 0; i < newShortArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newShortArray[i] = ExpressionUtils.toShort(typeConverter, typedValue); + } + } + + private void populateLongArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + long[] newLongArray = (long[]) newArray; + for (int i = 0; i < newLongArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newLongArray[i] = ExpressionUtils.toLong(typeConverter, typedValue); + } + } + + private void populateCharArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + char[] newCharArray = (char[]) newArray; + for (int i = 0; i < newCharArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newCharArray[i] = ExpressionUtils.toChar(typeConverter, typedValue); + } + } + + private void populateBooleanArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + boolean[] newBooleanArray = (boolean[]) newArray; + for (int i = 0; i < newBooleanArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newBooleanArray[i] = ExpressionUtils.toBoolean(typeConverter, typedValue); + } + } + + private void populateIntArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + int[] newIntArray = (int[]) newArray; + for (int i = 0; i < newIntArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newIntArray[i] = ExpressionUtils.toInt(typeConverter, typedValue); + } + } + + private boolean hasInitializer() { + return getChildCount() > 1; + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java new file mode 100644 index 00000000000..dbf4e941c4f --- /dev/null +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2010 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.ast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; + +/** + * Represent a list in an expression, e.g. '{1,2,3}' + * + * @author Andy Clement + * @since 3.0.4 + */ +public class InlineList extends SpelNodeImpl { + + // if the list is purely literals, it is a constant value and can be computed and cached + TypedValue constant = null; // TODO must be immutable list + + public InlineList(int pos, SpelNodeImpl... args) { + super(pos, args); + checkIfConstant(); + } + + /** + * If all the components of the list are constants, or lists that themselves contain constants, then a constant list + * can be built to represent this node. This will speed up later getValue calls and reduce the amount of garbage + * created. + */ + private void checkIfConstant() { + boolean isConstant = true; + for (int c = 0, max = getChildCount(); c < max; c++) { + SpelNode child = getChild(c); + if (!(child instanceof Literal)) { + if (child instanceof InlineList) { + InlineList inlineList = (InlineList) child; + if (!inlineList.isConstant()) { + isConstant = false; + } + } else { + isConstant = false; + } + } + } + if (isConstant) { + List constantList = new ArrayList(); + int childcount = getChildCount(); + for (int c = 0; c < childcount; c++) { + SpelNode child = getChild(c); + if ((child instanceof Literal)) { + constantList.add(((Literal) child).getLiteralValue().getValue()); + } else if (child instanceof InlineList) { + constantList.add(((InlineList) child).getConstantValue()); + } + } + this.constant = new TypedValue(Collections.unmodifiableList(constantList), TypeDescriptor + .valueOf(List.class)); + } + } + + @Override + public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException { + if (constant != null) { + return constant; + } else { + List returnValue = new ArrayList(); + int childcount = getChildCount(); + for (int c = 0; c < childcount; c++) { + returnValue.add(getChild(c).getValue(expressionState)); + } + return new TypedValue(returnValue, TypeDescriptor.valueOf(List.class)); + } + } + + @Override + public String toStringAST() { + StringBuilder s = new StringBuilder(); + // string ast matches input string, not the 'toString()' of the resultant collection, which would use [] + s.append('{'); + int count = getChildCount(); + for (int c = 0; c < count; c++) { + if (c > 0) { + s.append(','); + } + s.append(getChild(c).toStringAST()); + } + s.append('}'); + return s.toString(); + } + + /** + * @return whether this list is a constant value + */ + public boolean isConstant() { + return constant != null; + } + + @SuppressWarnings("unchecked") + private List getConstantValue() { + return (List) constant.getValue(); + } + +} diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java index 39c03272362..124a74752d2 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java @@ -13,9 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.expression.spel.ast; +/** + * Captures primitive types and their corresponding class objects, plus one special entry that represents all reference + * (non-primitive) types. + * + * @author Andy Clement + */ public enum TypeCode { OBJECT(Object.class), BOOLEAN(Boolean.TYPE), BYTE(Byte.TYPE), CHAR(Character.TYPE), // @@ -31,15 +36,26 @@ public enum TypeCode { return type; } -// public static TypeCode forClass(Class c) { -// TypeCode[] allValues = TypeCode.values(); -// for (int i = 0; i < allValues.length; i++) { -// TypeCode typeCode = allValues[i]; -// if (c == typeCode.getType()) { -// return typeCode; -// } -// } -// return OBJECT; -// } + public static TypeCode forName(String name) { + String searchingFor = name.toUpperCase(); + TypeCode[] tcs = values(); + for (int i = 1; i < tcs.length; i++) { + if (tcs[i].name().equals(searchingFor)) { + return tcs[i]; + } + } + return TypeCode.OBJECT; + } + + public static TypeCode forClass(Class c) { + TypeCode[] allValues = TypeCode.values(); + for (int i = 0; i < allValues.length; i++) { + TypeCode typeCode = allValues[i]; + if (c == typeCode.getType()) { + return typeCode; + } + } + return OBJECT; + } } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 8e4b4b40a43..80a567e5148 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -36,6 +36,7 @@ import org.springframework.expression.spel.ast.Elvis; import org.springframework.expression.spel.ast.FunctionReference; import org.springframework.expression.spel.ast.Identifier; import org.springframework.expression.spel.ast.Indexer; +import org.springframework.expression.spel.ast.InlineList; import org.springframework.expression.spel.ast.Literal; import org.springframework.expression.spel.ast.MethodReference; import org.springframework.expression.spel.ast.NullLiteral; @@ -454,6 +455,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { return pop(); } else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) { return pop(); + } else if (maybeEatInlineList()) { + return pop(); } else { return null; } @@ -526,6 +529,29 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { return true; } + // list = LCURLY (element (COMMA element)*) RCURLY + private boolean maybeEatInlineList() { + Token t = peekToken(); + if (!peekToken(TokenKind.LCURLY,true)) { + return false; + } + SpelNodeImpl expr = null; + Token closingCurly = peekToken(); + if (peekToken(TokenKind.RCURLY,true)) { + // empty list '[]' + expr = new InlineList(toPos(t.startpos,closingCurly.endpos)); + } else { + List listElements = new ArrayList(); + do { + listElements.add(eatExpression()); + } while (peekToken(TokenKind.COMMA,true)); + closingCurly = eatToken(TokenKind.RCURLY); + expr = new InlineList(toPos(t.startpos,closingCurly.endpos),listElements.toArray(new SpelNodeImpl[listElements.size()])); + } + constructedNodes.push(expr); + return true; + } + private boolean maybeEatIndexer() { Token t = peekToken(); if (!peekToken(TokenKind.LSQUARE,true)) { @@ -599,14 +625,33 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { SpelNodeImpl possiblyQualifiedConstructorName = eatPossiblyQualifiedId(); List nodes = new ArrayList(); nodes.add(possiblyQualifiedConstructorName); - eatConstructorArgs(nodes); - push(new ConstructorReference(toPos(newToken),nodes.toArray(new SpelNodeImpl[nodes.size()]))); // TODO correct end position? + if (peekToken(TokenKind.LSQUARE)) { + // array initializer + List dimensions = new ArrayList(); + while (peekToken(TokenKind.LSQUARE,true)) { + if (!peekToken(TokenKind.RSQUARE)) { + dimensions.add(eatExpression()); + } else { + dimensions.add(null); + } + eatToken(TokenKind.RSQUARE); + } + if (maybeEatInlineList()) { + nodes.add(pop()); + } + push(new ConstructorReference(toPos(newToken), dimensions.toArray(new SpelNodeImpl[dimensions.size()]), + nodes.toArray(new SpelNodeImpl[nodes.size()]))); + } else { + // regular constructor invocation + eatConstructorArgs(nodes); + // TODO correct end position? + push(new ConstructorReference(toPos(newToken), nodes.toArray(new SpelNodeImpl[nodes.size()]))); + } return true; } return false; } - private void push(SpelNodeImpl newNode) { constructedNodes.push(newNode); } @@ -691,7 +736,6 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { } private Token eatToken(TokenKind expectedKind) { - Assert.isTrue(moreTokens()); Token t = nextToken(); if (t==null) { raiseInternalException( expressionString.length(), SpelMessage.OOD); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java index b7d89a14568..deb311a21dc 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java @@ -24,6 +24,7 @@ enum TokenKind { LITERAL_INT, LITERAL_LONG, LITERAL_HEXINT, LITERAL_HEXLONG, LITERAL_STRING, LITERAL_REAL, LITERAL_REAL_FLOAT, LPAREN("("), RPAREN(")"), COMMA(","), IDENTIFIER, COLON(":"),HASH("#"),RSQUARE("]"), LSQUARE("["), + LCURLY("{"),RCURLY("}"), DOT("."), PLUS("+"), STAR("*"), DIV("/"), NOT("!"), MINUS("-"), SELECT_FIRST("^["), SELECT_LAST("$["), QMARK("?"), PROJECT("!["), GE(">="),GT(">"),LE("<="),LT("<"),EQ("=="),NE("!="),ASSIGN("="), INSTANCEOF("instanceof"), MATCHES("matches"), BETWEEN("between"), SELECT("?["), MOD("%"), POWER("^"), diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java index ac8eafd2f94..a31f16892b6 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java @@ -96,6 +96,12 @@ class Tokenizer { case ']': pushCharToken(TokenKind.RSQUARE); break; + case '{': + pushCharToken(TokenKind.LCURLY); + break; + case '}': + pushCharToken(TokenKind.RCURLY); + break; case '@': pushCharToken(TokenKind.BEAN_REF); break; diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java index 21484f2ca13..9f00ed34ec9 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java @@ -61,8 +61,12 @@ public class StandardTypeComparator implements TypeComparator { } } - if (left instanceof Comparable) { - return ((Comparable) left).compareTo(right); + try { + if (left instanceof Comparable) { + return ((Comparable) left).compareTo(right); + } + } catch (ClassCastException cce) { + throw new SpelEvaluationException(cce, SpelMessage.NOT_COMPARABLE, left.getClass(), right.getClass()); } throw new SpelEvaluationException(SpelMessage.NOT_COMPARABLE, left.getClass(), right.getClass()); diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java new file mode 100644 index 00000000000..d48266f4518 --- /dev/null +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2010 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 org.junit.Assert; +import org.junit.Test; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Test construction of arrays. + * + * @author Andy Clement + */ +public class ArrayConstructorTests extends ExpressionTestCase { + + @Test + public void testSimpleArrayWithInitializer() { + evaluateArrayBuildingExpression("new int[]{1,2,3}", "[1,2,3]"); + evaluateArrayBuildingExpression("new int[]{}", "[]"); + evaluate("new int[]{}.length", "0", Integer.class); + } + + @Test + public void testConversion() { + evaluate("new String[]{1,2,3}[0]", "1", String.class); + evaluate("new int[]{'123'}[0]", 123, Integer.class); + } + + @Test + public void testMultidimensionalArrays() { + evaluateAndCheckError("new int[][]{{1,2},{3,4}}", SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); + evaluateAndCheckError("new int[3][]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new int[]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new int[][1]", SpelMessage.MISSING_ARRAY_DIMENSION); + } + + @Test + public void testPrimitiveTypeArrayConstructors() { + evaluateArrayBuildingExpression("new int[]{1,2,3,4}", "[1,2,3,4]"); + evaluateArrayBuildingExpression("new boolean[]{true,false,true}", "[true,false,true]"); + evaluateArrayBuildingExpression("new char[]{'a','b','c'}", "[a,b,c]"); + evaluateArrayBuildingExpression("new long[]{1,2,3,4,5}", "[1,2,3,4,5]"); + evaluateArrayBuildingExpression("new short[]{2,3,4,5,6}", "[2,3,4,5,6]"); + evaluateArrayBuildingExpression("new double[]{1d,2d,3d,4d}", "[1.0,2.0,3.0,4.0]"); + evaluateArrayBuildingExpression("new float[]{1f,2f,3f,4f}", "[1.0,2.0,3.0,4.0]"); + evaluateArrayBuildingExpression("new byte[]{1,2,3,4}", "[1,2,3,4]"); + } + + @Test + public void testPrimitiveTypeArrayConstructorsElements() { + evaluate("new int[]{1,2,3,4}[0]", 1, Integer.class); + evaluate("new boolean[]{true,false,true}[0]", true, Boolean.class); + evaluate("new char[]{'a','b','c'}[0]", 'a', Character.class); + evaluate("new long[]{1,2,3,4,5}[0]", 1L, Long.class); + evaluate("new short[]{2,3,4,5,6}[0]", (short) 2, Short.class); + evaluate("new double[]{1d,2d,3d,4d}[0]", (double) 1, Double.class); + evaluate("new float[]{1f,2f,3f,4f}[0]", (float) 1, Float.class); + evaluate("new byte[]{1,2,3,4}[0]", (byte) 1, Byte.class); + evaluate("new String(new char[]{'h','e','l','l','o'})", "hello", String.class); + } + + @Test + public void testErrorCases() { + evaluateAndCheckError("new char[7]{'a','c','d','e'}", SpelMessage.INITIALIZER_LENGTH_INCORRECT); + evaluateAndCheckError("new char[3]{'a','c','d','e'}", SpelMessage.INITIALIZER_LENGTH_INCORRECT); + evaluateAndCheckError("new char[2]{'hello','world'}", SpelMessage.TYPE_CONVERSION_ERROR); + evaluateAndCheckError("new String('a','c','d')", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); + } + + @Test + public void testTypeArrayConstructors() { + evaluate("new String[]{'a','b','c','d'}[1]", "b", String.class); + evaluateAndCheckError("new String[]{'a','b','c','d'}.size()", SpelMessage.METHOD_NOT_FOUND, 30, "size()", + "java.lang.String[]"); + evaluate("new String[]{'a','b','c','d'}.length", 4, Integer.class); + } + + @Test + public void testBasicArray() { + evaluate("new String[3]", "java.lang.String[3]{null,null,null}", String[].class); + } + + @Test + public void testMultiDimensionalArray() { + evaluate("new String[2][2]", "[Ljava.lang.String;[2]{[2]{null,null},[2]{null,null}}", String[][].class); + evaluate("new String[3][2][1]", + "[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}", + String[][][].class); + } + + @Test + public void testConstructorInvocation03() { + evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); + } + + public void testConstructorInvocation04() { + evaluateAndCheckError("new Integer[3]{'3','ghi','5'}", SpelMessage.INCORRECT_ELEMENT_TYPE_FOR_ARRAY, 4); + } + + private String evaluateArrayBuildingExpression(String expression, String expectedToString) { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression e = parser.parseExpression(expression); + Object o = e.getValue(); + Assert.assertNotNull(o); + Assert.assertTrue(o.getClass().isArray()); + StringBuilder s = new StringBuilder(); + s.append('['); + if (o instanceof int[]) { + int[] array = (int[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof boolean[]) { + boolean[] array = (boolean[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof char[]) { + char[] array = (char[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof long[]) { + long[] array = (long[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof short[]) { + short[] array = (short[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof double[]) { + double[] array = (double[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof float[]) { + float[] array = (float[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else if (o instanceof byte[]) { + byte[] array = (byte[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } else { + Assert.fail("Not supported " + o.getClass()); + } + s.append(']'); + Assert.assertEquals(expectedToString, s.toString()); + return s.toString(); + } + +} diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestCase.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestCase.java index b413d2c073e..3090c68225f 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestCase.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestCase.java @@ -338,14 +338,17 @@ public abstract class ExpressionTestCase { } } } - + + public static String stringValueOf(Object value) { + return stringValueOf(value, false); + } /** * Produce a nice string representation of the input object. * * @param value object to be formatted * @return a nice string */ - public static String stringValueOf(Object value) { + public static String stringValueOf(Object value, boolean isNested) { // do something nice for arrays if (value == null) { return "null"; @@ -378,9 +381,27 @@ public abstract class ExpressionTestCase { throw new RuntimeException("Please implement support for type " + primitiveType.getName() + " in ExpressionTestCase.stringValueOf()"); } + } else if (value.getClass().getComponentType().isArray()) { + List l = Arrays.asList((Object[]) value); + if (!isNested) { + sb.append(value.getClass().getComponentType().getName()); + } + sb.append("[").append(l.size()).append("]{"); + int i = 0; + for (Object object : l) { + if (i > 0) { + sb.append(","); + } + i++; + sb.append(stringValueOf(object, true)); + } + sb.append("}"); } else { List l = Arrays.asList((Object[]) value); - sb.append(value.getClass().getComponentType().getName()).append("[").append(l.size()).append("]{"); + if (!isNested) { + sb.append(value.getClass().getComponentType().getName()); + } + sb.append("[").append(l.size()).append("]{"); int i = 0; for (Object object : l) { if (i > 0) { diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java index 143482dae6c..5b3359e5dee 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/InProgressTests.java @@ -16,15 +16,17 @@ package org.springframework.expression.spel; import java.util.ArrayList; +import java.util.HashMap; import junit.framework.Assert; import org.junit.Test; -import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.support.StandardEvaluationContext; /** - * These are tests for language features that are not yet considered 'live'. Either missing implementation or documentation. + * These are tests for language features that are not yet considered 'live'. Either missing implementation or + * documentation. * * Where implementation is missing the tests are commented out. * @@ -37,71 +39,73 @@ public class InProgressTests extends ExpressionTestCase { evaluate("1 between listOneFive", "true", Boolean.class); // evaluate("1 between {1, 5}", "true", Boolean.class); // no inline list building at the moment } - + @Test public void testRelOperatorsBetweenErrors01() { - evaluateAndCheckError("1 between T(String)", SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); + evaluateAndCheckError("1 between T(String)", SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); } - + @Test public void testRelOperatorsBetweenErrors03() { - evaluateAndCheckError("1 between listOfNumbersUpToTen", SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); + evaluateAndCheckError("1 between listOfNumbersUpToTen", + SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); } - - // PROJECTION + + // PROJECTION @Test public void testProjection01() { - evaluate("listOfNumbersUpToTen.![#this<5?'y':'n']","[y, y, y, y, n, n, n, n, n, n]",ArrayList.class); + evaluate("listOfNumbersUpToTen.![#this<5?'y':'n']", "[y, y, y, y, n, n, n, n, n, n]", ArrayList.class); // inline list creation not supported at the moment - // evaluate("{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}", "[n, y, n, y, n, y, n, y, n, y]", ArrayList.class); + // evaluate("{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}", "[n, y, n, y, n, y, n, y, n, y]", ArrayList.class); } - + @Test public void testProjection02() { // inline map creation not supported at the moment // evaluate("#{'a':'y','b':'n','c':'y'}.![value=='y'?key:null].nonnull().sort()", "[a, c]", ArrayList.class); - evaluate("mapOfNumbersUpToTen.![key>5?value:null]", "[null, null, null, null, null, six, seven, eight, nine, ten]", ArrayList.class); + evaluate("mapOfNumbersUpToTen.![key>5?value:null]", + "[null, null, null, null, null, six, seven, eight, nine, ten]", ArrayList.class); } - + @Test public void testProjection05() { evaluateAndCheckError("'abc'.![true]", SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE); evaluateAndCheckError("null.![true]", SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE); evaluate("null?.![true]", null, null); } - + @Test public void testProjection06() throws Exception { - SpelExpression expr = (SpelExpression)parser.parseExpression("'abc'.![true]"); - Assert.assertEquals("'abc'.![true]",expr.toStringAST()); + SpelExpression expr = (SpelExpression) parser.parseExpression("'abc'.![true]"); + Assert.assertEquals("'abc'.![true]", expr.toStringAST()); Assert.assertFalse(expr.isWritable(new StandardEvaluationContext())); } // SELECTION - + @Test public void testSelection02() { - evaluate("testMap.keySet().?[#this matches '.*o.*']", "[monday]", ArrayList.class); - evaluate("testMap.keySet().?[#this matches '.*r.*'].contains('saturday')", "true", Boolean.class); - evaluate("testMap.keySet().?[#this matches '.*r.*'].size()", "3", Integer.class); + evaluate("testMap.keySet().?[#this matches '.*o.*']", "[monday]", ArrayList.class); + evaluate("testMap.keySet().?[#this matches '.*r.*'].contains('saturday')", "true", Boolean.class); + evaluate("testMap.keySet().?[#this matches '.*r.*'].size()", "3", Integer.class); } - + @Test - public void testSelectionError_NonBooleanSelectionCriteria() { - evaluateAndCheckError("listOfNumbersUpToTen.?['nonboolean']", - SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); - } - + public void testSelectionError_NonBooleanSelectionCriteria() { + evaluateAndCheckError("listOfNumbersUpToTen.?['nonboolean']", + SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); + } + @Test public void testSelection03() { evaluate("mapOfNumbersUpToTen.?[key>5].size()", "5", Integer.class); -// evaluate("listOfNumbersUpToTen.?{#this>5}", "5", ArrayList.class); + // evaluate("listOfNumbersUpToTen.?{#this>5}", "5", ArrayList.class); } - @Test public void testSelection04() { - evaluateAndCheckError("mapOfNumbersUpToTen.?['hello'].size()",SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); + evaluateAndCheckError("mapOfNumbersUpToTen.?['hello'].size()", + SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); } @Test @@ -131,23 +135,25 @@ public class InProgressTests extends ExpressionTestCase { @Test public void testSelectionLast02() { + evaluate("mapOfNumbersUpToTen.$[key>5]", "{10=ten}", HashMap.class); evaluate("mapOfNumbersUpToTen.$[key>5].size()", "1", Integer.class); } @Test public void testSelectionAST() throws Exception { - SpelExpression expr = (SpelExpression)parser.parseExpression("'abc'.^[true]"); - Assert.assertEquals("'abc'.^[true]",expr.toStringAST()); + SpelExpression expr = (SpelExpression) parser.parseExpression("'abc'.^[true]"); + Assert.assertEquals("'abc'.^[true]", expr.toStringAST()); Assert.assertFalse(expr.isWritable(new StandardEvaluationContext())); - expr = (SpelExpression)parser.parseExpression("'abc'.?[true]"); - Assert.assertEquals("'abc'.?[true]",expr.toStringAST()); + expr = (SpelExpression) parser.parseExpression("'abc'.?[true]"); + Assert.assertEquals("'abc'.?[true]", expr.toStringAST()); Assert.assertFalse(expr.isWritable(new StandardEvaluationContext())); - expr = (SpelExpression)parser.parseExpression("'abc'.$[true]"); - Assert.assertEquals("'abc'.$[true]",expr.toStringAST()); + expr = (SpelExpression) parser.parseExpression("'abc'.$[true]"); + Assert.assertEquals("'abc'.$[true]", expr.toStringAST()); Assert.assertFalse(expr.isWritable(new StandardEvaluationContext())); } + // Constructor invocation - + // public void testPrimitiveTypeArrayConstructors() { // evaluate("new int[]{1,2,3,4}.count()", 4, Integer.class); // evaluate("new boolean[]{true,false,true}.count()", 3, Integer.class); @@ -254,52 +260,41 @@ public class InProgressTests extends ExpressionTestCase { // evaluate("(#answer=42;#answer)", "42", Integer.class, true); // evaluate("($answer=42;$answer)", "42", Integer.class, true); // } - // + +// // inline map creation + // @Test + // public void testInlineMapCreation01() { + // evaluate("#{'key1':'Value 1', 'today':'Monday'}", "{key1=Value 1, today=Monday}", HashMap.class); + // } // - // public void testRelOperatorsIs02() { - // evaluate("{1, 2, 3, 4, 5} instanceof T(List)", "true", Boolean.class); - // } + // @Test + // public void testInlineMapCreation02() { + // // "{2=February, 1=January, 3=March}", HashMap.class); + // evaluate("#{1:'January', 2:'February', 3:'March'}.size()", 3, Integer.class); + // } // - // public void testRelOperatorsIs03() { - // evaluate("{1, 2, 3, 4, 5} instanceof T(List)", "true", Boolean.class); - // } + // @Test + // public void testInlineMapCreation03() { + // evaluate("#{'key1':'Value 1', 'today':'Monday'}['key1']", "Value 1", String.class); +// } +// +// @Test +// public void testInlineMapCreation04() { +// evaluate("#{1:'January', 2:'February', 3:'March'}[3]", "March", String.class); +// } +// +// @Test +// public void testInlineMapCreation05() { +// evaluate("#{1:'January', 2:'February', 3:'March'}.get(2)", "February", String.class); +// } + + // set construction + @Test + public void testSetConstruction01() { + evaluate("new java.util.HashSet().addAll({'a','b','c'})", "true", Boolean.class); + } + // - // inline list creation - // public void testInlineListCreation01() { - // evaluate("{1, 2, 3, 4, 5}", "[1, 2, 3, 4, 5]", ArrayList.class); - // } - // - // public void testInlineListCreation02() { - // evaluate("{'abc', 'xyz'}", "[abc, xyz]", ArrayList.class); - // } - // - // // inline map creation - // public void testInlineMapCreation01() { - // evaluate("#{'key1':'Value 1', 'today':'Monday'}", "{key1=Value 1, today=Monday}", HashMap.class); - // } - // - // public void testInlineMapCreation02() { - // evaluate("#{1:'January', 2:'February', 3:'March'}.size()", 3, Integer.class);// "{2=February, 1=January, - // // 3=March}", HashMap.class); - // } - // - // public void testInlineMapCreation03() { - // evaluate("#{'key1':'Value 1', 'today':'Monday'}['key1']", "Value 1", String.class); - // } - // - // public void testInlineMapCreation04() { - // evaluate("#{1:'January', 2:'February', 3:'March'}[3]", "March", String.class); - // } - // - // public void testInlineMapCreation05() { - // evaluate("#{1:'January', 2:'February', 3:'March'}.get(2)", "February", String.class); - // } - // - // // set construction - // public void testSetConstruction01() { - // evaluate("new HashSet().addAll({'a','b','c'})", "true", Boolean.class); - // } - // // public void testConstructorInvocation02() { // evaluate("new String[3]", "java.lang.String[3]{null,null,null}", String[].class); // } @@ -311,7 +306,8 @@ public class InProgressTests extends ExpressionTestCase { // public void testConstructorInvocation04() { // evaluateAndCheckError("new String[3]{'abc',3,'def'}", SpelMessages.INCORRECT_ELEMENT_TYPE_FOR_ARRAY, 4); // } - // // array construction + // array construction + // @Test // public void testArrayConstruction01() { // evaluate("new int[] {1, 2, 3, 4, 5}", "int[5]{1,2,3,4,5}", int[].class); // } @@ -418,13 +414,13 @@ public class InProgressTests extends ExpressionTestCase { // public void testSelectionUsingIndex() { // evaluate("{1,2,3,4,5,6,7,8,9,10}.?{$index > 5 }", "[7, 8, 9, 10]", ArrayList.class); // } - //public void testSelection01() { - // inline list creation not supported: - // evaluate("{1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'}", "[2, 4, 6, 8, 10]", ArrayList.class); - //} + // public void testSelection01() { + // inline list creation not supported: + // evaluate("{1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'}", "[2, 4, 6, 8, 10]", ArrayList.class); + // } // - // public void testSelectionUsingIndex() { - // evaluate("listOfNumbersUpToTen.?[#index > 5 ]", "[7, 8, 9, 10]", ArrayList.class); - //} + // public void testSelectionUsingIndex() { + // evaluate("listOfNumbersUpToTen.?[#index > 5 ]", "[7, 8, 9, 10]", ArrayList.class); + // } } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ListTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ListTests.java new file mode 100644 index 00000000000..6fd8925ae01 --- /dev/null +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ListTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2010 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 java.util.ArrayList; +import java.util.Collections; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.expression.spel.ast.InlineList; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Test usage of inline lists. + * + * @author Andy Clement + * @since 3.0.4 + */ +public class ListTests extends ExpressionTestCase { + + // if the list is full of literals then it will be of the type unmodifiableClass rather than ArrayList + Class unmodifiableClass = Collections.unmodifiableList(new ArrayList()).getClass(); + + @Test + public void testInlineListCreation01() { + evaluate("{1, 2, 3, 4, 5}", "[1, 2, 3, 4, 5]", unmodifiableClass); + } + + @Test + public void testInlineListCreation02() { + evaluate("{'abc', 'xyz'}", "[abc, xyz]", unmodifiableClass); + } + + @Test + public void testInlineListCreation03() { + evaluate("{}", "[]", unmodifiableClass); + } + + @Test + public void testInlineListCreation04() { + evaluate("{'abc'=='xyz'}", "[false]", ArrayList.class); + } + + @Test + public void testInlineListAndNesting() { + evaluate("{{1,2,3},{4,5,6}}", "[[1, 2, 3], [4, 5, 6]]", unmodifiableClass); + evaluate("{{1,'2',3},{4,{'a','b'},5,6}}", "[[1, 2, 3], [4, [a, b], 5, 6]]", unmodifiableClass); + } + + @Test + public void testInlineListError() { + parseAndCheckError("{'abc'", SpelMessage.OOD); + } + + @Test + public void testRelOperatorsIs02() { + evaluate("{1, 2, 3, 4, 5} instanceof T(java.util.List)", "true", Boolean.class); + } + + @Test + public void testInlineListCreation05() { + evaluate("3 between {1,5}", "true", Boolean.class); + } + + @Test + public void testInlineListCreation06() { + evaluate("8 between {1,5}", "false", Boolean.class); + } + + @Test + public void testInlineListAndProjectionSelection() { + evaluate("{1,2,3,4,5,6}.![#this>3]", "[false, false, false, true, true, true]", ArrayList.class); + evaluate("{1,2,3,4,5,6}.?[#this>3]", "[4, 5, 6]", ArrayList.class); + evaluate("{1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this) == 'y']", "[2, 4, 6, 8, 10]", ArrayList.class); + } + + @Test + public void testSetConstruction01() { + evaluate("new java.util.HashSet().addAll({'a','b','c'})", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween01() { + evaluate("32 between {32, 42}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween02() { + evaluate("'efg' between {'abc', 'xyz'}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween03() { + evaluate("42 between {32, 42}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetweenErrors02() { + evaluateAndCheckError("'abc' between {5,7}", SpelMessage.NOT_COMPARABLE, 6); + } + + @Test + public void testConstantRepresentation1() { + checkConstantList("{1,2,3,4,5}", true); + checkConstantList("{'abc'}", true); + checkConstantList("{}", true); + checkConstantList("{#a,2,3}", false); + checkConstantList("{1,2,Integer.valueOf(4)}", false); + checkConstantList("{1,2,{#a}}", false); + } + + private void checkConstantList(String expressionText, boolean expectedToBeConstant) { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expression = (SpelExpression) parser.parseExpression(expressionText); + SpelNode node = expression.getAST(); + Assert.assertTrue(node instanceof InlineList); + InlineList inlineList = (InlineList) node; + if (expectedToBeConstant) { + Assert.assertTrue(inlineList.isConstant()); + } else { + Assert.assertFalse(inlineList.isConstant()); + } + } + + @Test + public void testInlineListWriting() { + // list should be unmodifiable + try { + evaluate("{1, 2, 3, 4, 5}[0]=6", "[1, 2, 3, 4, 5]", unmodifiableClass); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + // success + } + } +} diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 110d55b1ea3..5e714ac6611 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -432,7 +432,12 @@ public class ParsingTests { public void testTypeReferences02() { parseCheck("T(String)"); } - + + @Test + public void testInlineList1() { + parseCheck("{1,2,3,4}"); + } + /** * Parse the supplied expression and then create a string representation of the resultant AST, it should be the same * as the original expression. diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java index 01bf8c08c2b..00ddff35f40 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -17,13 +17,12 @@ package org.springframework.expression.spel.standard; import junit.framework.Assert; -import org.junit.Test; +import org.junit.Test; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.ExpressionException; import org.springframework.expression.ParseException; -import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.SpelParseException; @@ -111,16 +110,6 @@ public class SpelParserTests { @Test public void generalExpressions() throws Exception { - try { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("new String[3]"); - Assert.fail(); - } catch (ParseException e) { - Assert.assertTrue(e instanceof SpelParseException); - SpelParseException spe = (SpelParseException)e; - Assert.assertEquals(SpelMessage.MISSING_CONSTRUCTOR_ARGS,spe.getMessageCode()); - Assert.assertEquals(10,spe.getPosition()); - } try { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("new String");