diff --git a/stripes-jsp/src/main/java/org/stripesframework/jsp/tag/BeanFirstPopulationStrategy.java b/stripes-jsp/src/main/java/org/stripesframework/jsp/tag/BeanFirstPopulationStrategy.java index ed6313962..a4efb9697 100644 --- a/stripes-jsp/src/main/java/org/stripesframework/jsp/tag/BeanFirstPopulationStrategy.java +++ b/stripes-jsp/src/main/java/org/stripesframework/jsp/tag/BeanFirstPopulationStrategy.java @@ -14,12 +14,16 @@ */ package org.stripesframework.jsp.tag; +import java.util.Map; + +import org.stripesframework.jsp.exception.StripesJspException; import org.stripesframework.web.action.ActionBean; +import org.stripesframework.web.controller.ParameterName; import org.stripesframework.web.controller.StripesConstants; -import org.stripesframework.jsp.exception.StripesJspException; import org.stripesframework.web.util.Log; import org.stripesframework.web.util.bean.BeanUtil; import org.stripesframework.web.util.bean.ExpressionException; +import org.stripesframework.web.validation.ValidationMetadata; /** @@ -61,7 +65,19 @@ public Object getValue( InputTagSupport tag ) throws StripesJspException { boolean kaboom = false; if ( bean != null ) { try { - value = BeanUtil.getPropertyValue(tag.getName(), bean); + String propertyName = tag.getName(); + + Map validationInfos = getConfiguration().getValidationMetadataProvider().getValidationMetadata(bean.getClass()); + ValidationMetadata validationInfo = validationInfos.get(new ParameterName(propertyName).getStrippedName()); + + String fullPropertyName; + if ( validationInfo != null && validationInfo.rootBinding() ) { + fullPropertyName = validationInfo.bindingPrefix() + propertyName; + } else { + fullPropertyName = propertyName; + } + + value = BeanUtil.getPropertyValue(fullPropertyName, bean); } catch ( ExpressionException ee ) { if ( !StripesConstants.SPECIAL_URL_KEYS.contains(tag.getName()) ) { diff --git a/stripes-web/src/main/java/org/stripesframework/web/action/SingleBeanForm.java b/stripes-web/src/main/java/org/stripesframework/web/action/SingleBeanForm.java new file mode 100644 index 000000000..6bab711fd --- /dev/null +++ b/stripes-web/src/main/java/org/stripesframework/web/action/SingleBeanForm.java @@ -0,0 +1,8 @@ +package org.stripesframework.web.action; + +public interface SingleBeanForm extends ActionBean { + + T getBean(); + + void setBean( T initial ); +} diff --git a/stripes-web/src/main/java/org/stripesframework/web/controller/DefaultActionBeanPropertyBinder.java b/stripes-web/src/main/java/org/stripesframework/web/controller/DefaultActionBeanPropertyBinder.java index b855f0dc0..eee4382cc 100644 --- a/stripes-web/src/main/java/org/stripesframework/web/controller/DefaultActionBeanPropertyBinder.java +++ b/stripes-web/src/main/java/org/stripesframework/web/controller/DefaultActionBeanPropertyBinder.java @@ -124,9 +124,17 @@ public ValidationErrors bind( ActionBean bean, ActionBeanContext context, boolea // Determine the target type ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); + + String fullPropertyName; + if ( validationInfo != null && validationInfo.rootBinding() ) { + fullPropertyName = validationInfo.bindingPrefix() + pname; + } else { + fullPropertyName = pname; + } + PropertyExpressionEvaluation eval; try { - eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(pname), bean); + eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(fullPropertyName), bean); } catch ( Exception e ) { if ( pname.equals(context.getEventName()) ) { diff --git a/stripes-web/src/main/java/org/stripesframework/web/controller/DispatcherHelper.java b/stripes-web/src/main/java/org/stripesframework/web/controller/DispatcherHelper.java index 86cb38405..d9baaf8c0 100644 --- a/stripes-web/src/main/java/org/stripesframework/web/controller/DispatcherHelper.java +++ b/stripes-web/src/main/java/org/stripesframework/web/controller/DispatcherHelper.java @@ -15,14 +15,17 @@ package org.stripesframework.web.controller; import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.WeakHashMap; @@ -34,14 +37,19 @@ import org.stripesframework.web.action.DontBind; import org.stripesframework.web.action.DontValidate; import org.stripesframework.web.action.Resolution; +import org.stripesframework.web.action.SingleBeanForm; import org.stripesframework.web.config.Configuration; import org.stripesframework.web.exception.StripesServletException; import org.stripesframework.web.util.CollectionUtil; import org.stripesframework.web.util.HtmlUtil; import org.stripesframework.web.util.Log; +import org.stripesframework.web.util.bean.BeanUtil; +import org.stripesframework.web.validation.FormValidation; +import org.stripesframework.web.validation.ValidateForm; import org.stripesframework.web.validation.ValidationError; import org.stripesframework.web.validation.ValidationErrorHandler; import org.stripesframework.web.validation.ValidationErrors; +import org.stripesframework.web.validation.ValidationMetadata; import org.stripesframework.web.validation.ValidationMethod; import org.stripesframework.web.validation.ValidationState; @@ -152,19 +160,51 @@ public static Resolution doCustomValidation( final ExecutionContext ctx, final b public Resolution intercept( ExecutionContext context ) throws Exception { // Run any of the annotated validation methods Method[] validations = findCustomValidationMethods(bean.getClass()); - for ( Method validation : validations ) { - ValidationMethod ann = validation.getAnnotation(ValidationMethod.class); - - boolean run = (ann.when() == ValidationState.ALWAYS) || (ann.when() == ValidationState.DEFAULT && alwaysInvokeValidate) || errors.isEmpty(); - - if ( run && applies(ann, ctx.getActionBeanContext().getEventName()) ) { - Class[] args = validation.getParameterTypes(); - if ( args.length == 1 && args[0].equals(ValidationErrors.class) ) { - validation.invoke(bean, errors); - } else { - validation.invoke(bean); - } + doCustomValidation(ctx, bean, validations, alwaysInvokeValidate, errors); + + Map forms = context.getForms(); + for ( Map.Entry formEntry : forms.entrySet() ) { + FormValidation formValidation = formEntry.getValue(); + SingleBeanForm form = formValidation.getForm(); + + Set on = formValidation.getOn(); + if ( on != null && !CollectionUtil.applies(on.toArray(new String[0]), ctx.getActionBeanContext().getEventName()) ) { + continue; + } + + //noinspection unchecked,rawtypes + ((SingleBeanForm)form).setBean(BeanUtil.getPropertyValue(formEntry.getKey(), bean)); + + Method[] formValidations = findCustomValidationMethods(form.getClass()); + doCustomValidation(ctx, form, formValidations, alwaysInvokeValidate, errors); + } + + Map validationMetadata = StripesFilter.getConfiguration() + .getValidationMetadataProvider() + .getValidationMetadata(bean.getClass()); + + for ( Map.Entry validationMetadataEntry : validationMetadata.entrySet() ) { + ValidationMetadata formValidation = validationMetadataEntry.getValue(); + Class> formClass = formValidation.form(); + if ( formClass == null ) { + continue; } + if ( formClass != ValidateForm.AnyForm.class ) { + continue; + } + + Object form = BeanUtil.getPropertyValue(validationMetadataEntry.getKey(), bean); + if ( form == null ) { + continue; + } + + Set on = formValidation.on(); + if ( on != null && !CollectionUtil.applies(on.toArray(new String[0]), ctx.getActionBeanContext().getEventName()) ) { + continue; + } + + Method[] formValidations = findCustomValidationMethods(form.getClass()); + doCustomValidation(ctx, form, formValidations, alwaysInvokeValidate, errors); } fillInValidationErrors(ctx); @@ -269,7 +309,7 @@ public static void fillInValidationErrors( ExecutionContext ctx ) { * @return a Method[] containing all methods marked as custom validations. May return * an empty array, but never null. */ - public static Method[] findCustomValidationMethods( Class type ) throws Exception { + public static Method[] findCustomValidationMethods( Class type ) throws Exception { Method[] validations = null; WeakReference ref = customValidations.get(type); if ( ref != null ) { @@ -445,8 +485,11 @@ public Resolution intercept( ExecutionContext ctx ) throws Exception { // Look up the ActionBean and set it on the context ActionBeanContext context = ctx.getActionBeanContext(); ActionBean bean = StripesFilter.getConfiguration().getActionResolver().getActionBean(context); + ctx.setActionBean(bean); + Class beanClass = bean.getClass(); + // Prefer the context from the resolved bean if it differs from the ExecutionContext if ( context != bean.getContext() ) { ActionBeanContext other = bean.getContext(); @@ -458,6 +501,31 @@ public Resolution intercept( ExecutionContext ctx ) throws Exception { ctx.setActionBeanContext(context); } + Map validationMetadata = StripesFilter.getConfiguration() + .getValidationMetadataProvider() + .getValidationMetadata(beanClass); + + Map additionalForms = new HashMap<>(); + for ( Map.Entry validationMetadataEntry : validationMetadata.entrySet() ) { + Class> formClass = validationMetadataEntry.getValue().form(); + if ( formClass == null ) { + continue; + } + if ( formClass == ValidateForm.AnyForm.class ) { + continue; + } + + SingleBeanForm form = StripesFilter.getConfiguration().getObjectFactory().newInstance(formClass); + form.setContext(context); + + FormValidation formValidation = new FormValidation(); + formValidation.setForm(form); + formValidation.setOn(validationMetadataEntry.getValue().on()); + + additionalForms.put(validationMetadataEntry.getKey(), formValidation); + } + ctx.setForms(additionalForms); + // Then register it in the Request as THE ActionBean for this request HttpServletRequest request = context.getRequest(); request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, bean); @@ -516,4 +584,22 @@ public Resolution intercept( ExecutionContext ctx ) throws Exception { } }); } + + private static void doCustomValidation( ExecutionContext ctx, Object bean, Method[] validations, boolean alwaysInvokeValidate, ValidationErrors errors ) + throws IllegalAccessException, InvocationTargetException { + for ( Method validation : validations ) { + ValidationMethod ann = validation.getAnnotation(ValidationMethod.class); + + boolean run = (ann.when() == ValidationState.ALWAYS) || (ann.when() == ValidationState.DEFAULT && alwaysInvokeValidate) || errors.isEmpty(); + + if ( run && applies(ann, ctx.getActionBeanContext().getEventName()) ) { + Class[] args = validation.getParameterTypes(); + if ( args.length == 1 && args[0].equals(ValidationErrors.class) ) { + validation.invoke(bean, errors); + } else { + validation.invoke(bean); + } + } + } + } } diff --git a/stripes-web/src/main/java/org/stripesframework/web/controller/ExecutionContext.java b/stripes-web/src/main/java/org/stripesframework/web/controller/ExecutionContext.java index 89802964e..d2fe084a4 100644 --- a/stripes-web/src/main/java/org/stripesframework/web/controller/ExecutionContext.java +++ b/stripes-web/src/main/java/org/stripesframework/web/controller/ExecutionContext.java @@ -17,11 +17,13 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Iterator; +import java.util.Map; import org.stripesframework.web.action.ActionBean; import org.stripesframework.web.action.ActionBeanContext; import org.stripesframework.web.action.Resolution; import org.stripesframework.web.util.Log; +import org.stripesframework.web.validation.FormValidation; /** @@ -47,15 +49,16 @@ public static ExecutionContext currentContext() { return currentContext.get(); } - private Collection _interceptors; - private Iterator _iterator; - private Interceptor _target; - private ActionBeanContext _actionBeanContext; - private ActionBean _actionBean; - private Method _handler; - private Resolution _resolution; - private LifecycleStage _lifecycleStage; - private boolean _resolutionFromHandler = false; + private Collection _interceptors; + private Iterator _iterator; + private Interceptor _target; + private ActionBeanContext _actionBeanContext; + private ActionBean _actionBean; + private Method _handler; + private Resolution _resolution; + private LifecycleStage _lifecycleStage; + private boolean _resolutionFromHandler = false; + private Map _forms; /** * Retrieves the ActionBean instance that is associated with the current request. Available @@ -75,6 +78,10 @@ public ActionBeanContext getActionBeanContext() { return _actionBeanContext; } + public Map getForms() { + return _forms; + } + /** * Retrieves the handler Method that is targeted by the current request. Available * to interceptors only after {@link LifecycleStage#HandlerResolution} has occurred. @@ -131,7 +138,11 @@ public Resolution proceed() throws Exception { /** Sets the ActionBeanContext for the current request. */ public void setActionBeanContext( ActionBeanContext actionBeanContext ) { - _actionBeanContext = actionBeanContext; + _actionBeanContext = actionBeanContext; + } + + public void setForms( Map forms ) { + _forms = forms; } /** Sets the handler method that will be invoked to process the current request. */ @@ -149,14 +160,14 @@ public void setInterceptors( Collection stack ) { /** Sets the current stage in the request processing lifecycle. */ public void setLifecycleStage( LifecycleStage lifecycleStage ) { - _lifecycleStage = lifecycleStage; + _lifecycleStage = lifecycleStage; } /** Sets the Resolution that will be executed to terminate this execution. */ public void setResolution( Resolution resolution ) { _resolution = resolution; } public void setResolutionFromHandler( boolean resolutionFromHandler ) { - _resolutionFromHandler = resolutionFromHandler; + _resolutionFromHandler = resolutionFromHandler; } /** @@ -170,7 +181,7 @@ public void setResolutionFromHandler( boolean resolutionFromHandler ) { * @throws Exception if the lifecycle code or an interceptor throws an Exception */ public Resolution wrap( Interceptor target ) throws Exception { - _target = target; + _target = target; _iterator = null; // Before executing RequestInit, set this as the current execution context diff --git a/stripes-web/src/main/java/org/stripesframework/web/util/bean/PropertyExpressionEvaluation.java b/stripes-web/src/main/java/org/stripesframework/web/util/bean/PropertyExpressionEvaluation.java index 7f7e670da..be0dcf767 100644 --- a/stripes-web/src/main/java/org/stripesframework/web/util/bean/PropertyExpressionEvaluation.java +++ b/stripes-web/src/main/java/org/stripesframework/web/util/bean/PropertyExpressionEvaluation.java @@ -29,6 +29,9 @@ import java.util.List; import java.util.Map; +import org.stripesframework.web.action.ActionBean; +import org.stripesframework.web.action.ActionBeanContext; +import org.stripesframework.web.controller.ExecutionContext; import org.stripesframework.web.controller.StripesFilter; import org.stripesframework.web.util.ReflectUtil; @@ -689,6 +692,11 @@ private Object getDefaultValue( NodeEvaluation node ) throws EvaluationException return Array.newInstance(clazz.getComponentType(), 0); } else if ( clazz.isEnum() ) { return clazz.getEnumConstants()[0]; + } else if ( ActionBean.class.isAssignableFrom(clazz) ) { + ActionBean form = (ActionBean)StripesFilter.getConfiguration().getObjectFactory().newInstance(clazz); + ActionBeanContext actionBeanContext = ExecutionContext.currentContext().getActionBeanContext(); + form.setContext(actionBeanContext); + return form; } else { return StripesFilter.getConfiguration().getObjectFactory().newInstance(clazz); } diff --git a/stripes-web/src/main/java/org/stripesframework/web/validation/DefaultValidationMetadataProvider.java b/stripes-web/src/main/java/org/stripesframework/web/validation/DefaultValidationMetadataProvider.java index 0c1441270..556afe480 100644 --- a/stripes-web/src/main/java/org/stripesframework/web/validation/DefaultValidationMetadataProvider.java +++ b/stripes-web/src/main/java/org/stripesframework/web/validation/DefaultValidationMetadataProvider.java @@ -30,6 +30,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.stripesframework.web.action.SingleBeanForm; import org.stripesframework.web.config.Configuration; import org.stripesframework.web.controller.ParameterName; import org.stripesframework.web.exception.StripesRuntimeException; @@ -243,44 +244,168 @@ protected Map loadForClass( Class beanType ) { Map meta = new HashMap<>(); @SuppressWarnings("unchecked") Map annotationInfoMap = getAnnotationInfoMap(beanType, Validate.class, - ValidateNestedProperties.class); + ValidateNestedProperties.class, ValidateForm.class); for ( String propertyName : annotationInfoMap.keySet() ) { AnnotationInfo annotationInfo = annotationInfoMap.get(propertyName); // get the @Validate and/or @ValidateNestedProperties - Validate simple = annotationInfo.getAnnotation(Validate.class); - ValidateNestedProperties nested = annotationInfo.getAnnotation(ValidateNestedProperties.class); + Validate simpleAnnotation = annotationInfo.getAnnotation(Validate.class); + ValidateNestedProperties nestedAnnotation = annotationInfo.getAnnotation(ValidateNestedProperties.class); + ValidateForm formAnnotation = annotationInfo.getAnnotation(ValidateForm.class); Class clazz = annotationInfo.getTargetClass(); // add to allow list if @Validate present - if ( simple != null ) { - if ( simple.field() == null || "".equals(simple.field()) ) { - meta.put(propertyName, new ValidationMetadata(propertyName, simple)); + if ( simpleAnnotation != null ) { + if ( "".equals(simpleAnnotation.field()) ) { + meta.put(propertyName, new ValidationMetadata(propertyName, simpleAnnotation)); } else { - log.warn("Field name present in @Validate but should be omitted: ", clazz, ", property ", propertyName, ", given field name ", simple.field()); + log.warn("Field name present in @Validate but should be omitted: ", clazz, ", property ", propertyName, ", given field name ", + simpleAnnotation.field()); } } // add all sub-properties referenced in @ValidateNestedProperties - if ( nested != null ) { - Validate[] validates = nested.value(); - if ( validates != null ) { - for ( Validate validate : validates ) { - if ( validate.field() != null && !"".equals(validate.field()) ) { - String fullName = propertyName + '.' + validate.field(); - if ( meta.containsKey(fullName) ) { - log.warn("More than one nested @Validate with same field name: " + validate.field() + " on property " + propertyName); - } - meta.put(fullName, new ValidationMetadata(fullName, validate)); + if ( nestedAnnotation != null || formAnnotation != null ) { + Validate[] validates; + String[] ons; + boolean rootBinding; + + if ( formAnnotation != null ) { + if ( formAnnotation.form() == ValidateForm.AnyForm.class ) { + continue; + } + + Class> form = formAnnotation.form(); + rootBinding = formAnnotation.rootBinding(); + ons = formAnnotation.on(); + + //noinspection unchecked + Map formAnnotationInfoMap = getAnnotationInfoMap(form, Validate.class, ValidateNestedProperties.class, + ValidateForm.class); + if ( formAnnotationInfoMap.size() != 1 ) { + log.warn("Form used for proerty ", propertyName, " defines ", formAnnotationInfoMap.size(), + " validations. Expecting exactly 1 for property 'bean'."); + continue; + } else { + AnnotationInfo validatedProperty = formAnnotationInfoMap.get("bean"); + if ( validatedProperty == null ) { + log.warn("Form used for proerty ", propertyName, " does not define a validation for the property 'bean'."); + continue; + } else { + validates = validatedProperty.getAnnotation(ValidateNestedProperties.class).value(); + } + } + + ValidationMetadata validationMetadata = new ValidationMetadata(propertyName, form); + validationMetadata.rootBinding(rootBinding).on(ons); + meta.put(propertyName, validationMetadata); + } else { + rootBinding = false; + ons = null; + validates = nestedAnnotation.value(); + } + + for ( Validate validate : validates ) { + if ( "".equals(validate.field()) ) { + log.warn("Field name missing from nestedAnnotation @Validate: ", clazz, ", property ", propertyName); + } else { + String shortName; + String bindingPrefix; + if ( rootBinding ) { + shortName = validate.field(); + bindingPrefix = propertyName + '.'; } else { - log.warn("Field name missing from nested @Validate: ", clazz, ", property ", propertyName); + shortName = null; + bindingPrefix = null; + } + + String fullName = propertyName + '.' + validate.field(); + if ( meta.containsKey(fullName) ) { + log.warn("More than one nestedAnnotation @Validate with same field name: " + fullName + " on property " + propertyName); + } + + boolean ignoreFullNameProperty = shortName != null; + ValidationMetadata validationMetadata = new ValidationMetadata(fullName, validate).ignore(ignoreFullNameProperty); + if ( ons != null ) { + validationMetadata.on(ons); + } + meta.put(fullName, validationMetadata); + + if ( shortName != null ) { + if ( meta.containsKey(shortName) ) { + log.warn("More than one nestedAnnotation @Validate with same field name: " + shortName + " on property " + propertyName); + } + ValidationMetadata shortNameValidationMetadata = new ValidationMetadata(shortName, validate); + shortNameValidationMetadata.rootBinding(true).bindingPrefix(bindingPrefix); + for ( String additionalOn : ons ) { + shortNameValidationMetadata.on(additionalOn); + } + meta.put(shortName, shortNameValidationMetadata); } } } } } + @SuppressWarnings("unchecked") Map formsAnnotationInfoMap = getAnnotationInfoMap(beanType, ValidateForm.class); + for ( String propertyName : formsAnnotationInfoMap.keySet() ) { + AnnotationInfo annotationInfo = formsAnnotationInfoMap.get(propertyName); + + // get the @Validate and/or @ValidateNestedProperties + ValidateForm formAnnotation = annotationInfo.getAnnotation(ValidateForm.class); + Class clazz = annotationInfo.getTargetClass(); + + if ( formAnnotation.form() != ValidateForm.AnyForm.class ) { + continue; + } + + PropertyDescriptor propertyDescriptor = ReflectUtil.getPropertyDescriptor(clazz, propertyName); + Class form = propertyDescriptor.getPropertyType(); + + boolean rootBinding = formAnnotation.rootBinding(); + String[] ons = formAnnotation.on(); + + ValidationMetadata formValidationMetadata = new ValidationMetadata(propertyName, ValidateForm.AnyForm.class); + formValidationMetadata.rootBinding(rootBinding).on(ons); + meta.put(propertyName, formValidationMetadata); + + Map formValidationMetadataMap = loadForClass(form); + for ( String formPropertyName : formValidationMetadataMap.keySet() ) { + ValidationMetadata formPropertyValidationMetadata = formValidationMetadataMap.get(formPropertyName); + + String shortName; + String bindingPrefix; + if ( rootBinding ) { + shortName = formPropertyName; + bindingPrefix = propertyName + '.'; + } else { + shortName = null; + bindingPrefix = null; + } + + String fullName = propertyName + '.' + formPropertyName; + if ( meta.containsKey(fullName) ) { + log.warn("More than one nestedAnnotation @Validate with same field name: " + fullName + " on property " + propertyName); + } + + boolean ignoreFullNameProperty = shortName != null; + ValidationMetadata validationMetadata = ValidationMetadata.copy(fullName, formPropertyValidationMetadata).ignore(ignoreFullNameProperty); + validationMetadata.on(ons); + meta.put(fullName, validationMetadata); + + if ( shortName != null ) { + if ( meta.containsKey(shortName) ) { + log.warn("More than one nestedAnnotation @Validate with same field name: " + shortName + " on property " + propertyName); + } + ValidationMetadata shortNameValidationMetadata = ValidationMetadata.copy(shortName, formPropertyValidationMetadata); + shortNameValidationMetadata.rootBinding(true).bindingPrefix(bindingPrefix).on(ons); + meta.put(shortName, shortNameValidationMetadata); + } + } + + } + return Collections.unmodifiableMap(meta); } diff --git a/stripes-web/src/main/java/org/stripesframework/web/validation/FormValidation.java b/stripes-web/src/main/java/org/stripesframework/web/validation/FormValidation.java new file mode 100644 index 000000000..fb310d098 --- /dev/null +++ b/stripes-web/src/main/java/org/stripesframework/web/validation/FormValidation.java @@ -0,0 +1,32 @@ +package org.stripesframework.web.validation; + +import java.util.Collections; +import java.util.Set; + +import org.stripesframework.web.action.SingleBeanForm; + + +public class FormValidation { + + private SingleBeanForm _form; + private Set _on; + + public SingleBeanForm getForm() { + return _form; + } + + public Set getOn() { + if ( _on == null ) { + return Collections.emptySet(); + } + return _on; + } + + public void setForm( SingleBeanForm form ) { + _form = form; + } + + public void setOn( Set on ) { + _on = on; + } +} diff --git a/stripes-web/src/main/java/org/stripesframework/web/validation/ValidateForm.java b/stripes-web/src/main/java/org/stripesframework/web/validation/ValidateForm.java new file mode 100644 index 000000000..ea1e47208 --- /dev/null +++ b/stripes-web/src/main/java/org/stripesframework/web/validation/ValidateForm.java @@ -0,0 +1,39 @@ +/* Copyright 2005-2006 Tim Fennell + * + * 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.stripesframework.web.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.stripesframework.web.action.SingleBeanForm; + + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Documented +public @interface ValidateForm { + + Class> form() default AnyForm.class; + + String[] on() default {}; + + boolean rootBinding() default false; + + interface AnyForm extends SingleBeanForm {} + +} diff --git a/stripes-web/src/main/java/org/stripesframework/web/validation/ValidationMetadata.java b/stripes-web/src/main/java/org/stripesframework/web/validation/ValidationMetadata.java index 7c3930c1b..740b6650a 100644 --- a/stripes-web/src/main/java/org/stripesframework/web/validation/ValidationMetadata.java +++ b/stripes-web/src/main/java/org/stripesframework/web/validation/ValidationMetadata.java @@ -18,6 +18,8 @@ import java.util.Set; import java.util.regex.Pattern; +import org.stripesframework.web.action.SingleBeanForm; + /** *

Encapsulates the validation metadata for a single property of a single class. Structure @@ -34,21 +36,34 @@ */ public class ValidationMetadata { - private final String _property; - private boolean _encrypted; - private boolean _required; - private boolean _trim; - private Set _on; - private boolean _onIsPositive; - private boolean _ignore; - private Integer _minlength; - private Integer _maxlength; - private Double _minvalue; - private Double _maxvalue; - private Pattern _mask; + public static ValidationMetadata copy( String property, ValidationMetadata original ) { + if ( original.validate() != null ) { + return new ValidationMetadata(property, original.validate()); + } else { + return new ValidationMetadata(property, original.form()); + } + } + + private final String _property; + private Validate _validate; + private Class> _form; + private boolean _encrypted; + private boolean _required; + private boolean _trim; + private Set _on; + private boolean _onIsPositive; + private boolean _ignore; + private Integer _minlength; + private Integer _maxlength; + private Double _minvalue; + private Double _maxvalue; + private Pattern _mask; + @SuppressWarnings("rawtypes") - private Class _converter; - private String _label; + private Class _converter; + private String _label; + private boolean _rootBinding; + private String _bindingPrefix; /** * Constructs a ValidationMetadata object for the specified property. Further constraints @@ -71,6 +86,7 @@ public ValidationMetadata( String property ) { public ValidationMetadata( String property, Validate validate ) { // Copy over all the simple values _property = property; + _validate = validate; encrypted(validate.encrypted()); required(validate.required()); trim(validate.trim()); @@ -101,6 +117,20 @@ public ValidationMetadata( String property, Validate validate ) { } } + public ValidationMetadata( String property, Class> form ) { + _property = property; + _form = form; + } + + public ValidationMetadata bindingPrefix( String bindingPrefix ) { + _bindingPrefix = bindingPrefix; + return this; + } + + public String bindingPrefix() { + return _bindingPrefix; + } + /** Sets the overridden TypeConveter to use to convert values. */ @SuppressWarnings("rawtypes") public ValidationMetadata converter( Class converter ) { @@ -121,6 +151,10 @@ public ValidationMetadata encrypted( boolean encrypted ) { /** Returns true if the field in question is encrypted. */ public boolean encrypted() { return _encrypted; } + public Class> form() { + return _form; + } + /** Returns the name of the property this validation metadata represents. */ public String getProperty() { return _property; @@ -229,6 +263,15 @@ public boolean requiredOn( String event ) { return _required && !_ignore && ((_on == null) || (_onIsPositive && _on.contains(event)) || (!_onIsPositive && !_on.contains(event))); } + public boolean rootBinding() { + return _rootBinding; + } + + public ValidationMetadata rootBinding( boolean rootBinding ) { + _rootBinding = rootBinding; + return this; + } + /** * Overidden toString() that only outputs the constraints that are specified by * the instance of validation metadata (i.e. omits nulls, defaults etc.) @@ -254,5 +297,9 @@ public ValidationMetadata trim( boolean trim ) { /** Returns true if the field should be trimmed before validation or type conversion. */ public boolean trim() { return _trim; } + + public Validate validate() { + return _validate; + } } diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValue.java b/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValue.java new file mode 100644 index 000000000..25316d5c3 --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValue.java @@ -0,0 +1,35 @@ +package org.stripesframework.web.validation; + +public class BusinessValue { + + private int numberZero; + private int numberOne; + private int numberTwo; + + @SuppressWarnings("unused") + public int getNumberOne() { + return numberOne; + } + + @SuppressWarnings("unused") + public int getNumberTwo() { + return numberTwo; + } + + @SuppressWarnings("unused") + public int getNumberZero() { + return numberZero; + } + + public void setNumberOne( int numberOne ) { + this.numberOne = numberOne; + } + + public void setNumberTwo( int numberTwo ) { + this.numberTwo = numberTwo; + } + + public void setNumberZero( int numberZero ) { + this.numberZero = numberZero; + } +} diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValueForm.java b/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValueForm.java new file mode 100644 index 000000000..4ea75c851 --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValueForm.java @@ -0,0 +1,64 @@ +package org.stripesframework.web.validation; + +import org.stripesframework.web.action.ActionBeanContext; +import org.stripesframework.web.action.SingleBeanForm; + + +public class BusinessValueForm implements SingleBeanForm { + + static ThreadLocal counter = ThreadLocal.withInitial(() -> 1); + static ThreadLocal validateAlwaysRan = ThreadLocal.withInitial(() -> 0); + static ThreadLocal validateOneRan = ThreadLocal.withInitial(() -> 0); + static ThreadLocal validateTwoRan = ThreadLocal.withInitial(() -> 0); + + private ActionBeanContext _context; + private BusinessValue _businessObject; + + @ValidateNestedProperties({ // + @Validate(field = "numberZero", required = true), // + @Validate(field = "numberOne", required = true, minvalue = 0), // + @Validate(field = "numberTwo") // + }) + @Override + public BusinessValue getBean() { + return _businessObject; + } + + @Override + public ActionBeanContext getContext() { + return _context; + } + + @Override + public void setBean( BusinessValue businessObject ) { + _businessObject = businessObject; + } + + @Override + public void setContext( ActionBeanContext context ) { + _context = context; + } + + @ValidationMethod(priority = 0) + @SuppressWarnings("DefaultAnnotationParam") + public void validateAlways( ValidationErrors errors ) { + if ( errors == null ) { + throw new RuntimeException("errors must not be null"); + } + validateAlwaysRan.set(counter.get()); + counter.set(counter.get() + 1); + } + + @ValidationMethod(priority = 1, when = ValidationState.NO_ERRORS) + public void validateOne() { + validateOneRan.set(counter.get()); + counter.set(counter.get() + 1); + } + + @ValidationMethod(priority = 1, when = ValidationState.ALWAYS) + public void validateTwo( ValidationErrors errors ) { + validateTwoRan.set(counter.get()); + counter.set(counter.get() + 1); + } + +} diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValueFormAsWrapper.java b/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValueFormAsWrapper.java new file mode 100644 index 000000000..d6e08ff82 --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/BusinessValueFormAsWrapper.java @@ -0,0 +1,63 @@ +package org.stripesframework.web.validation; + +public class BusinessValueFormAsWrapper { + + static ThreadLocal counter = ThreadLocal.withInitial(() -> 1); + static ThreadLocal validateAlwaysRan = ThreadLocal.withInitial(() -> 0); + static ThreadLocal validateOneRan = ThreadLocal.withInitial(() -> 0); + static ThreadLocal validateTwoRan = ThreadLocal.withInitial(() -> 0); + + private final BusinessValue businessValue = new BusinessValue(); + + @SuppressWarnings("unused") + public int getNumberOne() { + return businessValue.getNumberOne(); + } + + @SuppressWarnings("unused") + public int getNumberTwo() { + return businessValue.getNumberTwo(); + } + + @SuppressWarnings("unused") + public int getNumberZero() { + return businessValue.getNumberZero(); + } + + @Validate(required = true, minvalue = 0) + public void setNumberOne( int numberOne ) { + businessValue.setNumberOne(numberOne); + } + + @Validate() + public void setNumberTwo( int numberTwo ) { + businessValue.setNumberTwo(numberTwo); + } + + @Validate(required = true) + public void setNumberZero( int numberZero ) { + businessValue.setNumberZero(numberZero); + } + + @SuppressWarnings("DefaultAnnotationParam") + @ValidationMethod(priority = 0) + public void validateAlways( ValidationErrors errors ) { + if ( errors == null ) { + throw new RuntimeException("errors must not be null"); + } + validateAlwaysRan.set(counter.get()); + counter.set(counter.get() + 1); + } + + @ValidationMethod(priority = 1, when = ValidationState.NO_ERRORS) + public void validateOne() { + validateOneRan.set(counter.get()); + counter.set(counter.get() + 1); + } + + @ValidationMethod(priority = 1, when = ValidationState.ALWAYS) + public void validateTwo( ValidationErrors errors ) { + validateTwoRan.set(counter.get()); + counter.set(counter.get() + 1); + } +} diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormFormAsValueWrapperRootBindingTest.java b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormFormAsValueWrapperRootBindingTest.java new file mode 100644 index 000000000..8ffcb9242 --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormFormAsValueWrapperRootBindingTest.java @@ -0,0 +1,226 @@ +package org.stripesframework.web.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.stripesframework.web.FilterEnabledTestBase; +import org.stripesframework.web.action.ActionBean; +import org.stripesframework.web.action.ActionBeanContext; +import org.stripesframework.web.action.DefaultHandler; +import org.stripesframework.web.action.HandlesEvent; +import org.stripesframework.web.action.Resolution; +import org.stripesframework.web.action.UrlBinding; +import org.stripesframework.web.mock.MockRoundtrip; + + +/** + * Test out various aspects of the validation subsystem in Stripes with regard to optional + * application, ordering and flow control. Each of the individual test methods has javadoc + * explaining why the expected results are as they are.
+ *
+ * This test handles the usecase for filling up business value objects (domain model, {@link BusinessValue}) + * by using a wrapper form ({@link BusinessValueFormAsWrapper}). The wrapper form defines all + * validations for validating the input parameters.
+ *
+ * This test tests the rootBinding option. This allows to bind a property like "businessValue1.numberOne" + * by using the parameter "numberOne" (the properties of the given beans are directly bound to the action).
+ *
+ * @author Olaf Stracke + */ +@UrlBinding("/test/ValidationFormFormAsValueWrapperRootBindingTest.action") +public class ValidationFormFormAsValueWrapperRootBindingTest extends FilterEnabledTestBase implements ActionBean { + + private ActionBeanContext _context; + private BusinessValueFormAsWrapper _businessValue1; + + @BeforeEach + public void before() { + BusinessValueFormAsWrapper.counter.set(1); + BusinessValueFormAsWrapper.validateAlwaysRan.set(0); + BusinessValueFormAsWrapper.validateOneRan.set(0); + BusinessValueFormAsWrapper.validateTwoRan.set(0); + } + + @ValidateForm(on = "eventOne", rootBinding = true) + public BusinessValueFormAsWrapper getBusinessValue1() { + if ( _businessValue1 == null ) { + _businessValue1 = new BusinessValueFormAsWrapper(); + } + return _businessValue1; + } + + @Override + public ActionBeanContext getContext() { return _context; } + + @DefaultHandler + @HandlesEvent("eventOne") + public Resolution one() { return null; } + + @Override + public void setContext( ActionBeanContext context ) { _context = context;} + + /** + * Almost identical to testEventOneWithNoErrors except that we invoke the 'default' event. + * Tests to make sure that event-specific validations are still applied correctly when the + * event name isn't present in the request. + */ + @Test + public void testEventOneAsDefault() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberOne", "101"); + trip.execute(); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Number one is a required field also for this event, so we supply it. This event should + * cause both validateAlways and validateOne to run. + */ + @Test + public void testEventOneNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberOne", "101"); + + trip.execute("eventOne"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + @Test + public void testEventOneWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", ""); // required field always + trip.addParameter("numberTwo", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getContext().getValidationErrors()).hasSize(2); + } + + /** + * Tests that a required field error is raised this time for numberOne which is only + * required for this event. Again this single error should prevent both validateAlways + * and validateOne from running. + */ + @Test + public void testEventOneWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberOne", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + /** + * Straightforward test for event two that makes sure it's validations run. This time + * numberTwo should be required (and is supplied) and validateAlways and validateTwo should + * run but not validateOne. + */ + @Test + public void testEventTwoNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberTwo", "102"); + + trip.execute("eventTwo"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberTwo()).isEqualTo(102); + assertThat(test.getContext().getValidationErrors().size()).isEqualTo(0); + } + + /** + * Tests that validateTwo is run event though there are errors and valiateAlways did not run, + * because validateTwo is marked to run always. + */ + @Test + public void testEventTwoWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", ""); // required field always + trip.addParameter("numberTwo", ""); // required field for event one + trip.execute("eventTwo"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).hasSize(0); + } + + /** + * numberZero is the only required field for eventZero, so there should be no validation + * errors generated. The only validation method that should be run is validateAlways() because + * the others are tied to specific events. + */ + @Test + public void testEventZeroNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "99"); + trip.execute("eventZero"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Generates an error by providing an invalid value for numberOne (which has a minimum + * value of 0). Validations other than required are still applied even though that @Validate + * has a on="one". The single validaiton error should prevent validateAlways() and + * validateOne from running. + */ + @Test + public void testEventZeroWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "99"); + trip.addParameter("numberOne", "-100"); + trip.execute("eventZero"); + + ValidationFormFormAsValueWrapperRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(99); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + @HandlesEvent("eventTwo") + @SuppressWarnings("unused") + public Resolution two() { return null; } + + @HandlesEvent("eventZero") + @SuppressWarnings("unused") + public Resolution zero() { return null; } + +} diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormFormAsValueWrapperTest.java b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormFormAsValueWrapperTest.java new file mode 100644 index 000000000..54b778647 --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormFormAsValueWrapperTest.java @@ -0,0 +1,223 @@ +package org.stripesframework.web.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.stripesframework.web.FilterEnabledTestBase; +import org.stripesframework.web.action.ActionBean; +import org.stripesframework.web.action.ActionBeanContext; +import org.stripesframework.web.action.DefaultHandler; +import org.stripesframework.web.action.HandlesEvent; +import org.stripesframework.web.action.Resolution; +import org.stripesframework.web.action.UrlBinding; +import org.stripesframework.web.mock.MockRoundtrip; + + +/** + * Test out various aspects of the validation subsystem in Stripes with regard to optional + * application, ordering and flow control. Each of the individual test methods has javadoc + * explaining why the expected results are as they are.
+ *
+ * This test handles the usecase for filling up business value objects (domain model, {@link BusinessValue}) + * by using a wrapper form ({@link BusinessValueFormAsWrapper}). The wrapper form defines all + * validations for validating the input parameters.
+ * + * @author Olaf Stracke + */ +@UrlBinding("/test/ValidationFormFormAsValueWrapperTest.action") +public class ValidationFormFormAsValueWrapperTest extends FilterEnabledTestBase implements ActionBean { + + private ActionBeanContext _context; + private BusinessValueFormAsWrapper _businessValue1; + + @BeforeEach + public void before() { + BusinessValueFormAsWrapper.counter.set(1); + BusinessValueFormAsWrapper.validateAlwaysRan.set(0); + BusinessValueFormAsWrapper.validateOneRan.set(0); + BusinessValueFormAsWrapper.validateTwoRan.set(0); + } + + @ValidateForm(on = "eventOne") + public BusinessValueFormAsWrapper getBusinessValue1() { + if ( _businessValue1 == null ) { + _businessValue1 = new BusinessValueFormAsWrapper(); + } + return _businessValue1; + } + + @Override + public ActionBeanContext getContext() { return _context; } + + @DefaultHandler + @HandlesEvent("eventOne") + public Resolution one() { return null; } + + @Override + public void setContext( ActionBeanContext context ) { _context = context;} + + /** + * Almost identical to testEventOneWithNoErrors except that we invoke the 'default' event. + * Tests to make sure that event-specific validations are still applied correctly when the + * event name isn't present in the request. + */ + @Test + public void testEventOneAsDefault() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberOne", "101"); + trip.execute(); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Number one is a required field also for this event, so we supply it. This event should + * cause both validateAlways and validateOne to run. + */ + @Test + public void testEventOneNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberOne", "101"); + + trip.execute("eventOne"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + @Test + public void testEventOneWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", ""); // required field always + trip.addParameter("businessValue1.numberTwo", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getContext().getValidationErrors()).hasSize(2); + } + + /** + * Tests that a required field error is raised this time for numberOne which is only + * required for this event. Again this single error should prevent both validateAlways + * and validateOne from running. + */ + @Test + public void testEventOneWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberOne", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + /** + * Straightforward test for event two that makes sure it's validations run. This time + * numberTwo should be required (and is supplied) and validateAlways and validateTwo should + * run but not validateOne. + */ + @Test + public void testEventTwoNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberTwo", "102"); + + trip.execute("eventTwo"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test._businessValue1.getNumberZero()).isEqualTo(100); + assertThat(test._businessValue1.getNumberTwo()).isEqualTo(102); + assertThat(test.getContext().getValidationErrors().size()).isEqualTo(0); + } + + /** + * Tests that validateTwo is run event though there are errors and valiateAlways did not run, + * because validateTwo is marked to run always. + */ + @Test + public void testEventTwoWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", ""); // required field always + trip.addParameter("businessValue1.numberTwo", ""); // required field for event one + trip.execute("eventTwo"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).hasSize(0); + } + + /** + * numberZero is the only required field for eventZero, so there should be no validation + * errors generated. The only validation method that should be run is validateAlways() because + * the others are tied to specific events. + */ + @Test + public void testEventZeroNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "99"); + trip.execute("eventZero"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Generates an error by providing an invalid value for numberOne (which has a minimum + * value of 0). Validations other than required are still applied even though that @Validate + * has a on="one". The single validaiton error should prevent validateAlways() and + * validateOne from running. + */ + @Test + public void testEventZeroWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "99"); + trip.addParameter("businessValue1.numberOne", "-100"); + trip.execute("eventZero"); + + ValidationFormFormAsValueWrapperTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueFormAsWrapper.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueFormAsWrapper.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(99); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + @HandlesEvent("eventTwo") + @SuppressWarnings("unused") + public Resolution two() { return null; } + + @HandlesEvent("eventZero") + @SuppressWarnings("unused") + public Resolution zero() { return null; } + +} diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormSingleBeanFormRootBindingTest.java b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormSingleBeanFormRootBindingTest.java new file mode 100644 index 000000000..5836119df --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormSingleBeanFormRootBindingTest.java @@ -0,0 +1,223 @@ +package org.stripesframework.web.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.stripesframework.web.FilterEnabledTestBase; +import org.stripesframework.web.action.ActionBean; +import org.stripesframework.web.action.ActionBeanContext; +import org.stripesframework.web.action.DefaultHandler; +import org.stripesframework.web.action.HandlesEvent; +import org.stripesframework.web.action.Resolution; +import org.stripesframework.web.action.UrlBinding; +import org.stripesframework.web.mock.MockRoundtrip; + + +/** + * Test out various aspects of the validation subsystem in Stripes with regard to optional + * application, ordering and flow control. Each of the individual test methods has javadoc + * explaining why the expected results are as they are.
+ *
+ * This test handles the usecase for filling up business value objects (domain model, {@link BusinessValue}) + * by using a spezilized form ({@link BusinessValueForm}). The form defines all + * validations for validating the input parameters.
+ *
+ * This test tests the rootBinding option. This allows to bind a property like "businessValue1.numberOne" + * by using the parameter "numberOne" (the properties of the given beans are directly bound to the action).
+ *
+ * @author Olaf Stracke + */ +@UrlBinding("/test/ValidationFormSingleBeanRootBindingTest.action") +public class ValidationFormSingleBeanFormRootBindingTest extends FilterEnabledTestBase implements ActionBean { + + private ActionBeanContext _context; + private final BusinessValue _businessValue1 = new BusinessValue(); + + @BeforeEach + public void before() { + BusinessValueForm.counter.set(1); + BusinessValueForm.validateAlwaysRan.set(0); + BusinessValueForm.validateOneRan.set(0); + BusinessValueForm.validateTwoRan.set(0); + } + + @ValidateForm(form = BusinessValueForm.class, on = "eventOne", rootBinding = true) + public BusinessValue getBusinessValue1() { + return _businessValue1; + } + + @Override + public ActionBeanContext getContext() { return _context; } + + @DefaultHandler + @HandlesEvent("eventOne") + public Resolution one() { return null; } + + @Override + public void setContext( ActionBeanContext context ) { _context = context;} + + /** + * Almost identical to testEventOneWithNoErrors except that we invoke the 'default' event. + * Tests to make sure that event-specific validations are still applied correctly when the + * event name isn't present in the request. + */ + @Test + public void testEventOneAsDefault() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberOne", "101"); + trip.execute(); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Number one is a required field also for this event, so we supply it. This event should + * cause both validateAlways and validateOne to run. + */ + @Test + public void testEventOneNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberOne", "101"); + + trip.execute("eventOne"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + @Test + public void testEventOneWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", ""); // required field always + trip.addParameter("numberTwo", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getContext().getValidationErrors()).hasSize(2); + } + + /** + * Tests that a required field error is raised this time for numberOne which is only + * required for this event. Again this single error should prevent both validateAlways + * and validateOne from running. + */ + @Test + public void testEventOneWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberOne", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + /** + * Straightforward test for event two that makes sure it's validations run. This time + * numberTwo should be required (and is supplied) and validateAlways and validateTwo should + * run but not validateOne. + */ + @Test + public void testEventTwoNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "100"); + trip.addParameter("numberTwo", "102"); + + trip.execute("eventTwo"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberTwo()).isEqualTo(102); + assertThat(test.getContext().getValidationErrors().size()).isEqualTo(0); + } + + /** + * Tests that validateTwo is run event though there are errors and valiateAlways did not run, + * because validateTwo is marked to run always. + */ + @Test + public void testEventTwoWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", ""); // required field always + trip.addParameter("numberTwo", ""); // required field for event one + trip.execute("eventTwo"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).hasSize(0); + } + + /** + * numberZero is the only required field for eventZero, so there should be no validation + * errors generated. The only validation method that should be run is validateAlways() because + * the others are tied to specific events. + */ + @Test + public void testEventZeroNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "99"); + trip.execute("eventZero"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Generates an error by providing an invalid value for numberOne (which has a minimum + * value of 0). Validations other than required are still applied even though that @Validate + * has a on="one". The single validaiton error should prevent validateAlways() and + * validateOne from running. + */ + @Test + public void testEventZeroWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("numberZero", "99"); + trip.addParameter("numberOne", "-100"); + trip.execute("eventZero"); + + ValidationFormSingleBeanFormRootBindingTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(99); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + @HandlesEvent("eventTwo") + @SuppressWarnings("unused") + public Resolution two() { return null; } + + @HandlesEvent("eventZero") + @SuppressWarnings("unused") + public Resolution zero() { return null; } + +} diff --git a/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormSingleBeanFormTest.java b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormSingleBeanFormTest.java new file mode 100644 index 000000000..8589bb821 --- /dev/null +++ b/stripes-web/src/test/java/org/stripesframework/web/validation/ValidationFormSingleBeanFormTest.java @@ -0,0 +1,221 @@ +package org.stripesframework.web.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.stripesframework.web.FilterEnabledTestBase; +import org.stripesframework.web.action.ActionBean; +import org.stripesframework.web.action.ActionBeanContext; +import org.stripesframework.web.action.DefaultHandler; +import org.stripesframework.web.action.HandlesEvent; +import org.stripesframework.web.action.Resolution; +import org.stripesframework.web.action.UrlBinding; +import org.stripesframework.web.mock.MockRoundtrip; + + +/** + * Test out various aspects of the validation subsystem in Stripes with regard to optional + * application, ordering and flow control. Each of the individual test methods has javadoc + * explaining why the expected results are as they are.
+ *
+ * This test handles the usecase for filling up business value objects (domain model, {@link BusinessValue}) + * by using a spezilized form ({@link BusinessValueForm}). The form defines all + * validations for validating the input parameters.
+ *
+ * + * @author Olaf Stracke + */ +@UrlBinding("/test/ValidationFormSingleBeanFormTest.action") +public class ValidationFormSingleBeanFormTest extends FilterEnabledTestBase implements ActionBean { + + private ActionBeanContext _context; + private final BusinessValue _businessValue1 = new BusinessValue(); + + @BeforeEach + public void before() { + BusinessValueForm.counter.set(1); + BusinessValueForm.validateAlwaysRan.set(0); + BusinessValueForm.validateOneRan.set(0); + BusinessValueForm.validateTwoRan.set(0); + } + + @ValidateForm(form = BusinessValueForm.class, on = "eventOne") + public BusinessValue getBusinessValue1() { + return _businessValue1; + } + + @Override + public ActionBeanContext getContext() { return _context; } + + @DefaultHandler + @HandlesEvent("eventOne") + public Resolution one() { return null; } + + @Override + public void setContext( ActionBeanContext context ) { _context = context;} + + /** + * Almost identical to testEventOneWithNoErrors except that we invoke the 'default' event. + * Tests to make sure that event-specific validations are still applied correctly when the + * event name isn't present in the request. + */ + @Test + public void testEventOneAsDefault() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberOne", "101"); + trip.execute(); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Number one is a required field also for this event, so we supply it. This event should + * cause both validateAlways and validateOne to run. + */ + @Test + public void testEventOneNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberOne", "101"); + + trip.execute("eventOne"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(1); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(2); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(3); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getBusinessValue1().getNumberOne()).isEqualTo(101); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + @Test + public void testEventOneWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", ""); // required field always + trip.addParameter("businessValue1.numberTwo", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getContext().getValidationErrors()).hasSize(2); + } + + /** + * Tests that a required field error is raised this time for numberOne which is only + * required for this event. Again this single error should prevent both validateAlways + * and validateOne from running. + */ + @Test + public void testEventOneWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberOne", ""); // required field for event one + trip.execute("eventOne"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(1); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(100); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + /** + * Straightforward test for event two that makes sure it's validations run. This time + * numberTwo should be required (and is supplied) and validateAlways and validateTwo should + * run but not validateOne. + */ + @Test + public void testEventTwoNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "100"); + trip.addParameter("businessValue1.numberTwo", "102"); + + trip.execute("eventTwo"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test._businessValue1.getNumberZero()).isEqualTo(100); + assertThat(test._businessValue1.getNumberTwo()).isEqualTo(102); + assertThat(test.getContext().getValidationErrors().size()).isEqualTo(0); + } + + /** + * Tests that validateTwo is run event though there are errors and valiateAlways did not run, + * because validateTwo is marked to run always. + */ + @Test + public void testEventTwoWithEmptyFields() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", ""); // required field always + trip.addParameter("businessValue1.numberTwo", ""); // required field for event one + trip.execute("eventTwo"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).hasSize(0); + } + + /** + * numberZero is the only required field for eventZero, so there should be no validation + * errors generated. The only validation method that should be run is validateAlways() because + * the others are tied to specific events. + */ + @Test + public void testEventZeroNoErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "99"); + trip.execute("eventZero"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getContext().getValidationErrors()).isEmpty(); + } + + /** + * Generates an error by providing an invalid value for numberOne (which has a minimum + * value of 0). Validations other than required are still applied even though that @Validate + * has a on="one". The single validaiton error should prevent validateAlways() and + * validateOne from running. + */ + @Test + public void testEventZeroWithErrors() throws Exception { + MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), getClass()); + trip.addParameter("businessValue1.numberZero", "99"); + trip.addParameter("businessValue1.numberOne", "-100"); + trip.execute("eventZero"); + + ValidationFormSingleBeanFormTest test = trip.getActionBean(getClass()); + assertThat(BusinessValueForm.validateAlwaysRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateOneRan.get().intValue()).isEqualTo(0); + assertThat(BusinessValueForm.validateTwoRan.get().intValue()).isEqualTo(0); + assertThat(test.getBusinessValue1().getNumberZero()).isEqualTo(99); + assertThat(test.getContext().getValidationErrors()).hasSize(1); + } + + @HandlesEvent("eventTwo") + @SuppressWarnings("unused") + public Resolution two() { return null; } + + @HandlesEvent("eventZero") + @SuppressWarnings("unused") + public Resolution zero() { return null; } + +}