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
This commit is contained in:
Rossen Stoyanchev
2012-11-16 05:47:49 -05:00
parent 5aa2896b1d
commit 508d6a7dfb
39 changed files with 722 additions and 74 deletions

View File

@@ -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=")
}
}
}
}

View File

@@ -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());
}

View File

@@ -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");
}

View File

@@ -28,7 +28,7 @@ import org.w3c.dom.Element;
/**
* {@link BeanDefinitionParser} for the <code>&lt;flow-builder-services&gt;</code> 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)) {

View File

@@ -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

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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();
}
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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<VarModel> 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
*/

View File

@@ -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.
* <p>
* The xmlns of the XSD expected to be resolved:
*
*
* <pre>
* &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
* &lt;flow xmlns=&quot;http://www.springframework.org/schema/webflow&quot;
@@ -35,13 +35,13 @@ import org.xml.sax.SAXException;
* xsi:schemaLocation=&quot;http://www.springframework.org/schema/webflow
* http://www.springframework.org/schema/webflow/spring-webflow.xsd&quot;&gt;
* </pre>
*
*
* @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) {

View File

@@ -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));

View File

@@ -18,7 +18,7 @@
* Defines the XmlFlowModelBuilder, for building FlowModels from XML-based resources.
*
* <p>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;

View File

@@ -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;
}

View File

@@ -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.
* </p>
*
*
* @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).
* </p>
*
*
* @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.
* </p>
*
*
* @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();
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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<Class<?>> result = new ArrayList<Class<?>>();
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 + "]");
}
}

View File

@@ -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.
* <p>
@@ -74,7 +78,7 @@ public class ValidationHelper {
* <p>
* For example: <code>model.validateEnterBookingDetails(VaticationContext)</code> or
* <code>context.getBean("modelValidator").validateEnterBookingDetails(model, VaticationContext)</code>
*
*
* @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;
}
}

View File

@@ -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);
}

View File

@@ -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
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

View File

@@ -12,7 +12,7 @@
<xsd:documentation>
<![CDATA[
Spring Web Flow Configuration Schema
Authors: Keith Donald, Jeremy Grelle, Scott Andrews
Authors: Keith Donald, Jeremy Grelle, Scott Andrews, Rossen Stoyanchev
<br>
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.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="validationHintResolver">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.webflow.validation.ValidationHintResolver">
<![CDATA[
The bean name of a ValidationHintResolver used to resolve String-based validation hints.
This attribute is not required. The default implementation used is BeanValidationHintResolver.
]]>
</xsd:documentation>
</xsd:annotation>

View File

@@ -9,7 +9,7 @@
<xsd:documentation>
<![CDATA[
Spring Web Flow Schema
Authors: Keith Donald, Erwin Vervaet, Scott Andrews
Authors: Keith Donald, Erwin Vervaet, Scott Andrews, Rossen Stoyanchev
<br>
This schema defines Spring Web Flow's XML-based flow definition language.
<br>
@@ -523,6 +523,21 @@ Displays the view in a popup dialog. Default is false.
<xsd:documentation>
<![CDATA[
The model object this view is bound to. Typically used as the source of form field values or other data input controls.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="validation-hints" type="expression">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
A comma-separated list of validation hints such as validation groups against a JSR-303 provider.
Each hint is used to find an inner Class either in the Class of the model or its parent classes.
For example, given org.example.MyModel with inner type MyGroup, the hint "MyGroup" can be used.
The hint "default" is reserved as the default validation group, i.e. "javax.validation.groups.Default".
A hint can also be a fully qualified class name.
The validation hints string is evaluated as an expression before hints are resolved.
The result of the expression can be a String or an Object[] with Class instances.
]]>
</xsd:documentation>
</xsd:annotation>

View File

@@ -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");
}

View File

@@ -0,0 +1,7 @@
package org.springframework.webflow.config;
import org.springframework.webflow.validation.BeanValidationHintResolver;
public class MyBeanValidationHintResolver extends BeanValidationHintResolver {
}

View File

@@ -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">
<webflow:flow-builder-services id="flowBuilderServicesDefault"/>
@@ -15,6 +15,7 @@
view-factory-creator="customViewFactoryCreator"
conversion-service="customConversionService"
validator="customValidator"
validationHintResolver="customValidationHintResolver"
development="true" />
<webflow:flow-builder-services id="flowBuilderServicesConversionServiceCustom"
@@ -31,5 +32,7 @@
<bean id="customConversionService" class="org.springframework.webflow.config.FlowBuilderServicesBeanDefinitionParserTests$TestConversionService"/>
<bean id="customValidator" class="org.springframework.webflow.config.EmptySpringValidator" />
<bean id="customValidationHintResolver" class="org.springframework.webflow.config.MyBeanValidationHintResolver" />
</beans>

View File

@@ -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<TransitionModel> transitions = new LinkedList<TransitionModel>();
@@ -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());

View File

@@ -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,

View File

@@ -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());
}
}

