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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -34,7 +34,9 @@ import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.RequestContextHolder; import org.springframework.webflow.execution.RequestContextHolder;
import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.View;
import org.springframework.webflow.validation.BeanValidationHintResolver;
import org.springframework.webflow.validation.ValidationHelper; import org.springframework.webflow.validation.ValidationHelper;
import org.springframework.webflow.validation.ValidationHintResolver;
import org.springframework.webflow.validation.WebFlowMessageCodesResolver; import org.springframework.webflow.validation.WebFlowMessageCodesResolver;
/** /**
@@ -58,10 +60,21 @@ public class FlowActionListener implements ActionListener {
private final MessageCodesResolver messageCodesResolver = new WebFlowMessageCodesResolver(); private final MessageCodesResolver messageCodesResolver = new WebFlowMessageCodesResolver();
private ValidationHintResolver validationHintResolver = new BeanValidationHintResolver();
public FlowActionListener(ActionListener delegate) { public FlowActionListener(ActionListener delegate) {
this.delegate = delegate; this.delegate = delegate;
} }
public void setValidationHintResolver(ValidationHintResolver validationHintResolver) {
this.validationHintResolver = validationHintResolver;
}
public ValidationHintResolver getValidationHintResolver() {
return validationHintResolver;
}
public void processAction(ActionEvent actionEvent) throws AbortProcessingException { public void processAction(ActionEvent actionEvent) throws AbortProcessingException {
if (!JsfUtils.isFlowRequest()) { if (!JsfUtils.isFlowRequest()) {
this.delegate.processAction(actionEvent); this.delegate.processAction(actionEvent);
@@ -151,7 +164,11 @@ public class FlowActionListener implements ActionListener {
} }
private void validate(RequestContext requestContext, Object model, String eventId) { 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.binding.message.MessageResolver;
import org.springframework.core.style.ToStringCreator; import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator; 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;
import org.springframework.webflow.engine.builder.BinderConfiguration.Binding; import org.springframework.webflow.engine.builder.BinderConfiguration.Binding;
import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.FlowExecutionKey; import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.View;
@@ -593,39 +591,6 @@ public abstract class AbstractMvcView implements View {
return (Expression) requestContext.getCurrentState().getAttributes().get("model"); 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) { private Object getEmptyValue(Class<?> fieldType) {
if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) { if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) {
// Special handling of boolean property. // Special handling of boolean property.
@@ -676,9 +641,8 @@ public abstract class AbstractMvcView implements View {
logger.debug("Validating model"); logger.debug("Validating model");
} }
ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, getModelExpression() ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, getModelExpression()
.getExpressionString(), expressionParser, messageCodesResolver, mappingResults); .getExpressionString(), expressionParser, messageCodesResolver, mappingResults, validationHintResolver);
helper.setValidator(this.validator); helper.setValidator(this.validator);
helper.setValidationHints(getValidationHints(model, transition));
helper.validate(); helper.validate();
} }

View File

@@ -24,6 +24,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory; 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.expression.ExpressionParser;
import org.springframework.binding.mapping.MappingResults; import org.springframework.binding.mapping.MappingResults;
import org.springframework.binding.message.MessageContext; import org.springframework.binding.message.MessageContext;
@@ -37,6 +39,8 @@ import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.SmartValidator; import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContext;
/** /**
@@ -86,8 +90,10 @@ public class ValidationHelper {
* @param expressionParser the expression parser * @param expressionParser the expression parser
* @param mappingResults object mapping results * @param mappingResults object mapping results
*/ */
public ValidationHelper(Object model, RequestContext requestContext, String eventId, String modelName, public ValidationHelper(Object model, RequestContext requestContext, String eventId,
ExpressionParser expressionParser, MessageCodesResolver messageCodesResolver, MappingResults mappingResults) { String modelName, ExpressionParser expressionParser, MessageCodesResolver messageCodesResolver,
MappingResults mappingResults, ValidationHintResolver hintResolver) {
Assert.notNull(model, "The model to validate is required"); Assert.notNull(model, "The model to validate is required");
Assert.notNull(requestContext, "The request context for the validator is required"); Assert.notNull(requestContext, "The request context for the validator is required");
this.model = model; this.model = model;
@@ -97,6 +103,55 @@ public class ValidationHelper {
this.expressionParser = expressionParser; this.expressionParser = expressionParser;
this.messageCodesResolver = messageCodesResolver; this.messageCodesResolver = messageCodesResolver;
this.mappingResults = mappingResults; 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) { public void setValidationHints(Object[] validationHints) {
this.validationHints = validationHints; this.validationHints = validationHints;

View File

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