Support validation hints in FlowActionListener

Issue: SWF-1626
This commit is contained in:
Rossen Stoyanchev
2014-03-31 09:42:01 -04:00
parent 76430e3e9f
commit a7035acc2d
4 changed files with 127 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2004-2012 the original author or authors.
* Copyright 2004-2014 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.
@@ -34,18 +34,20 @@ import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.RequestContextHolder;
import org.springframework.webflow.execution.View;
import org.springframework.webflow.validation.BeanValidationHintResolver;
import org.springframework.webflow.validation.ValidationHelper;
import org.springframework.webflow.validation.ValidationHintResolver;
import org.springframework.webflow.validation.WebFlowMessageCodesResolver;
/**
* The default {@link ActionListener} implementation to be used with Web Flow.
*
*
* This implementation bypasses the JSF {@link NavigationHandler} mechanism to instead let the event be handled directly
* by Web Flow.
* <p>
* Web Flow's model-level validation will be invoked here after an event has been detected if the event is not an
* immediate event.
*
*
* @author Jeremy Grelle
*/
public class FlowActionListener implements ActionListener {
@@ -58,10 +60,21 @@ public class FlowActionListener implements ActionListener {
private final MessageCodesResolver messageCodesResolver = new WebFlowMessageCodesResolver();
private ValidationHintResolver validationHintResolver = new BeanValidationHintResolver();
public FlowActionListener(ActionListener delegate) {
this.delegate = delegate;
}
public void setValidationHintResolver(ValidationHintResolver validationHintResolver) {
this.validationHintResolver = validationHintResolver;
}
public ValidationHintResolver getValidationHintResolver() {
return validationHintResolver;
}
public void processAction(ActionEvent actionEvent) throws AbortProcessingException {
if (!JsfUtils.isFlowRequest()) {
this.delegate.processAction(actionEvent);
@@ -151,7 +164,11 @@ public class FlowActionListener implements ActionListener {
}
private void validate(RequestContext requestContext, Object model, String eventId) {
new ValidationHelper(model, requestContext, eventId, getModelExpression(requestContext).getExpressionString(),
null, this.messageCodesResolver, null).validate();
String modelName = getModelExpression(requestContext).getExpressionString();
new ValidationHelper(model, requestContext,
eventId, modelName, null, this.messageCodesResolver, null, this.validationHintResolver).validate();
}
}

View File

@@ -42,7 +42,6 @@ import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageResolver;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
@@ -53,7 +52,6 @@ import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.engine.builder.BinderConfiguration;
import org.springframework.webflow.engine.builder.BinderConfiguration.Binding;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.View;
@@ -593,39 +591,6 @@ public abstract class AbstractMvcView implements View {
return (Expression) requestContext.getCurrentState().getAttributes().get("model");
}
private Object[] getValidationHints(Object model, TransitionDefinition transition) {
Expression expr = null;
if (transition != null) {
expr = (Expression) transition.getAttributes().get("validationHints");
}
if (expr == null) {
expr = (Expression) requestContext.getCurrentState().getAttributes().get("validationHints");
}
if (expr == null) {
return null;
}
String flowId = requestContext.getActiveFlow().getId();
String stateId = requestContext.getCurrentState().getId();
try {
Object hintsValue = expr.getValue(requestContext);
if (hintsValue instanceof String) {
String[] hints = StringUtils.commaDelimitedListToStringArray((String) hintsValue);
return validationHintResolver.resolveValidationHints(model, flowId, stateId, hints);
}
else if (hintsValue instanceof Object[]) {
return (Object[]) hintsValue;
}
else {
throw new FlowExecutionException(flowId, stateId,
"Failed to resolve validation hints [" + hintsValue + "]");
}
}
catch (EvaluationException e) {
throw new FlowExecutionException(flowId, stateId,
"Failed to resolve validation hints expression [" + expr + "]", e);
}
}
private Object getEmptyValue(Class<?> fieldType) {
if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) {
// Special handling of boolean property.
@@ -676,9 +641,8 @@ public abstract class AbstractMvcView implements View {
logger.debug("Validating model");
}
ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, getModelExpression()
.getExpressionString(), expressionParser, messageCodesResolver, mappingResults);
.getExpressionString(), expressionParser, messageCodesResolver, mappingResults, validationHintResolver);
helper.setValidator(this.validator);
helper.setValidationHints(getValidationHints(model, transition));
helper.validate();
}

View File

@@ -24,6 +24,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.binding.expression.EvaluationException;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.mapping.MappingResults;
import org.springframework.binding.message.MessageContext;
@@ -37,6 +39,8 @@ import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.RequestContext;
/**
@@ -86,8 +90,10 @@ public class ValidationHelper {
* @param expressionParser the expression parser
* @param mappingResults object mapping results
*/
public ValidationHelper(Object model, RequestContext requestContext, String eventId, String modelName,
ExpressionParser expressionParser, MessageCodesResolver messageCodesResolver, MappingResults mappingResults) {
public ValidationHelper(Object model, RequestContext requestContext, String eventId,
String modelName, ExpressionParser expressionParser, MessageCodesResolver messageCodesResolver,
MappingResults mappingResults, ValidationHintResolver hintResolver) {
Assert.notNull(model, "The model to validate is required");
Assert.notNull(requestContext, "The request context for the validator is required");
this.model = model;
@@ -97,6 +103,55 @@ public class ValidationHelper {
this.expressionParser = expressionParser;
this.messageCodesResolver = messageCodesResolver;
this.mappingResults = mappingResults;
this.validationHints = initValidationHints(model, requestContext, eventId, hintResolver);
}
/**
* An alternative constructor that creates an instance of {@link BeanValidationHintResolver}.
*/
public ValidationHelper(Object model, RequestContext requestContext, String eventId,
String modelName, ExpressionParser expressionParser, MessageCodesResolver messageCodesResolver,
MappingResults mappingResults) {
this(model, requestContext, eventId, modelName, expressionParser,
messageCodesResolver, mappingResults, new BeanValidationHintResolver());
}
private static Object[] initValidationHints(Object model, RequestContext requestContext, String eventId,
ValidationHintResolver hintResolver) {
Expression expr = null;
TransitionDefinition transition = requestContext.getMatchingTransition(eventId);
if (transition != null) {
expr = (Expression) transition.getAttributes().get("validationHints");
}
if (expr == null) {
expr = (Expression) requestContext.getCurrentState().getAttributes().get("validationHints");
}
if (expr == null) {
return null;
}
String flowId = requestContext.getActiveFlow().getId();
String stateId = requestContext.getCurrentState().getId();
try {
Object hintsValue = expr.getValue(requestContext);
if (hintsValue instanceof String) {
String[] hints = StringUtils.commaDelimitedListToStringArray((String) hintsValue);
return hintResolver.resolveValidationHints(model, flowId, stateId, hints);
}
else if (hintsValue instanceof Object[]) {
return (Object[]) hintsValue;
}
else {
throw new FlowExecutionException(flowId, stateId,
"Failed to resolve validation hints [" + hintsValue + "]");
}
}
catch (EvaluationException e) {
throw new FlowExecutionException(flowId, stateId,
"Failed to resolve validation hints expression [" + expr + "]", e);
}
}
/**
@@ -107,7 +162,9 @@ public class ValidationHelper {
}
/**
* Provide validation hints such as validation groups to use against a JSR-303 provider.
* Provide validation hints (e.g. JSR-303 validation groups). Note that the constructor
* automatically detects validation hints specified on a view state or a transition.
* Therefore use of this method should not be needed.
*/
public void setValidationHints(Object[] validationHints) {
this.validationHints = validationHints;

View File

@@ -17,6 +17,7 @@ package org.springframework.webflow.validation;
import junit.framework.TestCase;
import org.springframework.binding.expression.support.StaticExpression;
import org.springframework.binding.message.MessageContext;
import org.springframework.binding.validation.ValidationContext;
import org.springframework.context.support.StaticApplicationContext;
@@ -25,7 +26,9 @@ import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.StubViewFactory;
import org.springframework.webflow.engine.Transition;
import org.springframework.webflow.engine.ViewState;
import org.springframework.webflow.engine.support.DefaultTransitionCriteria;
import org.springframework.webflow.test.MockRequestControlContext;
/**
@@ -337,16 +340,54 @@ public class ValidationHelperTests extends TestCase {
assertTrue(validator.fallbackInvoked);
}
public void testSmartValidatorWithStateIdHint() {
public void testSmartValidatorWithClassHint() {
ViewState state = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory());
state.getAttributes().put("validationHints", new StaticExpression(new Object[] { Model.State1.class }));
requestContext.setCurrentState(state);
LegacyModelValidator validator = new LegacyModelValidator();
ExtendedModel model = new ExtendedModel();
ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, codesResolver, null);
helper.setValidator(validator);
helper.setValidationHints(new Object[] { Model.State1.class });
helper.validate();
assertTrue(validator.fallbackInvoked);
assertTrue(validator.hints.length > 0);
assertEquals(Model.State1.class, validator.hints[0]);
}
public void testSmartValidatorWithHintResolution() {
ViewState state = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory());
state.getAttributes().put("validationHints", new StaticExpression("State1"));
requestContext.setCurrentState(state);
LegacyModelValidator validator = new LegacyModelValidator();
ExtendedModel model = new ExtendedModel();
ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, codesResolver, null);
helper.setValidator(validator);
helper.validate();
assertTrue(validator.fallbackInvoked);
assertTrue(validator.hints.length > 0);
assertEquals(Model.State1.class, validator.hints[0]);
}
public void testSmartValidatorWithHintOnTransition() {
Transition transition = new Transition();
transition.setMatchingCriteria(new DefaultTransitionCriteria(new StaticExpression(eventId)));
transition.getAttributes().put("validationHints", new StaticExpression("State1"));
ViewState state = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory());
state.getTransitionSet().add(transition);
requestContext.setCurrentState(state);
LegacyModelValidator validator = new LegacyModelValidator();
ExtendedModel model = new ExtendedModel();
ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, codesResolver, null);
helper.setValidator(validator);
helper.validate();
assertTrue(validator.fallbackInvoked);
@@ -367,7 +408,7 @@ public class ValidationHelperTests extends TestCase {
fallbackInvoked = true;
}
private static class State1 {}
private static interface State1 {}
}
public static class ExtendedModel extends Model {