From 508d6a7dfb195d96b748c4a9465a75be9c262859 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 16 Nov 2012 05:47:49 -0500 Subject: [PATCH] Add support for partial JSR-303 bean validation This change makes it possible to use validation hints in conjunction with declarative, JSR-303 validation. Such hints may be defined in an attribute of the view state where the view state id itself is also interpeted as a hint. A custom ValidationHintResolver can be plugged in if necessary. Issue: SWF-1453 --- build.gradle | 3 +- .../faces/webflow/JsfViewFactoryCreator.java | 4 +- ...lderServicesBeanDefinitionParserTests.java | 4 +- ...owBuilderServicesBeanDefinitionParser.java | 11 +- .../engine/builder/FlowBuilderContext.java | 6 + .../engine/builder/ViewFactoryCreator.java | 5 +- .../builder/model/FlowModelFlowBuilder.java | 9 +- .../model/LocalFlowBuilderContext.java | 9 ++ .../support/FlowBuilderContextImpl.java | 5 + .../builder/support/FlowBuilderServices.java | 20 ++- .../webflow/engine/model/AbstractModel.java | 4 +- .../webflow/engine/model/ViewStateModel.java | 39 +++++- .../builder/xml/WebFlowEntityResolver.java | 6 +- .../builder/xml/XmlFlowModelBuilder.java | 3 +- .../model/builder/xml/package-info.java | 2 +- .../mvc/builder/MvcViewFactoryCreator.java | 9 +- .../webflow/mvc/view/AbstractMvcView.java | 56 +++++++-- .../mvc/view/AbstractMvcViewFactory.java | 11 +- .../webflow/test/MockViewFactoryCreator.java | 8 +- .../BeanValidationHintResolver.java | 116 ++++++++++++++++++ .../webflow/validation/ValidationHelper.java | 46 +++++-- .../validation/ValidationHintResolver.java | 42 +++++++ .../main/resources/META-INF/spring.schemas | 3 +- .../config/spring-webflow-config-2.4.xsd | 12 +- .../model/builder/xml/spring-webflow-2.4.xsd | 17 ++- ...lderServicesBeanDefinitionParserTests.java | 6 +- .../config/MyBeanValidationHintResolver.java | 7 ++ .../webflow/config/flow-builder-services.xml | 5 +- .../engine/model/ViewStateModelTests.java | 2 + .../xml/WebFlowEntityResolverTests.java | 7 ++ .../builder/xml/XmlFlowModelBuilderTests.java | 14 +++ .../builder/xml/flow-validation-hints.xml | 9 ++ .../xml/flow-viewstate-model-binding.xml | 4 +- .../webflow/mvc/view/MvcViewTests.java | 89 ++++++++++++++ .../BeanValidationHintResolverTests.java | 79 ++++++++++++ .../validation/ValidationHelperTests.java | 76 ++++++++---- src/dist/changelog.txt | 1 + src/reference/views.xml | 40 +++++- src/reference/whatsnew.xml | 7 ++ 39 files changed, 722 insertions(+), 74 deletions(-) create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/validation/BeanValidationHintResolver.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHintResolver.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/config/MyBeanValidationHintResolver.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-validation-hints.xml create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/validation/BeanValidationHintResolverTests.java diff --git a/build.gradle b/build.gradle index 39723ca0..29003089 100644 --- a/build.gradle +++ b/build.gradle @@ -190,6 +190,7 @@ project('spring-webflow') { compile("org.springframework:spring-webmvc-portlet:$springVersion", optional) compile("org.springframework.security:spring-security-core:$springSecurityVersion", optional) + testCompile "javax.validation:validation-api:1.0.0.GA" testCompile "org.apache.openjpa:openjpa:1.1.0" testCompile "org.apache.openjpa:openjpa-lib:1.1.0" testCompile "org.apache.openjpa:openjpa-persistence:1.1.0" @@ -385,4 +386,4 @@ configure(rootProject) { "set GRADLE_OPTS=$gradleBatOpts %GRADLE_OPTS%\nset DEFAULT_JVM_OPTS=") } } -} \ No newline at end of file +} diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactoryCreator.java b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactoryCreator.java index 3d942ae3..3d962618 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactoryCreator.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactoryCreator.java @@ -24,6 +24,7 @@ import org.springframework.validation.Validator; import org.springframework.webflow.engine.builder.BinderConfiguration; import org.springframework.webflow.engine.builder.ViewFactoryCreator; import org.springframework.webflow.execution.ViewFactory; +import org.springframework.webflow.validation.ValidationHintResolver; /** * A {@link ViewFactoryCreator} implementation for creating instances of a JSF-specific {@link ViewFactory}. @@ -37,7 +38,8 @@ public class JsfViewFactoryCreator implements ViewFactoryCreator { private Lifecycle lifecycle; public ViewFactory createViewFactory(Expression viewIdExpression, ExpressionParser expressionParser, - ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator) { + ConversionService conversionService, BinderConfiguration binderConfiguration, + Validator validator, ValidationHintResolver resolver) { return new JsfViewFactory(viewIdExpression, getLifecycle()); } diff --git a/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java b/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java index 419b291d..df009d89 100644 --- a/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/config/FacesFlowBuilderServicesBeanDefinitionParserTests.java @@ -21,6 +21,7 @@ import org.springframework.webflow.engine.builder.ViewFactoryCreator; import org.springframework.webflow.engine.builder.support.FlowBuilderServices; import org.springframework.webflow.execution.ViewFactory; import org.springframework.webflow.expression.spel.WebFlowSpringELExpressionParser; +import org.springframework.webflow.validation.ValidationHintResolver; public class FacesFlowBuilderServicesBeanDefinitionParserTests extends TestCase { @@ -77,7 +78,8 @@ public class FacesFlowBuilderServicesBeanDefinitionParserTests extends TestCase public static class TestViewFactoryCreator implements ViewFactoryCreator { public ViewFactory createViewFactory(Expression viewIdExpression, ExpressionParser expressionParser, - ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator) { + ConversionService conversionService, BinderConfiguration binderConfiguration, + Validator validator, ValidationHintResolver validationHintResolver) { throw new UnsupportedOperationException("Auto-generated method stub"); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParser.java index d187d5dc..4bf02794 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParser.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParser.java @@ -28,7 +28,7 @@ import org.w3c.dom.Element; /** * {@link BeanDefinitionParser} for the <flow-builder-services> tag. - * + * * @author Jeremy Grelle */ class FlowBuilderServicesBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @@ -53,6 +53,7 @@ class FlowBuilderServicesBeanDefinitionParser extends AbstractSingleBeanDefiniti private static final String EXPRESSION_PARSER_PROPERTY = "expressionParser"; private static final String VIEW_FACTORY_CREATOR_PROPERTY = "viewFactoryCreator"; private static final String VALIDATOR_PROPERTY = "validator"; + private static final String VALIDATION_HINT_RESOLVER_PROPERTY = "validationHintResolver"; protected String getBeanClassName(Element element) { return FLOW_BUILDER_SERVICES_CLASS_NAME; @@ -68,6 +69,7 @@ class FlowBuilderServicesBeanDefinitionParser extends AbstractSingleBeanDefiniti parseExpressionParser(element, parserContext, builder); parseViewFactoryCreator(element, parserContext, builder); parseValidator(element, parserContext, builder); + parseValidationHintResolver(element, parserContext, builder); parseDevelopment(element, builder); parserContext.popAndRegisterContainingComponent(); @@ -115,6 +117,13 @@ class FlowBuilderServicesBeanDefinitionParser extends AbstractSingleBeanDefiniti } } + private void parseValidationHintResolver(Element element, ParserContext context, BeanDefinitionBuilder definitionBuilder) { + String resolver = element.getAttribute(VALIDATION_HINT_RESOLVER_PROPERTY); + if (StringUtils.hasText(resolver)) { + definitionBuilder.addPropertyReference(VALIDATION_HINT_RESOLVER_PROPERTY, resolver); + } + } + private void parseDevelopment(Element element, BeanDefinitionBuilder definitionBuilder) { String development = element.getAttribute(DEVELOPMENT_ATTR); if (StringUtils.hasText(development)) { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderContext.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderContext.java index 439cce0e..8cbebed6 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderContext.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderContext.java @@ -21,6 +21,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.validation.Validator; import org.springframework.webflow.core.collection.AttributeMap; import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.validation.ValidationHintResolver; /** * Provides services needed to a direct a flow builder through building a flow definition. @@ -77,6 +78,11 @@ public interface FlowBuilderContext { */ public Validator getValidator(); + /** + * Return the {@link ValidationHintResolver}. + */ + public ValidationHintResolver getValidationHintResolver(); + /** * Returns the application context hosting the flow system. * @return the application context diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/ViewFactoryCreator.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/ViewFactoryCreator.java index 5bc7480a..099b093f 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/ViewFactoryCreator.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/ViewFactoryCreator.java @@ -21,6 +21,7 @@ import org.springframework.binding.expression.ExpressionParser; import org.springframework.validation.Validator; import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.ViewFactory; +import org.springframework.webflow.validation.ValidationHintResolver; /** * A factory for ViewFactory objects. This is an SPI interface and conceals specific types of view factories from the @@ -36,10 +37,12 @@ public interface ViewFactoryCreator { * @param conversionService an optional conversion service to use to format text values * @param binderConfiguration information on how the rendered view binds to a model that provides its data * @param validator a global validator to invoke + * @param validationHintResolver a custom ValidationHintResolver to use * @return the view factory */ public ViewFactory createViewFactory(Expression viewId, ExpressionParser expressionParser, - ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator); + ConversionService conversionService, BinderConfiguration binderConfiguration, + Validator validator, ValidationHintResolver validationHintResolver); /** * Get the default id of the view to render in the provided view state by convention. diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/FlowModelFlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/FlowModelFlowBuilder.java index 3a883c89..7f8b53a2 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/FlowModelFlowBuilder.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/FlowModelFlowBuilder.java @@ -114,7 +114,7 @@ import org.springframework.webflow.security.SecurityRule; /** * Builds a runtime {@link Flow} definition object from a {@link FlowModel}. - * + * * @author Keith Donald */ public class FlowModelFlowBuilder extends AbstractFlowBuilder { @@ -535,6 +535,11 @@ public class FlowModelFlowBuilder extends AbstractFlowBuilder { getLocalContext().getExpressionParser().parseExpression(state.getModel(), new FluentParserContext().evaluate(RequestContext.class))); } + if (state.getValidationHints() != null) { + attributes.put("validationHints", + getLocalContext().getExpressionParser().parseExpression(state.getValidationHints(), + new FluentParserContext().evaluate(RequestContext.class))); + } parseAndPutSecured(state.getSecured(), attributes); getLocalContext().getFlowArtifactFactory().createViewState(state.getId(), flow, parseViewVariables(state.getVars()), parseActions(state.getOnEntryActions()), viewFactory, redirect, @@ -622,7 +627,7 @@ public class FlowModelFlowBuilder extends AbstractFlowBuilder { BinderConfiguration binderConfiguration = createBinderConfiguration(binderModel); return getLocalContext().getViewFactoryCreator().createViewFactory(viewId, getLocalContext().getExpressionParser(), getLocalContext().getConversionService(), binderConfiguration, - getLocalContext().getValidator()); + getLocalContext().getValidator(), getLocalContext().getValidationHintResolver()); } private BinderConfiguration createBinderConfiguration(BinderModel binderModel) { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/LocalFlowBuilderContext.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/LocalFlowBuilderContext.java index 357d7d70..90323cf7 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/LocalFlowBuilderContext.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/model/LocalFlowBuilderContext.java @@ -25,6 +25,7 @@ import org.springframework.webflow.definition.registry.FlowDefinitionLocator; import org.springframework.webflow.engine.builder.FlowArtifactFactory; import org.springframework.webflow.engine.builder.FlowBuilderContext; import org.springframework.webflow.engine.builder.ViewFactoryCreator; +import org.springframework.webflow.validation.ValidationHintResolver; /** * A builder context that delegates to a flow-local bean factory for builder services. Such builder services override @@ -102,4 +103,12 @@ class LocalFlowBuilderContext implements FlowBuilderContext { } } + public ValidationHintResolver getValidationHintResolver() { + if (localFlowContext.containsLocalBean("validationHintResolver")) { + return localFlowContext.getBean("validationHintResolver", ValidationHintResolver.class); + } else { + return parent.getValidationHintResolver(); + } + } + } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java index a798e1a9..859ece94 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderContextImpl.java @@ -31,6 +31,7 @@ import org.springframework.webflow.definition.registry.FlowDefinitionLocator; import org.springframework.webflow.engine.builder.FlowArtifactFactory; import org.springframework.webflow.engine.builder.FlowBuilderContext; import org.springframework.webflow.engine.builder.ViewFactoryCreator; +import org.springframework.webflow.validation.ValidationHintResolver; /** * Generic implementation of a flow builder context, suitable for use by most flow assembly systems. @@ -109,6 +110,10 @@ public class FlowBuilderContextImpl implements FlowBuilderContext { return flowBuilderServices.getValidator(); } + public ValidationHintResolver getValidationHintResolver() { + return flowBuilderServices.getValidationHintResolver(); + } + /** * Factory method that creates the conversion service the flow builder will use. Subclasses may override. The * default implementation registers Web Flow-specific converters thought to be useful for most builder diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderServices.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderServices.java index 8252f8c1..bde49e8d 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderServices.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/support/FlowBuilderServices.java @@ -28,17 +28,18 @@ import org.springframework.webflow.engine.State; import org.springframework.webflow.engine.builder.FlowArtifactFactory; import org.springframework.webflow.engine.builder.FlowBuilderContext; import org.springframework.webflow.engine.builder.ViewFactoryCreator; +import org.springframework.webflow.validation.ValidationHintResolver; /** * A simple holder for configuring the services used by flow builders. These services are exposed to a builder in a * {@link FlowBuilderContext}. - * + * * Note this class does not attempt to default any service implementations other than the {@link FlowArtifactFactory}, * which is more like builder helper objects than a service. It is expected clients inject non-null references to * concrete service implementations appropriate for their environment. - * + * * @see FlowBuilderContextImpl - * + * * @author Keith Donald */ public class FlowBuilderServices implements ApplicationContextAware, InitializingBean { @@ -72,6 +73,11 @@ public class FlowBuilderServices implements ApplicationContextAware, Initializin */ private Validator validator; + /** + * A ValidationHintResolver for resolving string based validation hints. + */ + private ValidationHintResolver validationHintResolver; + /** * The Spring application context that provides access to the services of the application. */ @@ -122,6 +128,14 @@ public class FlowBuilderServices implements ApplicationContextAware, Initializin this.validator = validator; } + public ValidationHintResolver getValidationHintResolver() { + return validationHintResolver; + } + + public void setValidationHintResolver(ValidationHintResolver validationHintResolver) { + this.validationHintResolver = validationHintResolver; + } + public boolean getDevelopment() { return development; } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractModel.java index 4ec419eb..a7c6f910 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractModel.java @@ -18,9 +18,11 @@ package org.springframework.webflow.engine.model; import java.util.Collections; import java.util.LinkedList; +import org.springframework.util.StringUtils; + /** * Contains basic merge functions that can be utilized by other models. - * + * * @author Scott Andrews */ public abstract class AbstractModel implements Model { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java index 5ae06ade..cac27999 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java @@ -22,7 +22,7 @@ import org.springframework.util.StringUtils; /** * Model support for view states. - * + * * @author Scott Andrews */ public class ViewStateModel extends AbstractTransitionableStateModel { @@ -35,6 +35,8 @@ public class ViewStateModel extends AbstractTransitionableStateModel { private String model; + private String validationHints; + private LinkedList vars; private BinderModel binder; @@ -70,11 +72,32 @@ public class ViewStateModel extends AbstractTransitionableStateModel { setRedirect(merge(getRedirect(), state.getRedirect())); setPopup(merge(getPopup(), state.getPopup())); setModel(merge(getModel(), state.getModel())); + setValidationHints(mergeValidationHints(getValidationHints(), state.getValidationHints())); setVars(merge(getVars(), state.getVars(), false)); setBinder((BinderModel) merge(getBinder(), state.getBinder())); setOnRenderActions(merge(getOnRenderActions(), state.getOnRenderActions(), false)); } + /** + * Merge validation hints by taking the union of both with the child validation hints listed first. + * @param child child validation hints + * @param parent parent validation hints + * @return the merged hints or {@code null} + */ + private String mergeValidationHints(String child, String parent) { + StringBuilder sb = new StringBuilder(); + if (StringUtils.hasText(child)) { + sb.append(child); + } + if (StringUtils.hasText(parent)) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(parent); + } + return (sb.length() > 0) ? sb.toString() : null; + } + public Model createCopy() { ViewStateModel copy = new ViewStateModel(getId()); super.fillCopy(copy); @@ -82,6 +105,7 @@ public class ViewStateModel extends AbstractTransitionableStateModel { copy.setRedirect(redirect); copy.setPopup(popup); copy.setModel(model); + copy.setValidationHints(validationHints); copy.setVars(copyList(vars)); copy.setBinder((BinderModel) copy(binder)); copy.setOnRenderActions(copyList(onRenderActions)); @@ -160,6 +184,19 @@ public class ViewStateModel extends AbstractTransitionableStateModel { } } + public String getValidationHints() { + return validationHints; + } + + public void setValidationHints(String validationHints) { + if (StringUtils.hasText(validationHints)) { + this.validationHints = validationHints; + } + else { + this.validationHints = null; + } + } + /** * @return the vars */ diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolver.java index 5bbabcdc..1dddd44b 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolver.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolver.java @@ -27,7 +27,7 @@ import org.xml.sax.SAXException; * EntityResolver implementation for the Spring Web Flow XML Schema. This will load the XSD from the classpath. *

* The xmlns of the XSD expected to be resolved: - * + * *

  *     <?xml version="1.0" encoding="UTF-8"?>
  *     <flow xmlns="http://www.springframework.org/schema/webflow"
@@ -35,13 +35,13 @@ import org.xml.sax.SAXException;
  *           xsi:schemaLocation="http://www.springframework.org/schema/webflow
  *                               http://www.springframework.org/schema/webflow/spring-webflow.xsd">
  * 
- * + * * @author Erwin Vervaet * @author Ben Hale */ class WebFlowEntityResolver implements EntityResolver { - private static final String[] WEBFLOW_VERSIONS = new String[] { "spring-webflow-2.0" }; + private static final String[] WEBFLOW_VERSIONS = new String[] { "spring-webflow-2.4", "spring-webflow-2.0" }; public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { if (systemId != null && systemId.indexOf("spring-webflow.xsd") > -1) { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java index 68de9b4e..663d7392 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java @@ -60,7 +60,7 @@ import org.xml.sax.SAXException; /** * Builds a flow model from a XML-based flow definition resource. - * + * * @author Keith Donald * @author Scott Andrews */ @@ -570,6 +570,7 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { state.setRedirect(element.getAttribute("redirect")); state.setPopup(element.getAttribute("popup")); state.setModel(element.getAttribute("model")); + state.setValidationHints(element.getAttribute("validation-hints")); state.setVars(parseVars(element)); state.setBinder(parseBinder(element)); state.setOnRenderActions(parseOnRenderActions(element)); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/package-info.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/package-info.java index f1d20c6a..ca930cde 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/package-info.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/package-info.java @@ -18,7 +18,7 @@ * Defines the XmlFlowModelBuilder, for building FlowModels from XML-based resources. * *

This package also contains the definition of the XML-based flow definition language, defined within - * {@code spring-webflow-2.0.xsd}. See this schema for a detailed description of language elements. + * {@code spring-webflow-2.4.xsd}. See this schema for a detailed description of language elements. */ package org.springframework.webflow.engine.model.builder.xml; diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/builder/MvcViewFactoryCreator.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/builder/MvcViewFactoryCreator.java index 7cebb7ec..3760f8ed 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/builder/MvcViewFactoryCreator.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/builder/MvcViewFactoryCreator.java @@ -37,6 +37,7 @@ import org.springframework.webflow.mvc.portlet.PortletMvcViewFactory; import org.springframework.webflow.mvc.servlet.ServletMvcViewFactory; import org.springframework.webflow.mvc.view.AbstractMvcViewFactory; import org.springframework.webflow.mvc.view.FlowViewResolver; +import org.springframework.webflow.validation.ValidationHintResolver; import org.springframework.webflow.validation.WebFlowMessageCodesResolver; /** @@ -49,12 +50,12 @@ import org.springframework.webflow.validation.WebFlowMessageCodesResolver; * By default, this implementation creates view factories that resolve their views by loading flow-relative resources, * such as .jsp templates located in a flow working directory. This class also supports rendering views resolved by * pre-existing Spring MVC {@link ViewResolver view resolvers}. - * + * * @see ServletMvcViewFactory * @see PortletMvcViewFactory * @see FlowResourceFlowViewResolver * @see DelegatingFlowViewResolver - * + * * @author Keith Donald * @author Scott Andrews */ @@ -170,7 +171,8 @@ public class MvcViewFactoryCreator implements ViewFactoryCreator, ApplicationCon } public ViewFactory createViewFactory(Expression viewId, ExpressionParser expressionParser, - ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator) { + ConversionService conversionService, BinderConfiguration binderConfiguration, + Validator validator, ValidationHintResolver validationHintResolver) { if (useSpringBeanBinding) { expressionParser = new BeanWrapperExpressionParser(conversionService); } @@ -183,6 +185,7 @@ public class MvcViewFactoryCreator implements ViewFactoryCreator, ApplicationCon viewFactory.setFieldMarkerPrefix(fieldMarkerPrefix); } viewFactory.setValidator(validator); + viewFactory.setValidationHintResolver(validationHintResolver); return viewFactory; } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java index e5382b6e..4a3cf3bb 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java @@ -42,6 +42,7 @@ 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; @@ -52,14 +53,17 @@ 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; +import org.springframework.webflow.validation.BeanValidationHintResolver; import org.springframework.webflow.validation.ValidationHelper; +import org.springframework.webflow.validation.ValidationHintResolver; /** * Base view implementation for the Spring Web MVC Servlet and Spring Web MVC Portlet frameworks. - * + * * @author Keith Donald */ public abstract class AbstractMvcView implements View { @@ -94,6 +98,8 @@ public abstract class AbstractMvcView implements View { private boolean userEventProcessed; + private ValidationHintResolver validationHintResolver = new BeanValidationHintResolver(); + /** * Creates a new MVC view. * @param view the Spring MVC view to render @@ -124,6 +130,12 @@ public abstract class AbstractMvcView implements View { this.validator = validator; } + public void setValidationHintResolver(ValidationHintResolver validationHintResolver) { + if (validationHintResolver != null) { + this.validationHintResolver = validationHintResolver; + } + } + /** * Sets the configuration describing how this view should bind to its model to access data for rendering. * @param binderConfiguration the model binder configuration @@ -356,7 +368,7 @@ public abstract class AbstractMvcView implements View { * considered. In the absence of binding configuration all request parameters will be used to update matching fields * on the model. *

- * + * * @param model the model to be updated * @return an instance of MappingResults with information about the results of the binding. */ @@ -380,7 +392,7 @@ public abstract class AbstractMvcView implements View { * parameter. If there is no matching incoming request parameter, a special mapping is created that will set the * target field on the model to an empty value (typically null). *

- * + * * @param mapper the mapper to which mappings will be added * @param parameterNames the request parameters * @param model the model @@ -410,7 +422,7 @@ public abstract class AbstractMvcView implements View { * converters are supported for backwards compatibility only and will not result in use of the Spring 3 type * conversion system at runtime. *

- * + * * @param mapper the mapper to add the mapping to * @param binding the binding element * @param model the model @@ -437,7 +449,7 @@ public abstract class AbstractMvcView implements View { /** * Add a {@link DefaultMapping} instance for all incoming request parameters except those having a special field * marker prefix. This method is used when binding configuration was not specified on the view. - * + * * @param mapper the mapper to add mappings to * @param parameterNames the request parameter names * @param model the model @@ -458,7 +470,7 @@ public abstract class AbstractMvcView implements View { /** * Adds a special {@link DefaultMapping} that results in setting the target field on the model to an empty value * (typically null). - * + * * @param mapper the mapper to add the mapping to * @param field the field for which a mapping is to be added * @param model the model @@ -480,7 +492,7 @@ public abstract class AbstractMvcView implements View { /** * Adds a {@link DefaultMapping} between the given request parameter name and a matching model field. - * + * * @param mapper the mapper to add the mapping to * @param parameter the request parameter name * @param model the model @@ -574,6 +586,33 @@ public abstract class AbstractMvcView implements View { return (Expression) requestContext.getCurrentState().getAttributes().get("model"); } + private Object[] getValidationHints(Object model) { + Expression expr = (Expression) requestContext.getCurrentState().getAttributes().get("validationHints"); + String flowId = requestContext.getActiveFlow().getId(); + String stateId = requestContext.getCurrentState().getId(); + if (expr != null) { + 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); + } + } + return null; + } + private Object getEmptyValue(Class fieldType) { if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) { // Special handling of boolean property. @@ -625,7 +664,8 @@ public abstract class AbstractMvcView implements View { } ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, getModelExpression() .getExpressionString(), expressionParser, messageCodesResolver, mappingResults); - helper.setValidator(validator); + helper.setValidator(this.validator); + helper.setValidationHints(getValidationHints(model)); helper.validate(); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcViewFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcViewFactory.java index 58defb58..6e3dc7a3 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcViewFactory.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcViewFactory.java @@ -25,10 +25,11 @@ import org.springframework.webflow.engine.builder.BinderConfiguration; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.ViewFactory; +import org.springframework.webflow.validation.ValidationHintResolver; /** * Base class for mvc view factories. - * + * * @author Keith Donald */ public abstract class AbstractMvcViewFactory implements ViewFactory { @@ -43,6 +44,8 @@ public abstract class AbstractMvcViewFactory implements ViewFactory { private Validator validator; + private ValidationHintResolver validationHintResolver; + private BinderConfiguration binderConfiguration; private String eventIdParameterName; @@ -82,6 +85,11 @@ public abstract class AbstractMvcViewFactory implements ViewFactory { this.validator = validator; } + public void setValidationHintResolver(ValidationHintResolver validationHintResolver) { + this.validationHintResolver = validationHintResolver; + } + + public View getView(RequestContext context) { String viewId = (String) this.viewId.getValue(context); org.springframework.web.servlet.View view = viewResolver.resolveView(viewId, context); @@ -91,6 +99,7 @@ public abstract class AbstractMvcViewFactory implements ViewFactory { mvcView.setBinderConfiguration(binderConfiguration); mvcView.setMessageCodesResolver(messageCodesResolver); mvcView.setValidator(validator); + mvcView.setValidationHintResolver(validationHintResolver); if (StringUtils.hasText(eventIdParameterName)) { mvcView.setEventIdParameterName(eventIdParameterName); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java index c2cbfd3c..85519393 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java @@ -29,17 +29,19 @@ import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.ViewFactory; +import org.springframework.webflow.validation.ValidationHintResolver; /** * A view factory creator that returns view factories that produce Mock View implementations that can be used to assert * that the correct view id was selected as part of a flow execution test. - * + * * @author Keith Donald */ class MockViewFactoryCreator implements ViewFactoryCreator { public ViewFactory createViewFactory(Expression viewId, ExpressionParser expressionParser, - ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator) { + ConversionService conversionService, BinderConfiguration binderConfiguration, + Validator validator, ValidationHintResolver resolver) { return new MockViewFactory(viewId); } @@ -72,7 +74,7 @@ class MockViewFactoryCreator implements ViewFactoryCreator { * A Mock view implementation that simply holds a reference to a identifier for a view that should be rendered. * Useful to assert that the right view was selected as part of a flow execution test, without actually exercising * any real rendering logic. - * + * * @author Keith Donald */ static class MockView implements View { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/validation/BeanValidationHintResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/validation/BeanValidationHintResolver.java new file mode 100644 index 00000000..716f9196 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/validation/BeanValidationHintResolver.java @@ -0,0 +1,116 @@ +/* + * Copyright 2008-2012 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.webflow.validation; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.webflow.execution.FlowExecutionException; + +/** + * A JSR-303 (Bean Validation) implementation of {@link ValidationHintResolver} + * that resolves String-based hints to a {@code Class} array. + * + * @author Rossen Stoyanchev + * @since 2.4 + */ +public class BeanValidationHintResolver implements ValidationHintResolver { + + /** + * Resolve each hint as a fully qualified class name or the name of an inner + * {@code Class} in the model type or the model or its parent types. + * + * @param model the model object + * @param flowId the current flow id + * @param stateId the current view state id + * @param hints the hints to resolve + * + * @return the resolved hints or {@code null} + * @throws FlowExecutionException if a hint is unresolved + * + * @see #handleUnresolvedHint(Object, String, String, String) + */ + public Class[] resolveValidationHints(Object model, String flowId, String stateId, String[] hints) + throws FlowExecutionException { + + if (ObjectUtils.isEmpty(hints)) { + return null; + } + + List> result = new ArrayList>(); + for (String hint : hints) { + if (hint.equalsIgnoreCase("Default")) { + hint = "javax.validation.groups.Default"; + } + Class resolvedHint = toClass(hint); + if ((resolvedHint == null) && (model != null)) { + resolvedHint = findInnerClass(model.getClass(), StringUtils.capitalize(hint)); + } + if (resolvedHint == null) { + resolvedHint = handleUnresolvedHint(model, flowId, stateId, hint); + } + if (resolvedHint != null) { + result.add(resolvedHint); + } + } + + return result.toArray(new Class[result.size()]); + } + + private Class toClass(String hint) { + try { + return Class.forName(hint); + } + catch (ClassNotFoundException e) { + // Ignore + } + return null; + } + + private Class findInnerClass(Class targetClass, String hint) { + try { + return Class.forName(targetClass.getName() + "$" + hint); + } + catch (ClassNotFoundException e) { + Class superClass = targetClass.getSuperclass(); + if (superClass != null) { + return findInnerClass(superClass, hint); + } + } + return null; + } + + /** + * Invoked when a hint could not be resolved. This implementation raises a + * {@link FlowExecutionException}. + * + * @param model the model object that will be validated using the hints + * @param flowId the current flow id + * @param stateId the current state id + * @param hint the hint + * @return + * + * @throws FlowExecutionException + */ + protected Class handleUnresolvedHint(Object model, String flowId, String stateId, String hint) + throws FlowExecutionException { + + throw new FlowExecutionException(flowId, stateId, "Failed to resolve validation hint [" + hint + "]"); + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHelper.java b/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHelper.java index 41ae1ab0..2268f964 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHelper.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2008 the original author or authors. + * Copyright 2008-2012 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. @@ -35,12 +35,13 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.validation.Errors; import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; import org.springframework.webflow.execution.RequestContext; /** * A helper class the encapsulates conventions to invoke validation logic. - * + * * @author Scott Andrews * @author Canny Duck * @author Jeremy Grelle @@ -65,6 +66,9 @@ public class ValidationHelper { private Validator validator; + private Object[] validationHints; + + /** * Create a throwaway validation helper object. Validation is invoked by the {@link #validate()} method. *

@@ -74,7 +78,7 @@ public class ValidationHelper { *

* For example: model.validateEnterBookingDetails(VaticationContext) or * context.getBean("modelValidator").validateEnterBookingDetails(model, VaticationContext) - * + * * @param model the object to validate * @param requestContext the context for the request * @param eventId the event triggering validation @@ -102,17 +106,24 @@ public class ValidationHelper { this.validator = validator; } + /** + * Provide validation hints such as validation groups to use against a JSR-303 provider. + */ + public void setValidationHints(Object[] validationHints) { + this.validationHints = validationHints; + } + /** * Invoke the validators available by convention. */ public void validate() { if (this.validator != null) { - invokeValidatorDefaultValidateMethod(model, this.validator); + invokeValidatorDefaultValidateMethod(this.validator); } invokeModelValidationMethod(model); Object modelValidator = getModelValidator(); if (modelValidator != null) { - invokeModelValidator(model, modelValidator); + invokeModelValidator(modelValidator); } } @@ -191,12 +202,12 @@ public class ValidationHelper { return null; } - private void invokeModelValidator(Object model, Object validator) { - invokeValidatorValidateMethodForCurrentState(model, validator); - invokeValidatorDefaultValidateMethod(model, validator); + private void invokeModelValidator(Object validator) { + invokeValidatorValidateMethodForCurrentState(validator); + invokeValidatorDefaultValidateMethod(validator); } - private boolean invokeValidatorValidateMethodForCurrentState(Object model, Object validator) { + private boolean invokeValidatorValidateMethodForCurrentState(Object validator) { String methodName = "validate" + StringUtils.capitalize(requestContext.getCurrentState().getId()); // preferred Method validateMethod = findValidationMethod(model, validator, methodName, ValidationContext.class); @@ -233,7 +244,7 @@ public class ValidationHelper { return false; } - private boolean invokeValidatorDefaultValidateMethod(Object model, Object validator) { + private boolean invokeValidatorDefaultValidateMethod(Object validator) { if (validator instanceof Validator) { // Spring Framework Validator type Validator springValidator = (Validator) validator; @@ -243,7 +254,19 @@ public class ValidationHelper { if (springValidator.supports(model.getClass())) { MessageContextErrors errors = new MessageContextErrors(requestContext.getMessageContext(), modelName, model, expressionParser, messageCodesResolver, mappingResults); - springValidator.validate(model, errors); + + if (this.validationHints != null) { + if (springValidator instanceof SmartValidator) { + ((SmartValidator) springValidator).validate(model, errors, this.validationHints); + } + else { + logger.warn("Validation hints provided but validator not an instance of SmartValidator: [" + + springValidator.getClass().getName() + "]"); + } + } + else { + springValidator.validate(model, errors); + } } else { if (logger.isDebugEnabled()) { logger.debug("Spring Validator '" + ClassUtils.getShortName(validator.getClass()) @@ -295,4 +318,5 @@ public class ValidationHelper { } return null; } + } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHintResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHintResolver.java new file mode 100644 index 00000000..3d8ea900 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/validation/ValidationHintResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008-2012 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.webflow.validation; + +import org.springframework.webflow.execution.FlowExecutionException; + +/** + * A strategy for resolving String-based hints to Objects such as validation + * groups against a JSR-303 provider. + * + * @author Rossen Stoyanchev + * @since 2.4 + */ +public interface ValidationHintResolver { + + /** + * Resolve the given String hints. Implementations may raise a + * {@link FlowExecutionException} if a hint cannot be resolved. + * + * @param model the model object that will be validated using the hints + * @param flowId the current flow id + * @param stateId the current state id + * @param hints the hints to resolve + * + * @return an array of resolved hints + */ + Object[] resolveValidationHints(Object model, String flowId, String stateId, String[] hints); + +} diff --git a/spring-webflow/src/main/resources/META-INF/spring.schemas b/spring-webflow/src/main/resources/META-INF/spring.schemas index d7334c4c..dae657a7 100644 --- a/spring-webflow/src/main/resources/META-INF/spring.schemas +++ b/spring-webflow/src/main/resources/META-INF/spring.schemas @@ -1,3 +1,4 @@ http\://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd=org/springframework/webflow/config/spring-webflow-config-2.0.xsd http\://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd=org/springframework/webflow/config/spring-webflow-config-2.3.xsd -http\://www.springframework.org/schema/webflow-config/spring-webflow-config.xsd=org/springframework/webflow/config/spring-webflow-config-2.3.xsd \ No newline at end of file +http\://www.springframework.org/schema/webflow-config/spring-webflow-config-2.4.xsd=org/springframework/webflow/config/spring-webflow-config-2.4.xsd +http\://www.springframework.org/schema/webflow-config/spring-webflow-config.xsd=org/springframework/webflow/config/spring-webflow-config-2.4.xsd \ No newline at end of file diff --git a/spring-webflow/src/main/resources/org/springframework/webflow/config/spring-webflow-config-2.4.xsd b/spring-webflow/src/main/resources/org/springframework/webflow/config/spring-webflow-config-2.4.xsd index 04fb8040..321cd0b1 100644 --- a/spring-webflow/src/main/resources/org/springframework/webflow/config/spring-webflow-config-2.4.xsd +++ b/spring-webflow/src/main/resources/org/springframework/webflow/config/spring-webflow-config-2.4.xsd @@ -12,7 +12,7 @@ A XML-based DSL for configuring the Spring Web Flow 2.0 system. ]]> @@ -232,6 +232,16 @@ The custom ViewFactoryCreator implementation to use produce ViewFactories capabl The bean name of the Validator that is to be used for validating a model declared on a view state. This attribute is not required, and only needs to be specified explicitly if a custom Validator needs to be configured. If not specified, JSR-303 validation will be installed if a JSR-303 provider is present on the classpath. +]]> + + + + + + + diff --git a/spring-webflow/src/main/resources/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.4.xsd b/spring-webflow/src/main/resources/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.4.xsd index 1d9d7163..c7786941 100644 --- a/spring-webflow/src/main/resources/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.4.xsd +++ b/spring-webflow/src/main/resources/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.4.xsd @@ -9,7 +9,7 @@ This schema defines Spring Web Flow's XML-based flow definition language.
@@ -523,6 +523,21 @@ Displays the view in a popup dialog. Default is false. + + +
+ + + + diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java b/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java index d2ac2b35..393049b0 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/FlowBuilderServicesBeanDefinitionParserTests.java @@ -20,6 +20,7 @@ import org.springframework.webflow.engine.builder.ViewFactoryCreator; import org.springframework.webflow.engine.builder.support.FlowBuilderServices; import org.springframework.webflow.execution.ViewFactory; import org.springframework.webflow.mvc.builder.MvcViewFactoryCreator; +import org.springframework.webflow.validation.ValidationHintResolver; public class FlowBuilderServicesBeanDefinitionParserTests extends TestCase { @@ -47,6 +48,7 @@ public class FlowBuilderServicesBeanDefinitionParserTests extends TestCase { assertTrue(builderServices.getViewFactoryCreator() instanceof TestViewFactoryCreator); assertTrue(builderServices.getConversionService() instanceof TestConversionService); assertTrue(builderServices.getValidator() instanceof EmptySpringValidator); + assertTrue(builderServices.getValidationHintResolver() instanceof MyBeanValidationHintResolver); assertTrue(builderServices.getDevelopment()); } @@ -58,13 +60,15 @@ public class FlowBuilderServicesBeanDefinitionParserTests extends TestCase { assertTrue(((SpringELExpressionParser) builderServices.getExpressionParser()).getConversionService() instanceof TestConversionService); assertTrue(builderServices.getViewFactoryCreator() instanceof MvcViewFactoryCreator); assertNull(builderServices.getValidator()); + assertNull(builderServices.getValidationHintResolver()); assertFalse(builderServices.getDevelopment()); } public static class TestViewFactoryCreator implements ViewFactoryCreator { public ViewFactory createViewFactory(Expression viewIdExpression, ExpressionParser expressionParser, - ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator) { + ConversionService conversionService, BinderConfiguration binderConfiguration, + Validator validator, ValidationHintResolver validationHintResolver) { throw new UnsupportedOperationException("Auto-generated method stub"); } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/MyBeanValidationHintResolver.java b/spring-webflow/src/test/java/org/springframework/webflow/config/MyBeanValidationHintResolver.java new file mode 100644 index 00000000..fc640661 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/MyBeanValidationHintResolver.java @@ -0,0 +1,7 @@ +package org.springframework.webflow.config; + +import org.springframework.webflow.validation.BeanValidationHintResolver; + +public class MyBeanValidationHintResolver extends BeanValidationHintResolver { + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/flow-builder-services.xml b/spring-webflow/src/test/java/org/springframework/webflow/config/flow-builder-services.xml index 3c4eaac3..9d5578e9 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/config/flow-builder-services.xml +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/flow-builder-services.xml @@ -6,7 +6,7 @@ http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/webflow-config - http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd"> + http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.4.xsd"> @@ -15,6 +15,7 @@ view-factory-creator="customViewFactoryCreator" conversion-service="customConversionService" validator="customValidator" + validationHintResolver="customValidationHintResolver" development="true" /> + + diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/ViewStateModelTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/ViewStateModelTests.java index a60e47cc..ea1f9055 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/ViewStateModelTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/ViewStateModelTests.java @@ -59,6 +59,7 @@ public class ViewStateModelTests extends TestCase { parent.setRedirect("true"); parent.setPopup("true"); parent.setModel("fooModel"); + parent.setValidationHints("foo"); parent.setView("fooView"); LinkedList transitions = new LinkedList(); @@ -89,6 +90,7 @@ public class ViewStateModelTests extends TestCase { assertEquals("true", child.getRedirect()); assertEquals("true", child.getPopup()); assertEquals("fooModel", child.getModel()); + assertEquals("foo", child.getValidationHints()); assertEquals("fooView", child.getView()); assertEquals("bar", child.getAttributes().get(0).getValue()); assertEquals("foo", child.getBinder().getBindings().get(0).getProperty()); diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolverTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolverTests.java index 9207405f..52d312b4 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolverTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/WebFlowEntityResolverTests.java @@ -8,6 +8,13 @@ public class WebFlowEntityResolverTests extends TestCase { private static final String PUBLIC_ID = "http://www.springframework.org/schema/webflow"; + public void testResolve24() throws Exception { + WebFlowEntityResolver resolver = new WebFlowEntityResolver(); + InputSource source = resolver.resolveEntity(PUBLIC_ID, + "http://www.springframework.org/schema/webflow/spring-webflow-2.4.xsd"); + assertNotNull(source); + } + public void testResolve20() throws Exception { WebFlowEntityResolver resolver = new WebFlowEntityResolver(); InputSource source = resolver.resolveEntity(PUBLIC_ID, diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java index 143daa21..c424e3a2 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java @@ -146,6 +146,7 @@ public class XmlFlowModelBuilderTests extends TestCase { FlowModel flow = builder.getFlowModel(); ViewStateModel model = (ViewStateModel) flow.getStates().get(0); assertEquals("formObject", model.getModel()); + assertEquals("foo,bar", model.getValidationHints()); assertEquals("objectProperty", model.getBinder().getBindings().get(0).getProperty()); assertEquals("customConverter", model.getBinder().getBindings().get(0).getConverter()); } @@ -325,4 +326,17 @@ public class XmlFlowModelBuilderTests extends TestCase { execution.resume(context); assertTrue(((TestBeanValidator) action.getValidator()).getInvoked()); } + + public void testParseFlowValidationHints() { + ClassPathResource res = new ClassPathResource("flow-validation-hints.xml", getClass()); + XmlFlowModelBuilder builder = new XmlFlowModelBuilder(res); + DefaultFlowModelHolder holder = new DefaultFlowModelHolder(builder); + FlowModel model = holder.getFlowModel(); + + ViewStateModel state = (ViewStateModel) model.getStateById("state1"); + assertEquals("foo,bar", state.getValidationHints()); + + state = (ViewStateModel) model.getStateById("state2"); + assertNull(state.getValidationHints()); + } } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-validation-hints.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-validation-hints.xml new file mode 100644 index 00000000..002f9544 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-validation-hints.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-viewstate-model-binding.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-viewstate-model-binding.xml index 02e72465..d0ca9a5f 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-viewstate-model-binding.xml +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-viewstate-model-binding.xml @@ -1,8 +1,8 @@ + xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.4.xsd"> - + diff --git a/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/MvcViewTests.java b/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/MvcViewTests.java index 841e2ffb..d51c3c4f 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/MvcViewTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/mvc/view/MvcViewTests.java @@ -30,6 +30,8 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockServletContext; import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.SmartValidator; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.View; import org.springframework.webflow.action.ViewFactoryActionAdapter; @@ -628,6 +630,66 @@ public class MvcViewTests extends TestCase { assertFalse(bindBean.validationMethodInvoked); } + public void testResumeEventStringValidationHint() throws Exception { + StubSmartValidator validator = new StubSmartValidator(); + MockRequestContext context = new MockRequestContext(); + context.putRequestParameter("_eventId", "submit"); + TestModel testModel = new TestModel(); + StaticExpression validationHintsExpression = new StaticExpression("State1,AllStates"); + context.getCurrentState().getAttributes().put("validationHints", validationHintsExpression); + StaticExpression modelExpression = new StaticExpression(testModel); + modelExpression.setExpressionString("testModel"); + context.getCurrentState().getAttributes().put("model", modelExpression); + context.getFlowScope().put("testModel", testModel); + context.getMockExternalContext().setNativeContext(new MockServletContext()); + context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest()); + context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse()); + context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1")); + org.springframework.web.servlet.View mvcView = new MockView(); + AbstractMvcView view = new MockMvcView(mvcView, context); + view.setValidator(validator); + view.setExpressionParser(createExpressionParser()); + + view.processUserEvent(); + + assertFalse(view.userEventQueued()); + assertTrue(view.hasFlowEvent()); + assertEquals("submit", view.getFlowEvent().getId()); + assertEquals(TestModel.State1.class, validator.hints[0]); + assertEquals(TestModel.AllStates.class, validator.hints[1]); + assertTrue(validator.invoked); + } + + public void testResumeEventObjectArrayValidationHint() throws Exception { + StubSmartValidator validator = new StubSmartValidator(); + MockRequestContext context = new MockRequestContext(); + context.putRequestParameter("_eventId", "submit"); + TestModel testModel = new TestModel(); + Object[] validationHints = new Object[] { TestModel.State1.class }; + StaticExpression validationHintsExpression = new StaticExpression(validationHints); + context.getCurrentState().getAttributes().put("validationHints", validationHintsExpression); + StaticExpression modelExpression = new StaticExpression(testModel); + modelExpression.setExpressionString("testModel"); + context.getCurrentState().getAttributes().put("model", modelExpression); + context.getFlowScope().put("testModel", testModel); + context.getMockExternalContext().setNativeContext(new MockServletContext()); + context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest()); + context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse()); + context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1")); + org.springframework.web.servlet.View mvcView = new MockView(); + AbstractMvcView view = new MockMvcView(mvcView, context); + view.setValidator(validator); + view.setExpressionParser(createExpressionParser()); + + view.processUserEvent(); + + assertFalse(view.userEventQueued()); + assertTrue(view.hasFlowEvent()); + assertEquals("submit", view.getFlowEvent().getId()); + assertEquals(validationHints, validator.hints); + assertTrue(validator.invoked); + } + private SpringELExpressionParser createExpressionParser() { StringToDate c = new StringToDate(); c.setPattern("yyyy-MM-dd"); @@ -806,4 +868,31 @@ public class MvcViewTests extends TestCase { } } + public static class StubSmartValidator implements SmartValidator { + private boolean invoked; + private Object[] hints; + + public void validate(Object object, Errors errors) { + invoked = true; + } + + public void validate(Object object, Errors errors, Object... hints) { + invoked = true; + this.hints = hints; + } + + public boolean supports(Class clazz) { + return true; + } + } + + private static class TestModel { + + public static class State1 { + } + + public static class AllStates { + } + } + } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/validation/BeanValidationHintResolverTests.java b/spring-webflow/src/test/java/org/springframework/webflow/validation/BeanValidationHintResolverTests.java new file mode 100644 index 00000000..97f7815b --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/validation/BeanValidationHintResolverTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2008-2012 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.webflow.validation; + +import javax.validation.groups.Default; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionException; + +/** + * Test fixture for {@link BeanValidationHintResolver}; + * + * @author Rossen Stoyanchev + */ +public class BeanValidationHintResolverTests extends TestCase { + + private BeanValidationHintResolver resolver; + + public void setUp() { + this.resolver = new BeanValidationHintResolver(); + } + + public void testResolveFullyQualifiedClassNameHint() { + String[] hints = new String[] { this.getClass().getName() }; + Class[] resolvedHints = this.resolver.resolveValidationHints(null, "flowId", "state1", hints); + + assertNotNull(resolvedHints); + assertEquals(1, resolvedHints.length); + assertEquals(this.getClass(), resolvedHints[0]); + } + + public void testResolveInnterTypeHints() { + String[] hints = new String[] {"default", "state1", "state2"}; + Class[] resolvedHints = this.resolver.resolveValidationHints(new TestModel(), "flowId", "state1", hints); + + assertNotNull(resolvedHints); + assertEquals(3, resolvedHints.length); + assertEquals(Default.class, resolvedHints[0]); + assertEquals(BaseTestModel.State1.class, resolvedHints[1]); + assertEquals(TestModel.State2.class, resolvedHints[2]); + } + + public void testResolveHintNoMatch() { + try { + this.resolver.resolveValidationHints(null, "flowId", "state1", new String[] { "foo" }); + fail("Expected exception"); + } + catch (FlowExecutionException ex) { + // expected + } + } + + public static class BaseTestModel { + + private static class State1 { + } + } + + public static class TestModel extends BaseTestModel { + + private static class State2 { + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/validation/ValidationHelperTests.java b/spring-webflow/src/test/java/org/springframework/webflow/validation/ValidationHelperTests.java index e45e2d60..de88e673 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/validation/ValidationHelperTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/validation/ValidationHelperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008 the original author or authors. + * Copyright 2008-2012 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. @@ -22,7 +22,7 @@ import org.springframework.binding.validation.ValidationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.Errors; -import org.springframework.validation.Validator; +import org.springframework.validation.SmartValidator; import org.springframework.webflow.engine.Flow; import org.springframework.webflow.engine.StubViewFactory; import org.springframework.webflow.engine.ViewState; @@ -39,10 +39,14 @@ public class ValidationHelperTests extends TestCase { private String modelName; + private DefaultMessageCodesResolver codesResolver; + + protected void setUp() throws Exception { requestContext = new MockRequestControlContext(); eventId = "userEvent"; modelName = "model"; + codesResolver = new DefaultMessageCodesResolver(); } public void testValidateWithMessageContext() { @@ -58,7 +62,7 @@ public class ValidationHelperTests extends TestCase { public void testValidateWithValidationContext() { Object model = new StubModelValidationContext(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); helper.validate(); MessageContext messages = requestContext.getMessageContext(); assertEquals(1, messages.getAllMessages().length); @@ -70,7 +74,7 @@ public class ValidationHelperTests extends TestCase { applicationContext.registerSingleton("modelValidator", StubModelMessageContext.class); ((Flow) requestContext.getActiveFlow()).setApplicationContext(applicationContext); ValidationHelper helper = new ValidationHelper(new Object(), requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); helper.validate(); MessageContext messages = requestContext.getMessageContext(); assertEquals(1, messages.getAllMessages().length); @@ -82,7 +86,7 @@ public class ValidationHelperTests extends TestCase { applicationContext.registerSingleton("modelValidator", StubModelValidationContext.class); ((Flow) requestContext.getActiveFlow()).setApplicationContext(applicationContext); ValidationHelper helper = new ValidationHelper(new Object(), requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); helper.validate(); MessageContext messages = requestContext.getMessageContext(); assertEquals(1, messages.getAllMessages().length); @@ -94,7 +98,7 @@ public class ValidationHelperTests extends TestCase { applicationContext.registerSingleton("modelValidator", StubModelErrors.class); ((Flow) requestContext.getActiveFlow()).setApplicationContext(applicationContext); ValidationHelper helper = new ValidationHelper(new Object(), requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); helper.validate(); MessageContext messages = requestContext.getMessageContext(); assertEquals(1, messages.getAllMessages().length); @@ -106,7 +110,7 @@ public class ValidationHelperTests extends TestCase { applicationContext.registerSingleton("modelValidator", StubModelErrorsOverridden.class); ((Flow) requestContext.getActiveFlow()).setApplicationContext(applicationContext); ValidationHelper helper = new ValidationHelper(new Object(), requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); helper.validate(); MessageContext messages = requestContext.getMessageContext(); assertEquals(1, messages.getAllMessages().length); @@ -116,7 +120,7 @@ public class ValidationHelperTests extends TestCase { public void testStateAndFallbackModelValidationMethodInvoked() { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -127,7 +131,7 @@ public class ValidationHelperTests extends TestCase { public void testFallbackModelValidationMethodInvoked() { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -138,7 +142,7 @@ public class ValidationHelperTests extends TestCase { public void testStateAndFallbackErrorsModelValidationMethodInvoked() { ErrorsModel model = new ErrorsModel(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -149,7 +153,7 @@ public class ValidationHelperTests extends TestCase { public void testFallbackModelErrorsValidationMethodInvoked() { ErrorsModel model = new ErrorsModel(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -165,7 +169,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -181,7 +185,7 @@ public class ValidationHelperTests extends TestCase { ExtendedModel model = new ExtendedModel(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -197,7 +201,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -213,7 +217,7 @@ public class ValidationHelperTests extends TestCase { ExtendedModel model = new ExtendedModel(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -229,7 +233,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -245,7 +249,7 @@ public class ValidationHelperTests extends TestCase { ExtendedModel model = new ExtendedModel(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -261,7 +265,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -277,7 +281,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -293,7 +297,7 @@ public class ValidationHelperTests extends TestCase { ExtendedModel model = new ExtendedModel(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state1", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -309,7 +313,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -325,7 +329,7 @@ public class ValidationHelperTests extends TestCase { Model model = new Model(); ValidationHelper helper = new ValidationHelper(model, requestContext, eventId, modelName, null, - new DefaultMessageCodesResolver(), null); + this.codesResolver, null); ViewState state1 = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); requestContext.setCurrentState(state1); helper.validate(); @@ -333,6 +337,24 @@ public class ValidationHelperTests extends TestCase { assertTrue(validator.fallbackInvoked); } + public void testSmartValidatorWithStateIdHint() { + 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 }); + + ViewState state = new ViewState(requestContext.getRootFlow(), "state2", new StubViewFactory()); + requestContext.setCurrentState(state); + + helper.validate(); + + assertTrue(validator.fallbackInvoked); + assertTrue(validator.hints.length > 0); + assertEquals(Model.State1.class, validator.hints[0]); + } + + public static class Model { private boolean state1Invoked; private boolean fallbackInvoked; @@ -344,6 +366,8 @@ public class ValidationHelperTests extends TestCase { public void validate(ValidationContext context) { fallbackInvoked = true; } + + private static class State1 {} } public static class ExtendedModel extends Model { @@ -362,9 +386,10 @@ public class ValidationHelperTests extends TestCase { } } - public static class LegacyModelValidator implements Validator { + public static class LegacyModelValidator implements SmartValidator { private boolean state1Invoked; private boolean fallbackInvoked; + private Object[] hints; public void validateState1(Model model, Errors errors) { state1Invoked = true; @@ -374,6 +399,11 @@ public class ValidationHelperTests extends TestCase { fallbackInvoked = true; } + public void validate(Object object, Errors errors, Object... hints) { + fallbackInvoked = true; + this.hints = hints; + } + public boolean supports(Class clazz) { return true; } diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 9cce9b3b..21788b0c 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -7,6 +7,7 @@ Changes in version 2.4.0.RELEASE Move samples to a separate repository https://github.com/SpringSource/spring-webflow-samples Upgrade spring-faces to JSF 2 as a minimum requirement Upgrade portlet/JSF support to function in JSF 2 environment +Add partial validation support for JSR-303 bean validation Changes in version 2.3.1.RELEASE (Mar 27, 2012) ----------------------------------------------- diff --git a/src/reference/views.xml b/src/reference/views.xml index 06dc15dd..501ab786 100644 --- a/src/reference/views.xml +++ b/src/reference/views.xml @@ -479,7 +479,45 @@ public class ApplicationConversionServiceFactoryBean extends FormattingConversio In other words Web Flow will apply all available validation mechanisms. - + + + Partial Validation + + JSR-303 Bean Validation supports partial validation via groups. For example: + + +@NotNull +@Size(min = 2, max = 30, groups = State1.class) +private String name; + + You can specify validation hints in a view state: + +]]> + + Each hint can be an inner Class either in the model type or its parent types. + For example, given org.example.MyModel with inner type + Group1 and Group2 you can + specify the hints "group1", "group2" or both "group1,group2". + A hint can also be a fully qualified class name. + The hint "default" indicates the default validation group, i.e. + javax.validation.groups.Default. + Also, the validation-hints property can be an expression that + resolves to a String or an Object[] + containing Class based hints. + + + Note that a custom ValidationHintResolver can be plugged if + necessary through the validationHintResolver property of the + flow-builder-services element: + +<webflow:flow-registry flow-builder-services="flowBuilderServices" /> + +<webflow:flow-builder-services id="flowBuilderServices" validator=".." validationHintResolver=".." /> + + + + Programmatic validation diff --git a/src/reference/whatsnew.xml b/src/reference/whatsnew.xml index 9488e437..a2544779 100644 --- a/src/reference/whatsnew.xml +++ b/src/reference/whatsnew.xml @@ -29,6 +29,13 @@ Flow output can now be saved to Spring MVC Flash Scope for any end-state that issues an internal redirect. To enable this feature set FlowHandlerAdapter.saveOutputToFlashScopeOnRedirect. See . + + Partial JSR-303 Bean Validation + + Partial validation with JSR-303 Bean Validation groups is now supported through the validation-hints + view-state attribute. See . + + Spring Web Flow 2.3