View File

@@ -0,0 +1,9 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.4.xsd">
<view-state id="state1" validation-hints="foo,bar" />
<view-state id="state2" />
</flow>

View File

@@ -1,8 +1,8 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.4.xsd">
<view-state id="form" model="formObject">
<view-state id="form" model="formObject" validation-hints="foo,bar">
<binder>
<binding property="objectProperty" converter="customConverter" required="true" />
</binder>

View File

@@ -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 {
}
}
}

View File

@@ -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 {
}
}
}

View File

@@ -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;
}

View File

@@ -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)
-----------------------------------------------

View File

@@ -479,7 +479,45 @@ public class ApplicationConversionServiceFactoryBean extends FormattingConversio
In other words Web Flow will apply all available validation
mechanisms.
</para>
<sect3 id="view-validation-jsr303-partial">
<title>Partial Validation</title>
<para>
JSR-303 Bean Validation supports partial validation via groups. For example:
<programlisting language="java">
@NotNull
@Size(min = 2, max = 30, groups = State1.class)
private String name;
</programlisting>
You can specify validation hints in a view state:
<programlisting language="xml"><![CDATA[
<view-state id="state1" model="myModel" validation-hints="group1,group2">
]]>
</programlisting>
Each hint can be an inner Class either in the model type or its parent types.
For example, given <classname>org.example.MyModel</classname> with inner type
<classname>Group1</classname> and <classname>Group2</classname> 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.
<classname>javax.validation.groups.Default</classname>.
Also, the <emphasis>validation-hints</emphasis> property can be an expression that
resolves to a String or an <classname>Object[]</classname>
containing <classname>Class</classname> based hints.
</para>
<para>
Note that a custom <classname>ValidationHintResolver</classname> can be plugged if
necessary through the validationHintResolver property of the
flow-builder-services element:
<programlisting language="xml">
&lt;webflow:flow-registry flow-builder-services="flowBuilderServices" /&gt;
&lt;webflow:flow-builder-services id="flowBuilderServices" validator=".." validationHintResolver=".." /&gt;
</programlisting>
</para>
</sect3>
</sect2>
<sect2 id="view-validation-programmatic">
<title>Programmatic validation</title>

View File

@@ -29,6 +29,13 @@
Flow output can now be saved to Spring MVC Flash Scope for any <code>end-state</code> that issues an internal redirect. To enable this feature set <code>FlowHandlerAdapter.saveOutputToFlashScopeOnRedirect</code>. See <xref linkend="spring-mvc-flash-output"/>.
</para>
</sect2>
<sect2 id="whatsnew-partial-validation">
<title>Partial JSR-303 Bean Validation</title>
<para>
Partial validation with JSR-303 Bean Validation groups is now supported through the validation-hints
view-state attribute. See <xref linkend="view-validation-jsr303-partial" />.
</para>
</sect2>
</sect1>
<sect1 id="whatsnew-swf-230">
<title>Spring Web Flow 2.3</title>