web bind and lifecycle tests; polish
git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1393 50f2f4bb-b051-0410-bef5-90022cba6387
This commit is contained in:
parent
86abbc2b59
commit
caecdb26b0
|
|
@ -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 <code>null</code> if this is not an error
|
||||
*/
|
||||
Throwable getErrorCause();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, ? extends Object> 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?
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import org.springframework.ui.format.number.CurrencyFormatter;
|
|||
public class WebBinderTests {
|
||||
|
||||
TestBean bean = new TestBean();
|
||||
|
||||
Binder binder = new WebBinder(bean);
|
||||
|
||||
@Before
|
||||
|
|
|
|||
|
|
@ -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<String, Object> userMap = new HashMap<String, Object>();
|
||||
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<String, Object> userMap = new HashMap<String, Object>();
|
||||
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<FooEnum> foos;
|
||||
private List<Address> 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<FooEnum> getFoos() {
|
||||
return foos;
|
||||
}
|
||||
|
||||
public void setFoos(List<FooEnum> foos) {
|
||||
this.foos = foos;
|
||||
}
|
||||
|
||||
public List<Address> getAddresses() {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
public void setAddresses(List<Address> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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<String, List<Message>> messages = context.getMessages();
|
||||
assertEquals(1, messages.size());
|
||||
|
|
|
|||
Loading…
Reference in New Issue