diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/converters/NumberToNumber.java b/spring-binding/src/main/java/org/springframework/binding/convert/converters/NumberToNumber.java new file mode 100644 index 00000000..fac0469d --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/converters/NumberToNumber.java @@ -0,0 +1,37 @@ +package org.springframework.binding.convert.converters; + +import org.springframework.util.NumberUtils; + +/** + * A one-way converter that can convert from any JDK-standard Number implementation to any other JDK-standard Number + * implementation. + * + * Support Number classes include byte, short, integer, float, double, long, big integer, big decimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * + * @author Keith Donald + */ +public class NumberToNumber implements Converter { + + public Class getSourceClass() { + return Number.class; + } + + public Class getTargetClass() { + return Number.class; + } + + public Object convertSourceToTargetClass(Object source, Class targetClass) throws Exception { + return NumberUtils.convertNumberToTargetClass((Number) source, targetClass); + } + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/service/DefaultConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/service/DefaultConversionService.java index 8f7c4124..d300bbb0 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/service/DefaultConversionService.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/service/DefaultConversionService.java @@ -20,6 +20,7 @@ import java.math.BigInteger; import java.util.Date; import java.util.Locale; +import org.springframework.binding.convert.converters.NumberToNumber; import org.springframework.binding.convert.converters.ObjectToCollection; import org.springframework.binding.convert.converters.StringToBigDecimal; import org.springframework.binding.convert.converters.StringToBigInteger; @@ -70,6 +71,7 @@ public class DefaultConversionService extends GenericConversionService { addConverter(new StringToDate()); addConverter(new StringToLabeledEnum()); addConverter(new ObjectToCollection(this)); + addConverter(new NumberToNumber()); } protected void addDefaultAliases() { diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/el/BindingValueExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/el/BindingValueExpression.java new file mode 100644 index 00000000..b6db2686 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/el/BindingValueExpression.java @@ -0,0 +1,95 @@ +package org.springframework.binding.expression.el; + +import javax.el.ELContext; +import javax.el.ELException; +import javax.el.ExpressionFactory; +import javax.el.PropertyNotFoundException; +import javax.el.PropertyNotWritableException; +import javax.el.ValueExpression; + +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.util.Assert; + +/** + * A ValueExpression decorator that decorates a target ValueExpression returned by a EL Implementation's + * {@link ExpressionFactory} to allow use of Spring's type conversion system for coersing expression values. + * + * The Unified EL specification currently provides no standard way of plugging in custom type converters. This decorator + * allows Spring type converters to be utilized with any EL implementation. + * + * @author Keith Donald + */ +class BindingValueExpression extends ValueExpression { + + private ValueExpression targetExpression; + + private Class expectedType; + + private ConversionService conversionService; + + private boolean template; + + public BindingValueExpression(ValueExpression targetExpression, Class expectedType, + ConversionService conversionService, boolean template) { + Assert.notNull(expectedType, "The expectedType Class is required"); + Assert.notNull(conversionService, "The ConversionService to perform type coersions is required"); + this.targetExpression = targetExpression; + this.expectedType = expectedType; + this.conversionService = conversionService; + this.template = template; + } + + public Class getExpectedType() { + return targetExpression.getExpectedType(); + } + + public Class getType(ELContext context) throws NullPointerException, PropertyNotFoundException, ELException { + return targetExpression.getType(context); + } + + public Object getValue(ELContext context) throws NullPointerException, PropertyNotFoundException, ELException { + Object value = targetExpression.getValue(context); + return conversionService.executeConversion(value, expectedType); + } + + public boolean isReadOnly(ELContext context) throws NullPointerException, PropertyNotFoundException, ELException { + return targetExpression.isReadOnly(context); + } + + public void setValue(ELContext context, Object value) throws NullPointerException, PropertyNotFoundException, + PropertyNotWritableException, ELException { + Class targetType = targetExpression.getType(context); + if (value != null && targetType != null) { + ConversionExecutor converter = conversionService.getConversionExecutor(value.getClass(), targetType); + value = converter.execute(value); + } + targetExpression.setValue(context, value); + } + + public String getExpressionString() { + if (template) { + return targetExpression.getExpressionString(); + } else { + String rawExpressionString = targetExpression.getExpressionString(); + return rawExpressionString.substring("#{".length(), rawExpressionString.length() - 1); + } + } + + public boolean isLiteralText() { + return targetExpression.isLiteralText(); + } + + public boolean equals(Object obj) { + if (!(obj instanceof BindingValueExpression)) { + return false; + } + BindingValueExpression exp = (BindingValueExpression) obj; + return targetExpression.equals(exp.targetExpression); + } + + public int hashCode() { + return targetExpression.hashCode(); + } + +} diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java index 8299f284..54c05364 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpression.java @@ -19,8 +19,6 @@ import javax.el.ELContext; import javax.el.ELException; import javax.el.ValueExpression; -import org.springframework.binding.convert.ConversionExecutor; -import org.springframework.binding.convert.ConversionService; import org.springframework.binding.expression.EvaluationException; import org.springframework.binding.expression.Expression; import org.springframework.binding.expression.PropertyNotFoundException; @@ -37,26 +35,16 @@ public class ELExpression implements Expression { private ValueExpression valueExpression; - private boolean template; - - private ConversionService conversionService; - /** * Creates a new el expression * @param factory the el context factory for creating the EL context that will be used during expression evaluation * @param valueExpression the value expression to evaluate - * @param template whether or not this expression is a template expression; if not it was parsed as an implict eval - * expression (without delimiters) */ - public ELExpression(ELContextFactory factory, ValueExpression valueExpression, ConversionService conversionService, - boolean template) { + public ELExpression(ELContextFactory factory, ValueExpression valueExpression) { Assert.notNull(factory, "The ELContextFactory is required to evaluate EL expressions"); Assert.notNull(valueExpression, "The EL ValueExpression is required for evaluation"); - Assert.notNull(conversionService, "The ConversionService to perform type coersions is required"); this.elContextFactory = factory; this.valueExpression = valueExpression; - this.conversionService = conversionService; - this.template = template; } public Object getValue(Object context) throws EvaluationException { @@ -86,11 +74,6 @@ public class ELExpression implements Expression { public void setValue(Object context, Object value) throws EvaluationException { ELContext ctx = elContextFactory.getELContext(context); try { - Class targetType = getValueType(context); - if (value != null && targetType != null) { - ConversionExecutor converter = conversionService.getConversionExecutor(value.getClass(), targetType); - value = converter.execute(value); - } valueExpression.setValue(ctx, value); if (!ctx.isPropertyResolved()) { throw new EvaluationException(context.getClass(), getExpressionString(), "The expression '" @@ -120,12 +103,7 @@ public class ELExpression implements Expression { } public String getExpressionString() { - if (template) { - return valueExpression.getExpressionString(); - } else { - String rawExpressionString = valueExpression.getExpressionString(); - return rawExpressionString.substring("#{".length(), rawExpressionString.length() - 1); - } + return valueExpression.getExpressionString(); } private String getBaseVariable() { diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java index 5d91b428..539f6073 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/el/ELExpressionParser.java @@ -103,7 +103,7 @@ public class ELExpressionParser implements ExpressionParser { try { ValueExpression expression = parseValueExpression(expressionString, context); ELContextFactory contextFactory = getContextFactory(context.getEvaluationContextType(), expressionString); - return new ELExpression(contextFactory, expression, conversionService, template); + return new ELExpression(contextFactory, expression); } catch (ELException e) { throw new ParserException(expressionString, e); } @@ -112,16 +112,13 @@ public class ELExpressionParser implements ExpressionParser { private ValueExpression parseValueExpression(String expressionString, ParserContext context) throws ELException { ParserELContext elContext = new ParserELContext(); elContext.mapVariables(context.getExpressionVariables(), expressionFactory); - return expressionFactory.createValueExpression(elContext, expressionString, getExpectedType(context)); + ValueExpression expression = expressionFactory.createValueExpression(elContext, expressionString, Object.class); + return new BindingValueExpression(expression, getExpectedType(context), conversionService, context.isTemplate()); } private Class getExpectedType(ParserContext context) { Class expectedType = context.getExpectedEvaluationResultType(); - if (expectedType != null) { - return expectedType; - } else { - return Object.class; - } + return expectedType != null ? expectedType : Object.class; } private ELContextFactory getContextFactory(Class expressionTargetType, String expressionString) { @@ -134,20 +131,19 @@ public class ELExpressionParser implements ExpressionParser { private void init(ExpressionFactory expressionFactory) { this.expressionFactory = expressionFactory; - DefaultElContextFactory defaultContextFactory = new DefaultElContextFactory(); - putContextFactory(null, defaultContextFactory); - putContextFactory(Object.class, defaultContextFactory); + DefaultElContextFactory contextFactory = new DefaultElContextFactory(); + putContextFactory(null, contextFactory); + putContextFactory(Object.class, contextFactory); } private void assertNotDelimited(String expressionString) { if ((expressionString.startsWith("#{") && expressionString.endsWith("}")) || (expressionString.startsWith("${") && expressionString.endsWith("}"))) { - throw new ParserException( - expressionString, - "This expression '" - + expressionString - + "' being parsed is expected be an 'eval' EL expression string. Do not attempt to enclose such expression strings in #{} or ${} delimiters--this is redundant. If you need to parse a template that mixes literal text with evaluatable blocks, set the 'template' parser context attribute to true.", - null); + throw new ParserException(expressionString, "This expression '" + expressionString + + "' being parsed is expected be an 'eval' EL expression string. " + + "Do not attempt to enclose such expression strings in #{} or ${} delimiters. " + + "If you need to parse a template that mixes literal text with evaluatable blocks, " + + "set the 'template' parser context attribute to true.", null); } } diff --git a/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java b/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java index cd14ed3d..4648cecc 100644 --- a/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java +++ b/spring-binding/src/main/java/org/springframework/binding/message/MessageContextErrors.java @@ -15,10 +15,21 @@ */ package org.springframework.binding.message; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; import java.util.List; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.support.FluentParserContext; +import org.springframework.binding.mapping.MappingResult; +import org.springframework.binding.mapping.MappingResults; +import org.springframework.binding.mapping.MappingResultsCriteria; import org.springframework.validation.AbstractErrors; import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; /** * Adapts a MessageContext object to the Spring Errors interface. Allows Spring Validators to record errors that are @@ -30,12 +41,27 @@ public class MessageContextErrors extends AbstractErrors { private MessageContext messageContext; + private String objectName; + + private Object boundObject; + + private ExpressionParser expressionParser; + + private MappingResults mappingResults; + /** * Creates a new message context errors adapter. * @param messageContext the backing message context + * @param objectName the object name */ - public MessageContextErrors(MessageContext messageContext) { + public MessageContextErrors(MessageContext messageContext, String objectName, Object boundObject, + ExpressionParser expressionParser, MappingResults mappingResults) { this.messageContext = messageContext; + this.messageContext = messageContext; + this.boundObject = boundObject; + this.objectName = objectName; + this.expressionParser = expressionParser; + this.mappingResults = mappingResults; } public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { @@ -49,23 +75,97 @@ public class MessageContextErrors extends AbstractErrors { } public void addAllErrors(Errors errors) { - throw new UnsupportedOperationException("Not expected to be called by a validator"); - } - - public List getFieldErrors() { - throw new UnsupportedOperationException("Not expected to be called by a validator"); - } - - public Object getFieldValue(String field) { - throw new UnsupportedOperationException("Not expected to be called by a validator"); - } - - public List getGlobalErrors() { - throw new UnsupportedOperationException("Not expected to be called by a validator"); + Iterator it = errors.getAllErrors().iterator(); + while (it.hasNext()) { + ObjectError error = (ObjectError) it.next(); + if (error instanceof FieldError) { + FieldError fieldError = (FieldError) error; + rejectValue(fieldError.getField(), error.getCode(), error.getArguments(), error.getDefaultMessage()); + } else { + reject(error.getCode(), error.getArguments(), error.getDefaultMessage()); + } + } } public String getObjectName() { - throw new UnsupportedOperationException("Not expected to be called by a validator"); + return objectName; } -} + public List getGlobalErrors() { + Message[] messages = messageContext.getMessagesByCriteria(GLOBAL_ERROR); + if (messages.length == 0) { + return Collections.EMPTY_LIST; + } + List errors = new ArrayList(messages.length); + for (int i = 0; i < messages.length; i++) { + Message message = messages[i]; + errors.add(new ObjectError(objectName, message.getText())); + } + return errors; + } + + public List getFieldErrors() { + Message[] messages = messageContext.getMessagesByCriteria(FIELD_ERROR); + if (messages.length == 0) { + return Collections.EMPTY_LIST; + } + List errors = new ArrayList(messages.length); + for (int i = 0; i < messages.length; i++) { + Message message = messages[i]; + errors.add(new FieldError(objectName, (String) message.getSource(), message.getText())); + } + return errors; + } + + public Object getFieldValue(String field) { + if (mappingResults != null) { + List results = mappingResults.getResults(new FieldErrorResult(field)); + if (!results.isEmpty()) { + MappingResult fieldError = (MappingResult) results.get(0); + return fieldError.getResult().getOriginalValue(); + } + } + return parseFieldExpression(field).getValue(boundObject); + } + + private Expression parseFieldExpression(String field) { + return expressionParser.parseExpression(field, new FluentParserContext().evaluate(boundObject.getClass())); + } + + private static MessageCriteria GLOBAL_ERROR = new MessageCriteria() { + public boolean test(Message message) { + if (message.getSource() == null && message.getSeverity().equals(Severity.ERROR)) { + return true; + } else { + return false; + } + } + }; + + private static MessageCriteria FIELD_ERROR = new MessageCriteria() { + public boolean test(Message message) { + if (message.getSource() != null && message.getSeverity().equals(Severity.ERROR)) { + return true; + } else { + return false; + } + } + }; + + private static class FieldErrorResult implements MappingResultsCriteria { + + private String field; + + public FieldErrorResult(String field) { + this.field = field; + } + + public boolean test(MappingResult result) { + if (field.equals(result.getMapping().getTargetExpression().getExpressionString())) { + return true; + } else { + return false; + } + } + } +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/message/MessageContextErrorsTests.java b/spring-binding/src/test/java/org/springframework/binding/message/MessageContextErrorsTests.java index 92bf5f87..d8ec5ec2 100644 --- a/spring-binding/src/test/java/org/springframework/binding/message/MessageContextErrorsTests.java +++ b/spring-binding/src/test/java/org/springframework/binding/message/MessageContextErrorsTests.java @@ -1,10 +1,12 @@ package org.springframework.binding.message; +import java.util.HashMap; import java.util.Locale; import junit.framework.TestCase; import org.springframework.context.support.StaticMessageSource; +import org.springframework.validation.MapBindingResult; public class MessageContextErrorsTests extends TestCase { public void testReject() { @@ -13,7 +15,8 @@ public class MessageContextErrorsTests extends TestCase { messageSource.addMessage("bar", Locale.getDefault(), "{0}"); DefaultMessageContext context = new DefaultMessageContext(messageSource); - MessageContextErrors errors = new MessageContextErrors(context); + Object object = new Object(); + MessageContextErrors errors = new MessageContextErrors(context, "object", object, null, null); errors.reject("foo"); errors.reject("bogus", "baz"); errors.reject("bar", new Object[] { "boop" }, null); @@ -40,7 +43,8 @@ public class MessageContextErrorsTests extends TestCase { messageSource.addMessage("bar", Locale.getDefault(), "{0}"); DefaultMessageContext context = new DefaultMessageContext(messageSource); - MessageContextErrors errors = new MessageContextErrors(context); + Object object = new Object(); + MessageContextErrors errors = new MessageContextErrors(context, "object", object, null, null); errors.rejectValue("field", "foo"); errors.rejectValue("field", "bogus", "baz"); errors.rejectValue("field", "bar", new Object[] { "boop" }, null); @@ -60,4 +64,41 @@ public class MessageContextErrorsTests extends TestCase { assertEquals("boop", msg.getText()); assertEquals(Severity.ERROR, msg.getSeverity()); } -} + + public void testAddAllErrors() { + StaticMessageSource messageSource = new StaticMessageSource(); + messageSource.addMessage("foo", Locale.getDefault(), "bar"); + messageSource.addMessage("bar", Locale.getDefault(), "{0}"); + + DefaultMessageContext context = new DefaultMessageContext(messageSource); + Object object = new Object(); + MessageContextErrors errors = new MessageContextErrors(context, "object", object, null, null); + MapBindingResult result = new MapBindingResult(new HashMap(), "object"); + result.reject("bar", new Object[] { "boop" }, null); + result.rejectValue("field", "bar", new Object[] { "boop" }, null); + errors.addAllErrors(result); + + Message msg = context.getAllMessages()[0]; + assertEquals(null, msg.getSource()); + assertEquals("boop", msg.getText()); + assertEquals(Severity.ERROR, msg.getSeverity()); + + msg = context.getAllMessages()[1]; + assertEquals("field", msg.getSource()); + assertEquals("boop", msg.getText()); + assertEquals(Severity.ERROR, msg.getSeverity()); + } + + public void testGetGlobalErrors() { + + } + + public void testGetFieldErrors() { + + } + + public void testGetFieldValue() { + + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java index 158d28dd..0d403c93 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java @@ -434,8 +434,10 @@ public abstract class AbstractMvcView implements View { validateMethod = ReflectionUtils.findMethod(validator.getClass(), validateMethodName, new Class[] { model.getClass(), Errors.class }); if (validateMethod != null) { - ReflectionUtils.invokeMethod(validateMethod, validator, new Object[] { model, - new MessageContextErrors(requestContext.getMessageContext()) }); + String objectName = getModelExpression().getExpressionString(); + MessageContextErrors errors = new MessageContextErrors(requestContext.getMessageContext(), + objectName, model, expressionParser, mappingResults); + ReflectionUtils.invokeMethod(validateMethod, validator, new Object[] { model, errors }); } } }