diff --git a/spring-binding/.classpath b/spring-binding/.classpath index 0781f46a..228823ca 100644 --- a/spring-binding/.classpath +++ b/spring-binding/.classpath @@ -15,6 +15,7 @@ + diff --git a/spring-binding/ivy.xml b/spring-binding/ivy.xml index cfe7c0e6..4bc806a7 100644 --- a/spring-binding/ivy.xml +++ b/spring-binding/ivy.xml @@ -26,6 +26,7 @@ + diff --git a/spring-binding/pom.xml b/spring-binding/pom.xml index 81d5bbfd..b0f8927a 100644 --- a/spring-binding/pom.xml +++ b/spring-binding/pom.xml @@ -27,6 +27,11 @@ spring-core ${spring.version} + + org.springframework + spring-expression + ${spring.version} + ognl ognl diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/spel/SpringELExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/spel/SpringELExpression.java new file mode 100644 index 00000000..3ff96b0a --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/spel/SpringELExpression.java @@ -0,0 +1,136 @@ +/* + * Copyright 2004-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.binding.expression.spel; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ValueCoercionException; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; + +/** + *

+ * A wrapper for a Spring EL {@link org.springframework.expression.Expression} allowing it to be used under the Spring + * Binding {@link Expression} abstraction. + *

+ * + * @author Rossen Stoyanchev + * @since 2.1.0 + */ +public class SpringELExpression implements Expression { + + private org.springframework.expression.Expression expression; + + private StandardEvaluationContext evaluationContext; + + private Class expectedType; + + private Map expressionVariables; + + /** + * Constructor for SpringELExpression. + * + * @param expression a parsed Spring EL expression instance. Must not be null. + * @param expressionVariables provides a mapping between variables names and parsed Spring EL expression instances. + * This parameter is optional (may be null). + * @param expectedType the target type expected from the evaluation of the expression or null. This parameter is + * optional (may be null). + * @param evaluationContext a Spring EL evaluation context. + */ + public SpringELExpression(org.springframework.expression.Expression expression, Map expressionVariables, + Class expectedType, StandardEvaluationContext evaluationContext) { + Assert.notNull(expression, "The SpelExpression is required for evaluation"); + Assert.notNull(evaluationContext, "The StandardEvaluationContext is required for evaluation"); + this.expression = expression; + this.expressionVariables = expressionVariables; + this.expectedType = expectedType; + this.evaluationContext = evaluationContext; + } + + public String getExpressionString() { + return expression.getExpressionString(); + } + + public Object getValue(Object rootObject) throws EvaluationException { + try { + updateEvaluationContext(rootObject); + return expression.getValue(evaluationContext, expectedType); + } catch (SpelEvaluationException e) { + if (e.getMessageCode().equals(SpelMessage.TYPE_CONVERSION_ERROR)) { + throw new ValueCoercionException(rootObject.getClass(), getExpressionString(), null, expectedType, e); + } + throw new EvaluationException(rootObject.getClass(), expression.getExpressionString(), + "An ELException occurred getting the value for expression '" + getExpressionString() + + "' on context [" + rootObject.getClass() + "]", e); + } + } + + public Class getValueType(Object rootObject) throws EvaluationException { + evaluationContext.setRootObject(rootObject); + return expression.getValueType(evaluationContext); + } + + public void setValue(Object rootObject, Object value) throws EvaluationException { + try { + updateEvaluationContext(rootObject); + expression.setValue(evaluationContext, value); + } catch (SpelEvaluationException e) { + if (e.getMessageCode().equals(SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE)) { + throw new ValueCoercionException(rootObject.getClass(), getExpressionString(), value, expectedType, e); + } + throw new EvaluationException(rootObject.getClass(), getExpressionString(), + "An ELException occurred setting the value of expression '" + getExpressionString() + + "' on context [" + rootObject.getClass() + "] to [" + value + "]", e); + } + } + + /** + * Updates the Spring EL evaluation context to reflect the given rootObject. + * + * @param rootObject the object for the evaluation. + */ + private void updateEvaluationContext(Object rootObject) { + evaluationContext.setRootObject(rootObject); + evaluationContext.setVariables(getVariableValues(rootObject)); + } + + /** + * Turns the map of variable-names-to-expressions into a map of variable-names-to-plain-objects by evaluating each + * object against the input rootObject. + * + * @param rootObject the Object to evaluate variable expressions against. + * @return a mapping between variables names and plain Object's. + */ + private Map getVariableValues(Object rootObject) { + if (expressionVariables == null) { + return Collections.EMPTY_MAP; + } + Map variableValues = new HashMap(expressionVariables.size()); + for (Iterator iterator = expressionVariables.entrySet().iterator(); iterator.hasNext();) { + Map.Entry var = (Map.Entry) iterator.next(); + variableValues.put(var.getKey(), ((Expression) var.getValue()).getValue(rootObject)); + } + return variableValues; + } + +} diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/spel/SpringELExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/spel/SpringELExpressionParser.java new file mode 100644 index 00000000..f353aa0b --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/spel/SpringELExpressionParser.java @@ -0,0 +1,106 @@ +/* + * Copyright 2004-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.binding.expression.spel; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.ExpressionVariable; +import org.springframework.binding.expression.ParserContext; +import org.springframework.binding.expression.ParserException; +import org.springframework.binding.expression.support.NullParserContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.util.Assert; + +/** + *

+ * Adapts the Spring EL {@link SpelExpressionParser} to the Spring Binding {@link ExpressionParser} interface. + *

+ * + * @author Rossen Stoyanchev + * @since 2.1.0 + */ +public class SpringELExpressionParser implements ExpressionParser { + + private SpelExpressionParser expressionParser; + + private TypeConverter typeConverter = new StandardTypeConverter(); + + private List propertyAccessors = new ArrayList(); + + public SpringELExpressionParser(SpelExpressionParser expressionParser) { + this.expressionParser = expressionParser; + } + + public void setConversionService(ConversionService conversionService) { + typeConverter = new StandardTypeConverter(conversionService); + } + + public void addPropertyAccessor(PropertyAccessor propertyAccessor) { + propertyAccessors.add(propertyAccessor); + } + + public Expression parseExpression(String expressionString, ParserContext parserContext) throws ParserException { + Assert.hasText(expressionString, "The expression string to parse is required and must not be empty"); + parserContext = (parserContext == null) ? NullParserContext.INSTANCE : parserContext; + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + evaluationContext.setTypeConverter(typeConverter); + evaluationContext.getPropertyAccessors().addAll(propertyAccessors); + Map spelExpressionVariables = parseSpelExpressionVariables(parserContext.getExpressionVariables()); + return new SpringELExpression(parseSpelExpression(expressionString, parserContext), spelExpressionVariables, + parserContext.getExpectedEvaluationResultType(), evaluationContext); + } + + private org.springframework.expression.Expression parseSpelExpression(String expression, ParserContext parserContext) { + return expressionParser.parseExpression(expression, getSpelParserContext(parserContext)); + } + + private org.springframework.expression.ParserContext getSpelParserContext(ParserContext parserContext) { + return parserContext.isTemplate() ? org.springframework.expression.ParserContext.TEMPLATE_EXPRESSION : null; + } + + /** + * Turns {@link ExpressionVariable}'s (pairs of variable names and string expressions) into a map of variable names + * and parsed Spring EL expressions. The map will be saved in a Spring EL {@link EvaluationContext} for later use at + * evaluation time. + * + * @param expressionVariables an array of ExpressionVariable instances. + * @return a Map or null if the input array is empty. + */ + private Map parseSpelExpressionVariables(ExpressionVariable[] expressionVariables) { + if (expressionVariables == null || expressionVariables.length == 0) { + return null; + } + Map spelExpressionVariables = new HashMap(expressionVariables.length); + for (int i = 0; i < expressionVariables.length; i++) { + ExpressionVariable var = expressionVariables[i]; + spelExpressionVariables.put(var.getName(), + parseExpression(var.getValueExpression(), var.getParserContext())); + } + return spelExpressionVariables; + } + +} diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/el/ELExpressionParserTests.java b/spring-binding/src/test/java/org/springframework/binding/expression/el/ELExpressionParserTests.java index d95babbf..1ff18cfc 100644 --- a/spring-binding/src/test/java/org/springframework/binding/expression/el/ELExpressionParserTests.java +++ b/spring-binding/src/test/java/org/springframework/binding/expression/el/ELExpressionParserTests.java @@ -1,8 +1,6 @@ package org.springframework.binding.expression.el; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; import javax.el.ELContext; import javax.el.ELResolver; @@ -198,60 +196,6 @@ public class ELExpressionParserTests extends TestCase { } } - public static class TestBean { - private String value = "foo"; - - private int maximum = 2; - - private TestBean bean; - - private List list = new ArrayList(); - - public TestBean() { - initList(); - } - - public TestBean(TestBean bean) { - this.bean = bean; - initList(); - } - - private void initList() { - list.add("1"); - list.add("2"); - list.add("3"); - list.add(null); - } - - public TestBean getBean() { - return bean; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String encode(String data) { - return "!" + data; - } - - public int getMaximum() { - return maximum; - } - - public void setMaximum(int maximum) { - this.maximum = maximum; - } - - public List getList() { - return list; - } - } - private static class TestELContextFactory implements ELContextFactory { public ELContext getELContext(final Object target) { return new ELContext() { diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/el/TestBean.java b/spring-binding/src/test/java/org/springframework/binding/expression/el/TestBean.java new file mode 100644 index 00000000..4851e24b --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/expression/el/TestBean.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004-2007 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.binding.expression.el; + +import java.util.ArrayList; +import java.util.List; + +public class TestBean { + + private String value = "foo"; + private int maximum = 2; + private TestBean bean; + private List list = new ArrayList(); + + public TestBean() { + initList(); + } + + public TestBean(TestBean bean) { + this.bean = bean; + initList(); + } + + private void initList() { + list.add("1"); + list.add("2"); + list.add("3"); + list.add(null); + } + + public TestBean getBean() { + return bean; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String encode(String data) { + return "!" + data; + } + + public int getMaximum() { + return maximum; + } + + public void setMaximum(int maximum) { + this.maximum = maximum; + } + + public List getList() { + return list; + } +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/spel/ELExpressionParserCompatibilityTests.java b/spring-binding/src/test/java/org/springframework/binding/expression/spel/ELExpressionParserCompatibilityTests.java new file mode 100644 index 00000000..61b452cc --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/expression/spel/ELExpressionParserCompatibilityTests.java @@ -0,0 +1,222 @@ +/* + * Copyright 2004-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.binding.expression.spel; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionVariable; +import org.springframework.binding.expression.ValueCoercionException; +import org.springframework.binding.expression.el.ELExpressionParser; +import org.springframework.binding.expression.el.ELExpressionParserTests; +import org.springframework.binding.expression.el.TestBean; +import org.springframework.binding.expression.support.FluentParserContext; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + *

+ * Test cases to verify that {@link SpringELExpressionParser} matches to the functionality of {@link ELExpressionParser} + * as demonstrated in {@link ELExpressionParserTests}. + *

+ * + * @author Rossen Stoyanchev + */ +public class ELExpressionParserCompatibilityTests extends TestCase { + + private SpringELExpressionParser parser = new SpringELExpressionParser(new SpelExpressionParser()); + + protected void setUp() throws Exception { + parser.addPropertyAccessor(new SpecialPropertyAccessor()); + } + + public void testParseSimpleEvalExpressionNoParserContext() { + String expressionString = "3 + 4"; + Expression exp = parser.parseExpression(expressionString, null); + assertEquals(new Integer(7), exp.getValue(null)); // Unified EL returns Long + } + + public void testParseNullExpressionString() { + String expressionString = null; + try { + parser.parseExpression(expressionString, null); + fail("should have thrown iae"); + } catch (IllegalArgumentException e) { + + } + } + + public void testParseNull() { + Expression exp = parser.parseExpression("null", null); + assertEquals(null, exp.getValue(null)); + } + + public void testParseEmptyExpressionString() { + String expressionString = ""; + try { + parser.parseExpression(expressionString, null); + fail("should have thrown iae"); + } catch (IllegalArgumentException e) { + + } + } + + public void testParseSimpleEvalExpressionNoEvalContextWithTypeCoersion() { + String expressionString = "3 + 4"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext().expectResult(Long.class)); + assertEquals(new Long(7), exp.getValue(null)); + } + + public void testParseBeanEvalExpressionNoParserContext() { + String expressionString = "value"; + Expression exp = parser.parseExpression(expressionString, null); + assertEquals("foo", exp.getValue(new TestBean())); + } + + public void testParseEvalExpressionWithContextTypeCoersion() { + String expressionString = "maximum"; + Expression exp = parser + .parseExpression(expressionString, new FluentParserContext().expectResult(Integer.class)); + assertEquals(new Integer(2), exp.getValue(new TestBean())); + } + + public void testParseEvalExpressionWithContextCustomELVariableResolver() { + String expressionString = "specialProperty"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext().evaluate(TestBean.class)); + assertEquals("Custom resolver resolved this special property!", exp.getValue(new TestBean())); + } + + public void testParseBeanEvalExpressionInvalidELVariable() { + try { + String expressionString = "bogus"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext() + .evaluate(TestBean.class)); + exp.getValue(new TestBean()); + fail("Should have failed"); + } catch (EvaluationException e) { + + } + } + + public void testParseLiteralExpression() { + String expressionString = "'value'"; + Expression exp = parser.parseExpression(expressionString, null); + assertEquals("value", exp.getValue(null)); + } + + public void testParseTemplateExpression() { + String expressionString = "text text text #{value} text text text#{value}"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext().template()); + TestBean target = new TestBean(); + assertEquals("text text text foo text text textfoo", exp.getValue(target)); + } + + public void testParseTemplateExpressionWithVariables() { + String expressionString = "#{value}#{#max}"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext().template().variable( + new ExpressionVariable("max", "maximum"))); + TestBean target = new TestBean(); + assertEquals("foo2", exp.getValue(target)); // TODO: + } + + public void testGetExpressionString() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, null); + assertEquals("maximum", exp.getExpressionString()); + } + + public void testGetExpressionType() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, null); + TestBean context = new TestBean(); + assertEquals(int.class, exp.getValueType(context)); + } + + public void testGetValueWithCoersion() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext().expectResult(String.class)); + TestBean context = new TestBean(); + assertEquals("2", exp.getValue(context)); + } + + public void testGetValueCoersionError() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, new FluentParserContext() + .expectResult(TestBean.class)); + TestBean context = new TestBean(); + try { + exp.getValue(context); + fail("Should have failed with coersion"); + } catch (ValueCoercionException e) { + } + } + + public void testSetValue() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, null); + TestBean context = new TestBean(); + exp.setValue(context, new Integer(5)); + assertEquals(5, context.getMaximum()); + } + + public void testSetValueWithTypeCoersion() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, null); + TestBean context = new TestBean(); + exp.setValue(context, "5"); + assertEquals(5, context.getMaximum()); + } + + public void testSetValueCoersionError() { + String expressionString = "maximum"; + Expression exp = parser.parseExpression(expressionString, null); + TestBean context = new TestBean(); + try { + exp.setValue(context, "bogus"); + fail("Should have failed with coersion"); + } catch (ValueCoercionException e) { + } + } + + private final class SpecialPropertyAccessor implements PropertyAccessor { + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + } + + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue("Custom resolver resolved this special property!", TypeDescriptor + .valueOf(String.class)); + } + + public Class[] getSpecificTargetClasses() { + return null; + } + + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return "specialProperty".equals(name); + } + } + +}