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());