Browse Source

web bind and lifecycle tests; polish

conversation
Keith Donald 16 years ago
parent
commit
9368e76ffc
  1. 9
      org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java
  2. 32
      org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java
  3. 10
      org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java
  4. 11
      org.springframework.context/src/main/java/org/springframework/ui/message/DefaultMessageResolver.java
  5. 44
      org.springframework.context/src/main/java/org/springframework/ui/message/MessageBuilder.java
  6. 56
      org.springframework.context/src/main/java/org/springframework/ui/message/ResolvableArgument.java
  7. 1
      org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java
  8. 178
      org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java
  9. 2
      org.springframework.context/src/test/java/org/springframework/ui/message/MessageBuilderTests.java
  10. 5
      org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java

9
org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java

@ -35,13 +35,18 @@ public interface BindingResult { @@ -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();

32
org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java

@ -31,9 +31,12 @@ import java.util.Map; @@ -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 { @@ -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,14 +503,31 @@ public class GenericBinder implements Binder { @@ -496,14 +503,31 @@ 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() {
@ -538,6 +562,10 @@ public class GenericBinder implements Binder { @@ -538,6 +562,10 @@ public class GenericBinder implements Binder {
return null;
}
public String getErrorMessage() {
return null;
}
public Throwable getErrorCause() {
return null;
}

10
org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java

@ -21,9 +21,11 @@ import org.springframework.ui.binding.BindingResult; @@ -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 { @@ -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 { @@ -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 { @@ -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?

11
org.springframework.context/src/main/java/org/springframework/ui/message/DefaultMessageResolver.java

@ -20,6 +20,7 @@ import java.util.Map; @@ -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 @@ -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 @@ -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 @@ -114,6 +120,7 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso
}
@SuppressWarnings("unchecked")
static class MessageArgumentAccessor implements PropertyAccessor {
private MessageSource messageSource;

44
org.springframework.context/src/main/java/org/springframework/ui/message/MessageBuilder.java

@ -21,8 +21,6 @@ import java.util.Map; @@ -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; @@ -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 { @@ -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 { @@ -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();
}
}
}

56
org.springframework.context/src/main/java/org/springframework/ui/message/ResolvableArgument.java

@ -0,0 +1,56 @@ @@ -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();
}
}

1
org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java

@ -25,6 +25,7 @@ import org.springframework.ui.format.number.CurrencyFormatter; @@ -25,6 +25,7 @@ import org.springframework.ui.format.number.CurrencyFormatter;
public class WebBinderTests {
TestBean bean = new TestBean();
Binder binder = new WebBinder(bean);
@Before

178
org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java

@ -0,0 +1,178 @@ @@ -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;
}
}
}

2
org.springframework.context/src/test/java/org/springframework/ui/message/MessageBuilderTests.java

@ -12,7 +12,7 @@ public class MessageBuilderTests { @@ -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}");

5
org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java

@ -10,6 +10,7 @@ import org.junit.After; @@ -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 { @@ -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…
Cancel
Save