From caecdb26b0be0ed7afbcfe5bc73cd1cf33228722 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 17 Jun 2009 15:56:07 +0000 Subject: [PATCH] web bind and lifecycle tests; polish git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1393 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../ui/binding/BindingResult.java | 9 +- .../ui/binding/support/GenericBinder.java | 32 +++- .../WebBindAndValidateLifecycle.java | 10 +- .../ui/message/DefaultMessageResolver.java | 11 +- .../ui/message/MessageBuilder.java | 44 +---- .../ui/message/ResolvableArgument.java | 56 ++++++ .../ui/binding/support/WebBinderTests.java | 1 + .../lifecycle/WebBindAndLifecycleTests.java | 178 ++++++++++++++++++ .../ui/message/MessageBuilderTests.java | 2 +- .../support/DefaultMessageContextTests.java | 5 +- 10 files changed, 295 insertions(+), 53 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/ui/message/ResolvableArgument.java create mode 100644 org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java index c758288a11d..5135b38084f 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java @@ -35,13 +35,18 @@ public interface BindingResult { boolean isError(); /** - * If an error result, the error code; for example, "invalidFormat", "propertyNotFound", or "evaluationException". + * If an error result, the error code; for example, "invalidFormat" or "propertyNotFound". */ String getErrorCode(); + /** + * If an error, result returns a default message describing what went wrong. + */ + String getErrorMessage(); + + /** * If an error result, the cause of the error. - * @return the cause, or null if this is not an error */ Throwable getErrorCause(); diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java index a02c3e42319..f5b9ebf39d4 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java @@ -31,9 +31,12 @@ import java.util.Map; import org.springframework.context.expression.MapAccessor; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeConverter; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultTypeConverter; +import org.springframework.core.style.StylerUtils; +import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; @@ -463,6 +466,10 @@ public class GenericBinder implements Binder { return "invalidFormat"; } + public String getErrorMessage() { + return "Failed to bind to property '" + property + "'; the user value " + StylerUtils.style(formatted) + " has an invalid format and could no be parsed"; + } + public Throwable getErrorCause() { return e; } @@ -496,16 +503,33 @@ public class GenericBinder implements Binder { public String getErrorCode() { SpelMessage spelCode = ((SpelEvaluationException) e).getMessageCode(); - if (spelCode==SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { + if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { return "typeConversionFailure"; } else if (spelCode==SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { return "propertyNotFound"; } else { // TODO return more specific code based on underlying EvaluationException error code - return "couldNotSetValue"; + return "couldNotSetValue"; } } + public String getErrorMessage() { + SpelMessage spelCode = ((SpelEvaluationException) e).getMessageCode(); + if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { + AccessException accessException = (AccessException) e.getCause(); + if (accessException.getCause() != null) { + Throwable cause = accessException.getCause(); + if (cause instanceof SpelEvaluationException && ((SpelEvaluationException)cause).getMessageCode() == SpelMessage.TYPE_CONVERSION_ERROR) { + ConversionFailedException failure = (ConversionFailedException) cause.getCause(); + return "Failed to bind to property '" + property + "'; user value " + StylerUtils.style(formatted) + " could not be converted to property type [" + failure.getTargetType() + "]"; + } + } + } else if (spelCode==SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { + return "Failed to bind to property '" + property + "'; no such property exists on model"; + } + return "Failed to bind to property '" + property + "'; reason = " + e.getLocalizedMessage(); + } + public Throwable getErrorCause() { return e; } @@ -538,6 +562,10 @@ public class GenericBinder implements Binder { return null; } + public String getErrorMessage() { + return null; + } + public Throwable getErrorCause() { return null; } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java b/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java index 22a386a0fc0..e58a90a1312 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java @@ -21,9 +21,11 @@ import org.springframework.ui.binding.BindingResult; import org.springframework.ui.binding.BindingResults; import org.springframework.ui.binding.UserValues; import org.springframework.ui.binding.support.WebBinder; +import org.springframework.ui.message.ResolvableArgument; import org.springframework.ui.message.MessageBuilder; import org.springframework.ui.message.MessageContext; import org.springframework.ui.message.MessageResolver; +import org.springframework.ui.message.Severity; import org.springframework.ui.validation.Validator; /** @@ -42,7 +44,7 @@ public class WebBindAndValidateLifecycle { private Validator validator; public WebBindAndValidateLifecycle(Object model, MessageContext messageContext) { - // TODO allow binder to be configured with bindings from model metadata + // TODO allow binder to be configured with bindings from @Model metadata // TODO support @Bound property annotation? // TODO support @StrictBinding class-level annotation? this.binder = new WebBinder(model); @@ -52,7 +54,7 @@ public class WebBindAndValidateLifecycle { public void execute(Map userMap) { UserValues values = binder.createUserValues(userMap); BindingResults bindingResults = binder.bind(values); - if (validationDecider.shouldValidateAfter(bindingResults)) { + if (validator != null && validationDecider.shouldValidateAfter(bindingResults)) { // TODO get validation results validator.validate(binder.getModel(), bindingResults.successes().properties()); } @@ -60,14 +62,16 @@ public class WebBindAndValidateLifecycle { MessageBuilder builder = new MessageBuilder(); for (BindingResult result : bindingResults.failures()) { MessageResolver message = builder. + severity(Severity.ERROR). code(modelPropertyError(result)). code(propertyError(result)). code(typeError(result)). code(error(result)). - resolvableArg("label", getModelProperty(result)). + arg("label", new ResolvableArgument(getModelProperty(result))). arg("value", result.getUserValue()). // TODO add binding el resolver allowing binding.format to be called arg("binding", binder.getBinding(result.getProperty())). + defaultText(result.getErrorMessage()). // TODO allow binding result to contribute additional arguments build(); // TODO should model name be part of element id? diff --git a/org.springframework.context/src/main/java/org/springframework/ui/message/DefaultMessageResolver.java b/org.springframework.context/src/main/java/org/springframework/ui/message/DefaultMessageResolver.java index bc9a0e2fce1..5d36cd99226 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/message/DefaultMessageResolver.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/message/DefaultMessageResolver.java @@ -20,6 +20,7 @@ import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; import org.springframework.core.style.ToStringCreator; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; @@ -56,7 +57,12 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso // implementing MessageResolver public Message resolveMessage(MessageSource messageSource, Locale locale) { - String messageString = messageSource.getMessage(this, locale); + String messageString; + try { + messageString = messageSource.getMessage(this, locale); + } catch (NoSuchMessageException e) { + throw new MessageResolutionException("Unable to resolve message in MessageSource [" + messageSource + "]", e); + } Expression message; try { message = expressionParser.parseExpression(messageString, ParserContext.TEMPLATE_EXPRESSION); @@ -70,7 +76,7 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso String text = (String) message.getValue(context); return new TextMessage(severity, text); } catch (EvaluationException e) { - throw new MessageResolutionException("Failed to evaluate expression to generate message text", e); + throw new MessageResolutionException("Failed to evaluate message expression '" + message.getExpressionString() + "' to generate final message text", e); } } @@ -114,6 +120,7 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso } + @SuppressWarnings("unchecked") static class MessageArgumentAccessor implements PropertyAccessor { private MessageSource messageSource; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/message/MessageBuilder.java b/org.springframework.context/src/main/java/org/springframework/ui/message/MessageBuilder.java index 093f72ad293..eb6e39ca42e 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/message/MessageBuilder.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/message/MessageBuilder.java @@ -21,8 +21,6 @@ import java.util.Map; import java.util.Set; import org.springframework.context.MessageSource; -import org.springframework.context.MessageSourceResolvable; -import org.springframework.core.style.ToStringCreator; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -37,7 +35,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; * new MessageBuilder(). * severity(Severity.ERROR). * code("invalidFormat"). - * resolvableArg("label", "mathForm.decimalField"). + * arg("label", new LocalizedArgumentValue("mathForm.decimalField")). * arg("format", "#,###.##"). * defaultText("The decimal field must be in format #,###.##"). * build(); @@ -89,27 +87,17 @@ public class MessageBuilder { * Named message arguments are inserted by eval expressions denoted within the resolved message template. * For example, the value of the 'format' argument would be inserted where a corresponding #{format} expression is defined in the message template. * Successive calls to this method add additional arguments. + * May also add {@link ResolvableArgument resolvable arguments} whose values are resolved against the MessageSource passed to the {@link MessageResolver}. * @param name the argument name * @param value the argument value * @return this, for fluent API usage + * @see ResolvableArgument */ public MessageBuilder arg(String name, Object value) { args.put(name, value); return this; } - /** - * Add a message argument to insert into the message text, where the actual value to be inserted should be resolved by the {@link MessageSource}. - * Successive calls to this method add additional resolvable arguments. - * @param name the argument name - * @param code the code to use to resolve the argument value - * @return this, for fluent API usage - */ - public MessageBuilder resolvableArg(String name, Object code) { - args.put(name, new ResolvableArgumentValue(code)); - return this; - } - /** * Set the fallback text for the message. * If the message has no codes, this will always be used as the text. @@ -140,30 +128,4 @@ public class MessageBuilder { return new DefaultMessageResolver(severity, codesArray, args, defaultText, expressionParser); } - private static class ResolvableArgumentValue implements MessageSourceResolvable { - - private Object code; - - public ResolvableArgumentValue(Object code) { - this.code = code; - } - - public Object[] getArguments() { - return null; - } - - public String[] getCodes() { - return new String[] { code.toString() }; - } - - public String getDefaultMessage() { - return String.valueOf(code); - } - - public String toString() { - return new ToStringCreator(this).append("code", code).toString(); - } - - } - } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/message/ResolvableArgument.java b/org.springframework.context/src/main/java/org/springframework/ui/message/ResolvableArgument.java new file mode 100644 index 00000000000..380bb2192d9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/message/ResolvableArgument.java @@ -0,0 +1,56 @@ +/* + * Copyright 2004-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.ui.message; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.style.ToStringCreator; + +/** + * A message argument value that is resolved from a MessageSource. + * Allows the value to be localized. + * @see MessageSource + * @author Keith Donald + */ +public class ResolvableArgument implements MessageSourceResolvable { + + private String code; + + /** + * Creates a resolvable argument. + * @param code the code that will be used to lookup the argument value from the message source + */ + public ResolvableArgument(String code) { + this.code = code; + } + + public String[] getCodes() { + return new String[] { code.toString() }; + } + + public Object[] getArguments() { + return null; + } + + public String getDefaultMessage() { + return String.valueOf(code); + } + + public String toString() { + return new ToStringCreator(this).append("code", code).toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java index 3a2dc7dc3c9..ee9889025c3 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java @@ -25,6 +25,7 @@ import org.springframework.ui.format.number.CurrencyFormatter; public class WebBinderTests { TestBean bean = new TestBean(); + Binder binder = new WebBinder(bean); @Before diff --git a/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java b/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java new file mode 100644 index 00000000000..773ae8749a9 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java @@ -0,0 +1,178 @@ +package org.springframework.ui.lifecycle; + +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.ui.format.number.CurrencyFormat; +import org.springframework.ui.message.MockMessageSource; +import org.springframework.ui.message.Severity; +import org.springframework.ui.message.support.DefaultMessageContext; + +public class WebBindAndLifecycleTests { + + private WebBindAndValidateLifecycle lifecycle; + + private DefaultMessageContext messages; + + @Before + public void setUp() { + MockMessageSource messageSource = new MockMessageSource(); + messageSource.addMessage("invalidFormat", Locale.US, "#{label} must be a ${objectType} in format #{format}; parsing of your value '#{value}' failed at the #{errorPosition} character"); + messageSource.addMessage("typeConversionFailure", Locale.US, "The value '#{value}' entered into the #{label} field could not be converted"); + messageSource.addMessage("org.springframework.ui.lifecycle.WebBindAndLifecycleTests$TestBean.integer", Locale.US, "Integer"); + messages = new DefaultMessageContext(messageSource); + TestBean model = new TestBean(); + lifecycle = new WebBindAndValidateLifecycle(model, messages); + } + + @Test + public void testExecuteLifecycleNoErrors() { + Map userMap = new HashMap(); + userMap.put("string", "test"); + userMap.put("integer", "3"); + userMap.put("foo", "BAR"); + lifecycle.execute(userMap); + assertEquals(0, messages.getMessages().size()); + } + + @Test + public void testExecuteLifecycleBindingErrors() { + Map userMap = new HashMap(); + userMap.put("string", "test"); + userMap.put("integer", "bogus"); + userMap.put("foo", "BAR"); + lifecycle.execute(userMap); + assertEquals(1, messages.getMessages().size()); + assertEquals(Severity.ERROR, messages.getMessages("integer").get(0).getSeverity()); + assertEquals("The value 'bogus' entered into the Integer field could not be converted", messages.getMessages("integer").get(0).getText()); + } + + public static enum FooEnum { + BAR, BAZ, BOOP; + } + + public static class TestBean { + private String string; + private int integer; + private Date date; + private FooEnum foo; + private BigDecimal currency; + private List foos; + private List
addresses; + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + public int getInteger() { + return integer; + } + + public void setInteger(int integer) { + this.integer = integer; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public FooEnum getFoo() { + return foo; + } + + public void setFoo(FooEnum foo) { + this.foo = foo; + } + + @CurrencyFormat + public BigDecimal getCurrency() { + return currency; + } + + public void setCurrency(BigDecimal currency) { + this.currency = currency; + } + + public List getFoos() { + return foos; + } + + public void setFoos(List foos) { + this.foos = foos; + } + + public List
getAddresses() { + return addresses; + } + + public void setAddresses(List
addresses) { + this.addresses = addresses; + } + + } + + public static class Address { + private String street; + private String city; + private String state; + private String zip; + private String country; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZip() { + return zip; + } + + public void setZip(String zip) { + this.zip = zip; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/ui/message/MessageBuilderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/message/MessageBuilderTests.java index 7328e9e2ae1..d9273709bbb 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/message/MessageBuilderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/message/MessageBuilderTests.java @@ -12,7 +12,7 @@ public class MessageBuilderTests { @Test public void buildMessage() { - MessageResolver resolver = builder.severity(Severity.ERROR).code("invalidFormat").resolvableArg("label", "mathForm.decimalField") + MessageResolver resolver = builder.severity(Severity.ERROR).code("invalidFormat").arg("label", new ResolvableArgument("mathForm.decimalField")) .arg("format", "#,###.##").defaultText("Field must be in format #,###.##").build(); MockMessageSource messageSource = new MockMessageSource(); messageSource.addMessage("invalidFormat", Locale.US, "#{label} must be in format #{format}"); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java b/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java index 0c203309e26..474cbcd7c94 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java @@ -10,6 +10,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.ui.message.ResolvableArgument; import org.springframework.ui.message.Message; import org.springframework.ui.message.MessageBuilder; import org.springframework.ui.message.MessageResolver; @@ -37,8 +38,8 @@ public class DefaultMessageContextTests { @Test public void addMessage() { MessageBuilder builder = new MessageBuilder(); - MessageResolver message = builder.severity(Severity.ERROR).code("invalidFormat").resolvableArg("label", - "mathForm.decimalField").arg("format", "#,###.##").defaultText("Field must be in format #,###.##").build(); + MessageResolver message = builder.severity(Severity.ERROR).code("invalidFormat").arg("label", new ResolvableArgument("mathForm.decimalField")). + arg("format", "#,###.##").defaultText("Field must be in format #,###.##").build(); context.add(message, "mathForm.decimalField"); Map> messages = context.getMessages(); assertEquals(1, messages.size());