diff --git a/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageContext.java b/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageContext.java index f770eee8..b74306e0 100644 --- a/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageContext.java +++ b/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageContext.java @@ -52,9 +52,8 @@ class DefaultMessageContext implements StateManageableMessageContext { public Message[] getAllMessages() { List messages = new ArrayList(); - Iterator it = objectMessages.keySet().iterator(); - while (it.hasNext()) { - messages.addAll((List) objectMessages.get(it.next())); + for (Iterator it = objectMessages.values().iterator(); it.hasNext();) { + messages.addAll((List) it.next()); } return (Message[]) messages.toArray(new Message[messages.size()]); } @@ -80,6 +79,20 @@ class DefaultMessageContext implements StateManageableMessageContext { return (Message[]) messages.toArray(new Message[messages.size()]); } + public boolean hasErrorMessages() { + Iterator it = objectMessages.values().iterator(); + while (it.hasNext()) { + List sourceMessages = (List) it.next(); + for (Iterator it2 = sourceMessages.iterator(); it2.hasNext();) { + Message message = (Message) it2.next(); + if (message.getSeverity() == Severity.ERROR) { + return true; + } + } + } + return false; + } + public void addMessage(MessageResolver messageResolver) { Locale currentLocale = LocaleContextHolder.getLocale(); Message message = messageResolver.resolveMessage(messageSource, currentLocale); diff --git a/spring-binding/src/main/java/org/springframework/binding/message/MessageBuilder.java b/spring-binding/src/main/java/org/springframework/binding/message/MessageBuilder.java index 2690b6f6..bc6596e8 100644 --- a/spring-binding/src/main/java/org/springframework/binding/message/MessageBuilder.java +++ b/spring-binding/src/main/java/org/springframework/binding/message/MessageBuilder.java @@ -1,6 +1,7 @@ package org.springframework.binding.message; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -84,6 +85,18 @@ public class MessageBuilder { return this; } + /** + * Records that the message being built should try and resolve its text using the codes provided. Adds the codes to + * the codes list. Successive calls to this method add additional codes. Codes are applied in the order they are + * added. + * @param codes the message codes + * @return this, for fluent API usage + */ + public MessageBuilder codes(String[] codes) { + this.codes.add(Arrays.asList(codes)); + return this; + } + /** * Records that the message being built has a variable argument. Adds the arg to the args list. Successive calls to * this method add additional args. Args are applied in the order they are added. @@ -95,6 +108,17 @@ public class MessageBuilder { return this; } + /** + * Records that the message being built has variable arguments. Adds the args to the args list. Successive calls to + * this method add additional args. Args are applied in the order they are added. + * @param arg the message argument value + * @return this, for fluent API usage + */ + public MessageBuilder args(Object[] args) { + this.args.add(Arrays.asList(args)); + return this; + } + /** * Records that the message being built has a variable argument, whose display value is also * {@link MessageSourceResolvable}. Adds the arg to the args list. Successive calls to this method add additional @@ -107,6 +131,20 @@ public class MessageBuilder { return this; } + /** + * Records that the message being built has variable arguments, whose display values are also + * {@link MessageSourceResolvable} instances. Adds the args to the args list. Successive calls to this method add + * additional resolvable args. Args are applied in the order they are added. + * @param args the resolvable message arguments + * @return this, for fluent API usage + */ + public MessageBuilder resolvableArgs(Object[] args) { + for (int i = 0; i < args.length; i++) { + this.args.add(new ResolvableArgument(args[i])); + } + return this; + } + /** * Records the fallback text of the message being built. If the message has no codes, this will always be used as * the text. If the message has codes but none can be resolved, this will alway be used as the text. diff --git a/spring-binding/src/main/java/org/springframework/binding/message/MessageContext.java b/spring-binding/src/main/java/org/springframework/binding/message/MessageContext.java index 8dee7754..a96ec1c6 100644 --- a/spring-binding/src/main/java/org/springframework/binding/message/MessageContext.java +++ b/spring-binding/src/main/java/org/springframework/binding/message/MessageContext.java @@ -24,6 +24,12 @@ public interface MessageContext { */ public Message[] getMessagesByCriteria(MessageCriteria criteria); + /** + * Returns true if there are error messages in this context. + * @return error messages + */ + public boolean hasErrorMessages(); + /** * Add a new message to this context. * @param messageResolver the resolver that will resolve the message to be added diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/FlowFacesContextTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/FlowFacesContextTests.java index f5ede8a5..82299f7b 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/FlowFacesContextTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/FlowFacesContextTests.java @@ -118,6 +118,10 @@ public class FlowFacesContextTests extends TestCase { throw new UnsupportedOperationException("Auto-generated method stub"); } + public boolean hasErrorMessages() { + throw new UnsupportedOperationException("Auto-generated method stub"); + } + public void clearMessages() { throw new UnsupportedOperationException("Auto-generated method stub"); } @@ -150,6 +154,10 @@ public class FlowFacesContextTests extends TestCase { throw new UnsupportedOperationException("Auto-generated method stub"); } + public boolean hasErrorMessages() { + throw new UnsupportedOperationException("Auto-generated method stub"); + } + public void clearMessages() { throw new UnsupportedOperationException("Auto-generated method stub"); } diff --git a/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java b/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java index 69d5674b..3dca77b6 100755 --- a/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java +++ b/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java @@ -178,7 +178,7 @@ public class Booking implements Serializable { this.creditCardExpiryYear = creditCardExpiryYear; } - public boolean validate(MessageContext context) { + public boolean validateEnterBookingDetails(MessageContext context) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -1); boolean valid = true; diff --git a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java index c81873d9..c69681e4 100755 --- a/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java +++ b/spring-webflow-samples/booking-mvc/src/main/java/org/springframework/webflow/samples/booking/Booking.java @@ -176,7 +176,7 @@ public class Booking implements Serializable { this.creditCardExpiryYear = creditCardExpiryYear; } - public boolean validate(MessageContext context) { + public boolean validateEnterBookingDetails(MessageContext context) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -1); boolean valid = true; diff --git a/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/hotels/booking/booking.xml b/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/hotels/booking/booking.xml index d05a068d..787b8821 100644 --- a/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/hotels/booking/booking.xml +++ b/spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/hotels/booking/booking.xml @@ -14,9 +14,7 @@ - - - + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MessageContextErrors.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MessageContextErrors.java new file mode 100644 index 00000000..0d588c5f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MessageContextErrors.java @@ -0,0 +1,137 @@ +package org.springframework.webflow.mvc.view; + +import java.util.List; + +import org.springframework.binding.message.MessageBuilder; +import org.springframework.binding.message.MessageContext; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +public class MessageContextErrors implements Errors { + + private MessageContext messageContext; + + public MessageContextErrors(MessageContext messageContext) { + this.messageContext = messageContext; + } + + public void reject(String errorCode) { + messageContext.addMessage(new MessageBuilder().error().code(errorCode).build()); + } + + public void reject(String errorCode, String defaultMessage) { + messageContext.addMessage(new MessageBuilder().error().code(errorCode).defaultText(defaultMessage).build()); + } + + public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { + messageContext.addMessage(new MessageBuilder().error().code(errorCode).defaultText(defaultMessage).build()); + } + + public void rejectValue(String field, String errorCode) { + messageContext.addMessage(new MessageBuilder().error().source(field).code(errorCode).build()); + } + + public void rejectValue(String field, String errorCode, String defaultMessage) { + messageContext.addMessage(new MessageBuilder().error().source(field).code(errorCode) + .defaultText(defaultMessage).build()); + } + + public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { + messageContext.addMessage(new MessageBuilder().error().source(field).code(errorCode).args(errorArgs) + .defaultText(defaultMessage).build()); + } + + public void addAllErrors(Errors errors) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public List getAllErrors() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public int getErrorCount() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public FieldError getFieldError() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public FieldError getFieldError(String field) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public int getFieldErrorCount() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public int getFieldErrorCount(String field) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public List getFieldErrors() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public List getFieldErrors(String field) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public Class getFieldType(String field) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public Object getFieldValue(String field) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public ObjectError getGlobalError() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public int getGlobalErrorCount() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public List getGlobalErrors() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public String getNestedPath() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public String getObjectName() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public boolean hasErrors() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public boolean hasFieldErrors() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public boolean hasFieldErrors(String field) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public boolean hasGlobalErrors() { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public void popNestedPath() throws IllegalStateException { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public void pushNestedPath(String subPath) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + + public void setNestedPath(String nestedPath) { + throw new UnsupportedOperationException("Should not be called by a validator"); + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MvcView.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MvcView.java index 23997307..ec35a4e3 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MvcView.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/MvcView.java @@ -16,6 +16,7 @@ package org.springframework.webflow.mvc.view; import java.io.IOException; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -24,6 +25,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.BeanFactory; import org.springframework.binding.collection.MapAdaptable; import org.springframework.binding.convert.ConversionException; import org.springframework.binding.convert.ConversionExecutor; @@ -41,8 +43,12 @@ import org.springframework.binding.mapping.impl.DefaultMapper; import org.springframework.binding.mapping.impl.DefaultMapping; import org.springframework.binding.mapping.impl.DefaultMappingContext; import org.springframework.binding.message.MessageBuilder; +import org.springframework.binding.message.MessageContext; import org.springframework.binding.message.MessageResolver; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; import org.springframework.webflow.core.collection.ParameterMap; import org.springframework.webflow.definition.TransitionDefinition; import org.springframework.webflow.definition.TransitionableStateDefinition; @@ -115,9 +121,14 @@ class MvcView implements View { } if (shouldBind(model)) { mappingResults = bind(model); - if (mappingResults.hasErrorResults() && !onlyPropertyNotFoundErrorsPresent(mappingResults)) { + if (hasMappingErrors(mappingResults)) { viewErrors = true; addErrorMessages(mappingResults); + } else { + validate(model); + if (context.getMessageContext().hasErrorMessages()) { + viewErrors = true; + } } } } @@ -191,6 +202,10 @@ class MvcView implements View { } } + private boolean hasMappingErrors(MappingResults results) { + return results.hasErrorResults() && !onlyPropertyNotFoundErrorsPresent(results); + } + private boolean onlyPropertyNotFoundErrorsPresent(MappingResults results) { return results.getResults(PROPERTY_NOT_FOUND_ERROR).size() == mappingResults.getErrorResults().size(); } @@ -212,6 +227,33 @@ class MvcView implements View { .defaultText(errorCode + " on " + field).build(); } + private void validate(Object model) { + String validateMethodName = "validate" + StringUtils.capitalize(context.getCurrentState().getId()); + Method validateMethod = ReflectionUtils.findMethod(model.getClass(), validateMethodName, + new Class[] { MessageContext.class }); + if (validateMethod != null) { + ReflectionUtils.invokeMethod(validateMethod, model, new Object[] { context.getMessageContext() }); + } + BeanFactory beanFactory = context.getActiveFlow().getBeanFactory(); + String validatorName = getModelExpression().getExpressionString() + "Validator"; + if (beanFactory.containsBean(validatorName)) { + Object validator = beanFactory.getBean(validatorName); + validateMethod = ReflectionUtils.findMethod(validator.getClass(), validateMethodName, new Class[] { + model.getClass(), MessageContext.class }); + if (validateMethod != null) { + ReflectionUtils.invokeMethod(validateMethod, validator, new Object[] { model, + context.getMessageContext() }); + } else { + validateMethod = ReflectionUtils.findMethod(validator.getClass(), validateMethodName, new Class[] { + model.getClass(), Errors.class }); + if (validateMethod != null) { + ReflectionUtils.invokeMethod(validateMethod, validator, new Object[] { model, + new MessageContextErrors(context.getMessageContext()) }); + } + } + } + } + private void determineEventId(RequestContext context) { eventId = findParameter("_eventId", context.getRequestParameters()); }