diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java index 0fd1e89f0fc..cc4a9920017 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java @@ -19,6 +19,7 @@ import org.springframework.expression.Expression; import org.springframework.expression.ExpressionException; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.ui.format.Formatter; @@ -68,7 +69,10 @@ public class Binder { public Binder(T model) { this.model = model; bindings = new HashMap(); - expressionParser = new SpelExpressionParser(); + int parserConfig = + SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull | + SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize; + expressionParser = new SpelExpressionParser(parserConfig); typeConverter = new DefaultTypeConverter(); } diff --git a/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java index 24fca477f34..bc77156f18a 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import junit.framework.Assert; + import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -146,21 +148,39 @@ public class BinderTests { } @Test - @Ignore public void bindHandleNullValueInNestedPath() { - Binder binder = new Binder(new TestBean()); + TestBean testbean = new TestBean(); + Binder binder = new Binder(testbean); Map propertyValues = new HashMap(); - // TODO should auto add(new Address) at 0 + + // EL configured with some options from SpelExpressionParserConfiguration: + // (see where Binder creates the parser) + // - new addresses List is created if null + // - new entries automatically built if List is currently too short - all new entries + // are new instances of the type of the list entry, they are not null. + // not currently doing anything for maps or arrays + propertyValues.put("addresses[0].street", "4655 Macy Lane"); propertyValues.put("addresses[0].city", "Melbourne"); propertyValues.put("addresses[0].state", "FL"); propertyValues.put("addresses[0].state", "35452"); - // TODO should auto add(new Address) at 1 + + // Auto adds new Address at 1 propertyValues.put("addresses[1].street", "1234 Rostock Circle"); propertyValues.put("addresses[1].city", "Palm Bay"); propertyValues.put("addresses[1].state", "FL"); propertyValues.put("addresses[1].state", "32901"); + + // Auto adds new Address at 5 (plus intermediates 2,3,4) + propertyValues.put("addresses[5].street", "1234 Rostock Circle"); + propertyValues.put("addresses[5].city", "Palm Bay"); + propertyValues.put("addresses[5].state", "FL"); + propertyValues.put("addresses[5].state", "32901"); + binder.bind(propertyValues); + Assert.assertEquals(6,testbean.addresses.size()); + Assert.assertEquals("Palm Bay",testbean.addresses.get(1).city); + Assert.assertNotNull(testbean.addresses.get(2)); } @Test diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index e35f0437b47..3efd2ad32b1 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -28,6 +28,7 @@ import org.springframework.expression.OperatorOverloader; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypeComparator; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration; /** * An ExpressionState is for maintaining per-expression-evaluation state, any changes to it are not seen by other @@ -47,12 +48,20 @@ public class ExpressionState { private final Stack variableScopes = new Stack(); private final Stack contextObjects = new Stack(); + + private int configuration = 0; public ExpressionState(EvaluationContext context) { this.relatedContext = context; createVariableScope(); } + public ExpressionState(EvaluationContext context, int configuration) { + this.relatedContext = context; + this.configuration = configuration; + createVariableScope(); + } + // create an empty top level VariableScope private void createVariableScope() { this.variableScopes.add(new VariableScope()); @@ -204,4 +213,12 @@ public class ExpressionState { } } + public boolean configuredToGrowCollection() { + return (configuration & SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize)!=0; + } + + public boolean configuredToCreateCollection() { + return (configuration & SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull)!=0; + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java index f4af8165504..6f7e49f31dc 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelExpression.java @@ -37,13 +37,15 @@ public class SpelExpression implements Expression { public final SpelNodeImpl ast; + public final int configuration; /** * Construct an expression, only used by the parser. */ - public SpelExpression(String expression, SpelNodeImpl ast) { + public SpelExpression(String expression, SpelNodeImpl ast, int configuration) { this.expression = expression; this.ast = ast; + this.configuration = configuration; } /** @@ -57,7 +59,7 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public Object getValue() throws EvaluationException { - ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext(),configuration); return this.ast.getValue(expressionState); } @@ -65,7 +67,7 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public Object getValue(EvaluationContext context) throws EvaluationException { - return this.ast.getValue(new ExpressionState(context)); + return this.ast.getValue(new ExpressionState(context,configuration)); } /** @@ -73,7 +75,7 @@ public class SpelExpression implements Expression { */ @SuppressWarnings("unchecked") public T getValue(EvaluationContext context, Class expectedResultType) throws EvaluationException { - Object result = ast.getValue(new ExpressionState(context)); + Object result = ast.getValue(new ExpressionState(context,configuration)); if (result != null && expectedResultType != null) { Class resultType = result.getClass(); @@ -89,14 +91,14 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public void setValue(EvaluationContext context, Object value) throws EvaluationException { - this.ast.setValue(new ExpressionState(context), value); + this.ast.setValue(new ExpressionState(context,configuration), value); } /** * {@inheritDoc} */ public boolean isWritable(EvaluationContext context) throws EvaluationException { - return this.ast.isWritable(new ExpressionState(context)); + return this.ast.isWritable(new ExpressionState(context,configuration)); } /** @@ -121,7 +123,7 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public Class getValueType(EvaluationContext context) throws EvaluationException { - ExpressionState eState = new ExpressionState(context); + ExpressionState eState = new ExpressionState(context,configuration); TypeDescriptor typeDescriptor = this.ast.getValueInternal(eState).getTypeDescriptor(); return typeDescriptor.getType(); } @@ -130,7 +132,7 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { - ExpressionState eState = new ExpressionState(context); + ExpressionState eState = new ExpressionState(context,configuration); TypeDescriptor typeDescriptor = this.ast.getValueInternal(eState).getTypeDescriptor(); return typeDescriptor; } @@ -139,14 +141,14 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public Class getValueType() throws EvaluationException { - return this.ast.getValueInternal(new ExpressionState(new StandardEvaluationContext())).getTypeDescriptor().getType(); + return this.ast.getValueInternal(new ExpressionState(new StandardEvaluationContext(),configuration)).getTypeDescriptor().getType(); } /** * {@inheritDoc} */ public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { - return this.ast.getValueInternal(new ExpressionState(new StandardEvaluationContext())).getTypeDescriptor(); + return this.ast.getValueInternal(new ExpressionState(new StandardEvaluationContext(),configuration)).getTypeDescriptor(); } @@ -154,7 +156,7 @@ public class SpelExpression implements Expression { * {@inheritDoc} */ public T getValue(Class expectedResultType) throws EvaluationException { - ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext(),configuration); Object result = this.ast.getValue(expressionState); return ExpressionUtils.convert(expressionState.getEvaluationContext(), result, expectedResultType); } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessages.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessages.java index e0d030c41ac..0c7dd4ba1dc 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessages.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessages.java @@ -88,6 +88,9 @@ public enum SpelMessages { UNEXPECTED_DATA_AFTER_DOT(Kind.ERROR,1049,"Unexpected data after ''.'': ''{0}''"),// MISSING_CONSTRUCTOR_ARGS(Kind.ERROR,1050,"The arguments '(...)' for the constructor call are missing"),// RUN_OUT_OF_ARGUMENTS(Kind.ERROR,1051,"Unexpected ran out of arguments"),// + UNABLE_TO_GROW_COLLECTION(Kind.ERROR,1052,"Unable to grow collection"),// + UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE(Kind.ERROR,1053,"Unable to grow collection: unable to determine list element type"),// + UNABLE_TO_CREATE_LIST_FOR_INDEXING(Kind.ERROR,1054,"Unable to create a List for the following indexer"),// ; private Kind kind; diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 73c6f2e9914..83a02ad3240 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -67,9 +67,32 @@ public class Indexer extends SpelNodeImpl { if (targetObject.getClass().isArray()) { return new TypedValue(accessArrayElement(targetObject, idx),TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); } else if (targetObject instanceof Collection) { - Collection c = (Collection) targetObject; + Collection c = (Collection) targetObject; if (idx >= c.size()) { - throw new SpelEvaluationException(getStartPosition(),SpelMessages.COLLECTION_INDEX_OUT_OF_BOUNDS, c.size(), idx); + if (state.configuredToGrowCollection()) { + // Grow the collection + Object newCollectionElement = null; + try { + int newElements = idx-c.size(); + Class elementClass = targetObjectTypeDescriptor.getElementType(); + if (elementClass == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessages.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE); + } + while (newElements>0) { + c.add(elementClass.newInstance()); + newElements--; + } + newCollectionElement = targetObjectTypeDescriptor.getElementType().newInstance(); + } catch (InstantiationException e) { + throw new SpelEvaluationException(getStartPosition(), e, SpelMessages.UNABLE_TO_GROW_COLLECTION); + } catch (IllegalAccessException e) { + throw new SpelEvaluationException(getStartPosition(), e, SpelMessages.UNABLE_TO_GROW_COLLECTION); + } + c.add(newCollectionElement); + return new TypedValue(newCollectionElement,TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); + } else { + throw new SpelEvaluationException(getStartPosition(),SpelMessages.COLLECTION_INDEX_OUT_OF_BOUNDS, c.size(), idx); + } } int pos = 0; for (Object o : c) { diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index 4ee7970aa64..9510264b1b0 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -19,6 +19,7 @@ package org.springframework.expression.spel.ast; import java.util.ArrayList; import java.util.List; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; @@ -52,7 +53,22 @@ public class PropertyOrFieldReference extends SpelNodeImpl { @Override public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException { - return readProperty(state, this.name); + TypedValue result = readProperty(state, this.name); + if (result.getValue()==null && state.configuredToCreateCollection() && result.getTypeDescriptor().getType().equals(List.class) && nextChildIs(Indexer.class)) { + // Create a new list ready for the indexer + try { + if (isWritable(state)) { + List newList = ArrayList.class.newInstance(); + writeProperty(state, name, newList); + result = readProperty(state, this.name); + } + } catch (InstantiationException e) { + throw new SpelEvaluationException(getStartPosition(), e, SpelMessages.UNABLE_TO_CREATE_LIST_FOR_INDEXING); + } catch (IllegalAccessException e) { + throw new SpelEvaluationException(getStartPosition(), e, SpelMessages.UNABLE_TO_CREATE_LIST_FOR_INDEXING); + } + } + return result; } @Override @@ -172,6 +188,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl { public boolean isWritableProperty(String name, ExpressionState state) throws SpelEvaluationException { Object contextObject = state.getActiveContextObject().getValue(); + TypeDescriptor td = state.getActiveContextObject().getTypeDescriptor(); EvaluationContext eContext = state.getEvaluationContext(); List resolversToTry = getPropertyAccessorsToTry(getObjectClass(contextObject),state); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index d32412af5f7..cb4322c7459 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -37,6 +37,7 @@ public abstract class SpelNodeImpl implements SpelNode, CommonTypeDescriptors { protected int pos; // start = top 16bits, end = bottom 16bits protected SpelNodeImpl[] children = SpelNodeImpl.NO_CHILDREN; + private SpelNodeImpl parent; public SpelNodeImpl(int pos, SpelNodeImpl... operands) { this.pos = pos; @@ -44,13 +45,40 @@ public abstract class SpelNodeImpl implements SpelNode, CommonTypeDescriptors { assert pos!=0; if (operands!=null && operands.length>0) { this.children = operands; + for (SpelNodeImpl childnode: operands) { + childnode.parent = this; + } } } + + protected SpelNodeImpl getPreviousChild() { + SpelNodeImpl result = null; + if (parent!=null) { + for (SpelNodeImpl child: parent.children) { + if (this==child) break; + result = child; + } + } + return result; + } + + protected boolean nextChildIs(Class clazz) { + if (parent!=null) { + SpelNodeImpl[] peers = parent.children; + for (int i=0,max=peers.length;i constructedNodes = new Stack(); + + private int configuration; public SpelExpressionParser() { + this(0); + } + + /** + * Create a parser with some configured behaviour. Supported configuration + * bit flags can be seen in @see {@link SpelExpressionParserConfiguration} + * @param configuration bitflags for configuration options + */ + public SpelExpressionParser(int configuration) { + this.configuration = configuration; } public SpelExpression parse(String expressionString) throws ParseException { @@ -108,7 +120,7 @@ public class SpelExpressionParser extends TemplateAwareExpressionParser { throw new SpelParseException(peekToken().startpos,SpelMessages.MORE_INPUT,toString(nextToken())); } assert constructedNodes.isEmpty(); - return new SpelExpression(expressionString,ast); + return new SpelExpression(expressionString, ast, configuration); } catch (InternalParseException ipe) { throw ipe.getCause(); } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParserConfiguration.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParserConfiguration.java new file mode 100644 index 00000000000..a927e94ac62 --- /dev/null +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParserConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2008-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.standard; + +/** + * Bit flags that configure optional behaviour in the parser. Pass the necessary + * bits when calling the expression parser constructor. + * + * @author Andy Clement + * @since 3.0 + */ +public interface SpelExpressionParserConfiguration { + + static final int CreateListsOnAttemptToIndexIntoNull = 0x0001; + static final int GrowListsOnIndexBeyondSize = 0x0002; + +} diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index bb1b60ed563..7de16717fe3 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -16,14 +16,18 @@ package org.springframework.expression.spel; +import java.util.List; + import junit.framework.Assert; import org.junit.Test; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeLocator; @@ -34,6 +38,38 @@ import org.springframework.expression.spel.support.StandardTypeLocator; */ public class EvaluationTests extends ExpressionTestCase { + @Test + public void testCreateListsOnAttemptToIndexNull01() throws EvaluationException, ParseException { + ExpressionParser parser = new SpelExpressionParser(SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize); + Expression expression = parser.parseExpression("list[0]"); + TestClass testClass = new TestClass(); + Object o = null; + o = expression.getValue(new StandardEvaluationContext(testClass)); + Assert.assertEquals("",o); + o = parser.parseExpression("list[3]").getValue(new StandardEvaluationContext(testClass)); + Assert.assertEquals("",o); + Assert.assertEquals(4, testClass.list.size()); + try { + o = parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass)); + Assert.fail(); + } catch (EvaluationException ee) { + // success! + } + o = parser.parseExpression("foo[3]").getValue(new StandardEvaluationContext(testClass)); + Assert.assertEquals("",o); + Assert.assertEquals(4, testClass.getFoo().size()); + } + + static class TestClass { + public List list; + public List list2; + + private List foo; + public List getFoo() { return this.foo; } + public void setFoo(List newfoo) { this.foo = newfoo; } + } + + @Test public void testElvis01() { evaluate("'Andy'?:'Dave'","Andy",String.class);