new feature for binder - automatically create lists and entries in lists upon referencing nulls

git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1300 50f2f4bb-b051-0410-bef5-90022cba6387
This commit is contained in:
Andy Clement 2009-06-02 16:42:43 +00:00
parent e9288fdb4e
commit 1e2cecfd76
11 changed files with 212 additions and 20 deletions

View File

@ -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<T> {
public Binder(T model) {
this.model = model;
bindings = new HashMap<String, Binding>();
expressionParser = new SpelExpressionParser();
int parserConfig =
SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull |
SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize;
expressionParser = new SpelExpressionParser(parserConfig);
typeConverter = new DefaultTypeConverter();
}

View File

@ -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<TestBean> binder = new Binder<TestBean>(new TestBean());
TestBean testbean = new TestBean();
Binder<TestBean> binder = new Binder<TestBean>(testbean);
Map<String, String> propertyValues = new HashMap<String, String>();
// 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

View File

@ -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<VariableScope> variableScopes = new Stack<VariableScope>();
private final Stack<TypedValue> contextObjects = new Stack<TypedValue>();
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;
}
}

View File

@ -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> T getValue(EvaluationContext context, Class<T> 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> T getValue(Class<T> 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);
}

View File

@ -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;

View File

@ -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) {

View File

@ -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<PropertyAccessor> resolversToTry = getPropertyAccessorsToTry(getObjectClass(contextObject),state);

View File

@ -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<max;i++) {
if (peers[i]==this) {
return (i+1)<max && peers[i+1].getClass().equals(clazz);
}
}
}
return false;
}
public final Object getValue(ExpressionState expressionState) throws EvaluationException {
if (expressionState != null) {
return getValueInternal(expressionState).getValue();
} else {
// configuration not set - does that matter?
return getValue(new ExpressionState(new StandardEvaluationContext()));
}
}

View File

@ -86,8 +86,20 @@ public class SpelExpressionParser extends TemplateAwareExpressionParser {
// For rules that build nodes, they are stacked here for return
private Stack<SpelNodeImpl> constructedNodes = new Stack<SpelNodeImpl>();
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();
}

View File

@ -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;
}

View File

@ -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<String> list;
public List list2;
private List<String> foo;
public List<String> getFoo() { return this.foo; }
public void setFoo(List<String> newfoo) { this.foo = newfoo; }
}
@Test
public void testElvis01() {
evaluate("'Andy'?:'Dave'","Andy",String.class);