From 81b10be1d06b6cf96b227515030f822325ab3207 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Thu, 11 Mar 2010 01:09:50 +0000 Subject: [PATCH] SPR-6968: indexing via square brackets can now treat the index as an attempt at property access --- .../expression/spel/ast/AstUtils.java | 72 ++++++++ .../expression/spel/ast/Indexer.java | 172 +++++++++++++----- .../spel/ast/PropertyOrFieldReference.java | 1 + .../expression/spel/SpringEL300Tests.java | 119 ++++++++++++ 4 files changed, 322 insertions(+), 42 deletions(-) create mode 100644 org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java new file mode 100644 index 0000000000..ff998fef13 --- /dev/null +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 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.List; + +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.spel.ExpressionState; + +/** + * Utilities methods for use in the Ast classes. + * + * @author Andy Clement + * @since 3.0.2 + */ +public class AstUtils { + + /** + * Determines the set of property resolvers that should be used to try and access a property on the specified target + * type. The resolvers are considered to be in an ordered list, however in the returned list any that are exact + * matches for the input target type (as opposed to 'general' resolvers that could work for any type) are placed at + * the start of the list. In addition, there are specific resolvers that exactly name the class in question and + * resolvers that name a specific class but it is a supertype of the class we have. These are put at the end of the + * specific resolvers set and will be tried after exactly matching accessors but before generic accessors. + * + * @param targetType the type upon which property access is being attempted + * @return a list of resolvers that should be tried in order to access the property + */ + public static List getPropertyAccessorsToTry(Class targetType, ExpressionState state) { + List specificAccessors = new ArrayList(); + List generalAccessors = new ArrayList(); + for (PropertyAccessor resolver : state.getPropertyAccessors()) { + Class[] targets = resolver.getSpecificTargetClasses(); + if (targets == null) { // generic resolver that says it can be used for any type + generalAccessors.add(resolver); + } + else { + if (targetType != null) { + int pos = 0; + for (Class clazz : targets) { + if (clazz == targetType) { // put exact matches on the front to be tried first? + specificAccessors.add(pos++, resolver); + } + else if (clazz.isAssignableFrom(targetType)) { // put supertype matches at the end of the + // specificAccessor list + generalAccessors.add(resolver); + } + } + } + } + } + List resolvers = new ArrayList(); + resolvers.addAll(specificAccessors); + resolvers.addAll(generalAccessors); + return resolvers; + } +} 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 96f273b9df..c806959152 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 @@ -21,11 +21,15 @@ import java.util.List; import java.util.Map; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; +import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; /** * An Indexer can index into some proceeding structure to access a particular piece of it. Supported structures are: @@ -38,6 +42,21 @@ import org.springframework.expression.spel.SpelMessage; // TODO support correct syntax for multidimensional [][][] and not [,,,] public class Indexer extends SpelNodeImpl { + // These fields are used when the indexer is being used as a property read accessor. If the name and + // target type match these cached values then the cachedReadAccessor is used to read the property. + // If they do not match, the correct accessor is discovered and then cached for later use. + private String cachedReadName; + private Class cachedReadTargetType; + private PropertyAccessor cachedReadAccessor; + + // These fields are used when the indexer is being used as a property write accessor. If the name and + // target type match these cached values then the cachedWriteAccessor is used to write the property. + // If they do not match, the correct accessor is discovered and then cached for later use. + private String cachedWriteName; + private Class cachedWriteTargetType; + private PropertyAccessor cachedWriteAccessor; + + public Indexer(int pos, SpelNodeImpl expr) { super(pos, expr); } @@ -87,61 +106,96 @@ public class Indexer extends SpelNodeImpl { return new TypedValue(o); } } - - int idx = (Integer)state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); - + if (targetObject == null) { throw new SpelEvaluationException(getStartPosition(),SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); } - if (targetObject.getClass().isArray()) { - return new TypedValue(accessArrayElement(targetObject, idx),TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); - } else if (targetObject instanceof Collection) { - Collection c = (Collection) targetObject; - if (idx >= c.size()) { - if (state.getConfiguration().isAutoGrowCollections()) { - // Grow the collection - Object newCollectionElement = null; - try { - int newElements = idx-c.size(); - Class elementClass = targetObjectTypeDescriptor.getElementType(); - if (elementClass == null) { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE); + // if the object is something that looks indexable by an integer, attempt to treat the index value as a number + if ((targetObject instanceof Collection ) || targetObject.getClass().isArray() || targetObject instanceof String) { + int idx = (Integer)state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); + if (targetObject.getClass().isArray()) { + return new TypedValue(accessArrayElement(targetObject, idx),TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); + } else if (targetObject instanceof Collection) { + Collection c = (Collection) targetObject; + if (idx >= c.size()) { + if (state.getConfiguration().isAutoGrowCollections()) { + // Grow the collection + Object newCollectionElement = null; + try { + int newElements = idx-c.size(); + Class elementClass = targetObjectTypeDescriptor.getElementType(); + if (elementClass == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE); + } + while (newElements>0) { + c.add(elementClass.newInstance()); + newElements--; + } + newCollectionElement = targetObjectTypeDescriptor.getElementType().newInstance(); } - while (newElements>0) { - c.add(elementClass.newInstance()); - newElements--; + catch (Exception ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW_COLLECTION); } - newCollectionElement = targetObjectTypeDescriptor.getElementType().newInstance(); + c.add(newCollectionElement); + return new TypedValue(newCollectionElement,TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); } - catch (Exception ex) { - throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW_COLLECTION); + else { + throw new SpelEvaluationException(getStartPosition(),SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS, c.size(), idx); } - c.add(newCollectionElement); - return new TypedValue(newCollectionElement,TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); } - else { - throw new SpelEvaluationException(getStartPosition(),SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS, c.size(), idx); + int pos = 0; + for (Object o : c) { + if (pos == idx) { + return new TypedValue(o,TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); + } + pos++; } - } - int pos = 0; - for (Object o : c) { - if (pos == idx) { - return new TypedValue(o,TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); + } else if (targetObject instanceof String) { + String ctxString = (String) targetObject; + if (idx >= ctxString.length()) { + throw new SpelEvaluationException(getStartPosition(),SpelMessage.STRING_INDEX_OUT_OF_BOUNDS, ctxString.length(), idx); } - pos++; + return new TypedValue(String.valueOf(ctxString.charAt(idx))); } - } else if (targetObject instanceof String) { - String ctxString = (String) targetObject; - if (idx >= ctxString.length()) { - throw new SpelEvaluationException(getStartPosition(),SpelMessage.STRING_INDEX_OUT_OF_BOUNDS, ctxString.length(), idx); + } + + // Try and treat the index value as a property of the context object + // TODO could call the conversion service to convert the value to a String + if (indexValue.getTypeDescriptor().getType()==String.class) { + Class targetObjectRuntimeClass = getObjectClass(targetObject); + String name = (String)indexValue.getValue(); + EvaluationContext eContext = state.getEvaluationContext(); + + try { + if (cachedReadName!=null && cachedReadName.equals(name) && cachedReadTargetType!=null && cachedReadTargetType.equals(targetObjectRuntimeClass)) { + // it is OK to use the cached accessor + return cachedReadAccessor.read(eContext, targetObject, name); + } + + List accessorsToTry = AstUtils.getPropertyAccessorsToTry(targetObjectRuntimeClass, state); + + if (accessorsToTry != null) { + for (PropertyAccessor accessor : accessorsToTry) { + if (accessor.canRead(eContext, targetObject, name)) { + if (accessor instanceof ReflectivePropertyAccessor) { + accessor = ((ReflectivePropertyAccessor)accessor).createOptimalAccessor(eContext, targetObject, name); + } + this.cachedReadAccessor = accessor; + this.cachedReadName = name; + this.cachedReadTargetType = targetObjectRuntimeClass; + return accessor.read(eContext, targetObject, name); + } + } + } + } catch (AccessException e) { + throw new SpelEvaluationException(getStartPosition(), e, SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetObjectTypeDescriptor.asString()); } - return new TypedValue(String.valueOf(ctxString.charAt(idx))); } + throw new SpelEvaluationException(getStartPosition(),SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetObjectTypeDescriptor.asString()); } - - + @Override public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException { return true; @@ -174,6 +228,7 @@ public class Indexer extends SpelNodeImpl { if (targetObjectTypeDescriptor.isArray()) { int idx = (Integer)state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); setArrayElement(state, contextObject.getValue(), idx, newValue, targetObjectTypeDescriptor.getElementType()); + return; } else if (targetObjectTypeDescriptor.isCollection()) { int idx = (Integer)state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); @@ -185,13 +240,46 @@ public class Indexer extends SpelNodeImpl { List list = (List)targetObject; Object possiblyConvertedValue = state.convertValue(newValue,TypeDescriptor.valueOf(targetObjectTypeDescriptor.getElementType())); list.set(idx,possiblyConvertedValue); + return; } else { - throw new SpelEvaluationException(getStartPosition(),SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, contextObject.getClass().getName()); + throw new SpelEvaluationException(getStartPosition(),SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetObjectTypeDescriptor.asString()); } - } else { - throw new SpelEvaluationException(getStartPosition(),SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, contextObject.getClass().getName()); } + + // Try and treat the index value as a property of the context object + // TODO could call the conversion service to convert the value to a String + if (index.getTypeDescriptor().getType()==String.class) { + Class contextObjectClass = getObjectClass(contextObject.getValue()); + String name = (String)index.getValue(); + EvaluationContext eContext = state.getEvaluationContext(); + try { + if (cachedWriteName!=null && cachedWriteName.equals(name) && cachedWriteTargetType!=null && cachedWriteTargetType.equals(contextObjectClass)) { + // it is OK to use the cached accessor + cachedWriteAccessor.write(eContext, targetObject, name,newValue); + return; + } + + List accessorsToTry = AstUtils.getPropertyAccessorsToTry(contextObjectClass, state); + if (accessorsToTry != null) { + for (PropertyAccessor accessor : accessorsToTry) { + if (accessor.canWrite(eContext, contextObject.getValue(), name)) { + this.cachedWriteName = name; + this.cachedWriteTargetType = contextObjectClass; + this.cachedWriteAccessor = accessor; + accessor.write(eContext, contextObject.getValue(), name, newValue); + return; + } + } + } + } catch (AccessException ae) { + throw new SpelEvaluationException(getStartPosition(), ae, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, + name, ae.getMessage()); + } + + } + + throw new SpelEvaluationException(getStartPosition(),SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetObjectTypeDescriptor.asString()); } @Override 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 d15f2bbe48..bfe55213da 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 @@ -249,6 +249,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl { return false; } + // TODO when there is more time, remove this and use the version in AstUtils /** * Determines the set of property resolvers that should be used to try and access a property on the specified target * type. The resolvers are considered to be in an ordered list, however in the returned list any that are exact diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java index e823b4817e..54dd02fd80 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java @@ -291,6 +291,125 @@ public class SpringEL300Tests extends ExpressionTestCase { } } + + @Test + public void testNestedProperties_SPR6923() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new Foo()); + String name = null; + Expression expr = null; + + expr = new SpelExpressionParser().parseRaw("resource.resource.server"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("abc",name); + } + + static class Foo { + public ResourceSummary resource = new ResourceSummary(); + } + + static class ResourceSummary { + ResourceSummary() { + this.resource = new Resource(); + } + private final Resource resource; + public Resource getResource() { + return resource; + } + } + + static class Resource { + public String getServer() { + return "abc"; + } + } + + /** Should be accessing Goo.getKey because 'bar' field evaluates to "key" */ + @Test + public void testIndexingAsAPropertyAccess_SPR6968_1() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new Goo()); + String name = null; + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[bar]"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("hello",name); + name = expr.getValue(eContext,String.class); // will be using the cached accessor this time + Assert.assertEquals("hello",name); + } + + /** Should be accessing Goo.getKey because 'bar' variable evaluates to "key" */ + @Test + public void testIndexingAsAPropertyAccess_SPR6968_2() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new Goo()); + eContext.setVariable("bar","key"); + String name = null; + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[#bar]"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("hello",name); + name = expr.getValue(eContext,String.class); // will be using the cached accessor this time + Assert.assertEquals("hello",name); + } + + /** Should be accessing Goo.wibble field because 'bar' variable evaluates to "wibble" */ + @Test + public void testIndexingAsAPropertyAccess_SPR6968_3() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new Goo()); + eContext.setVariable("bar","wibble"); + String name = null; + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[#bar]"); + // will access the field 'wibble' and not use a getter + name = expr.getValue(eContext,String.class); + Assert.assertEquals("wobble",name); + name = expr.getValue(eContext,String.class); // will be using the cached accessor this time + Assert.assertEquals("wobble",name); + } + + /** Should be accessing (setting) Goo.wibble field because 'bar' variable evaluates to "wibble" */ + @Test + public void testIndexingAsAPropertyAccess_SPR6968_4() { + Goo g = Goo.instance; + StandardEvaluationContext eContext = new StandardEvaluationContext(g); + eContext.setVariable("bar","wibble"); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[#bar]='world'"); + // will access the field 'wibble' and not use a getter + expr.getValue(eContext,String.class); + Assert.assertEquals("world",g.wibble); + expr.getValue(eContext,String.class); // will be using the cached accessor this time + Assert.assertEquals("world",g.wibble); + } + + /** Should be accessing Goo.setKey field because 'bar' variable evaluates to "key" */ + @Test + public void testIndexingAsAPropertyAccess_SPR6968_5() { + Goo g = Goo.instance; + StandardEvaluationContext eContext = new StandardEvaluationContext(g); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[bar]='world'"); + expr.getValue(eContext,String.class); + Assert.assertEquals("world",g.value); + expr.getValue(eContext,String.class); // will be using the cached accessor this time + Assert.assertEquals("world",g.value); + } + + static class Goo { + + public static Goo instance = new Goo(); + public String bar = "key"; + public String value = null; + + public String wibble = "wobble"; + + public String getKey() { + return "hello"; + } + + public void setKey(String s) { + value = s; + } + + } // ---