Programming Thoughts
Struts 2 - Annotation-based Validation
Alternate Interceptor
Actually reducing Struts 2's arcane configuration
Struts 2 is a popular MVC framework for Java-based web applications but can suffer from tedious configuration. The annotation Java language feature can reduce this but the out-of-the-box annotation-based validation conflicts with making redirect after post work and falls apart with page designs the Struts UI tags can't create. Annotation-based validation must be redesigned.
Entry Point
The interceptor itself is the core framework of validation. This differs from Struts 2's interceptor as that
delegates to an injected instance of ActionValidatorManager
. That's needed for Struts'
older, non-annotated validation and formatting done in the JSP page but this design deliberately avoids that.
If annotations on a form field are omitted, validation of them must revert to older, manual validation, and
formatting is done within Actions.
However, like Struts' validation interceptor, this is derived from MethodFilterInterceptor
. The
basic algorithm is obviously, first identify the form, wether it's the Action itself or the model for
ModelDriven. For each form field, process each adjuster annotation, then each non-conversion validator
annotation, then the converter annotation (if present), then each post-conversion validator. The
doIntercept
method is:-
@Override protected String doIntercept(ActionInvocation invocation) throws Exception { TextProvider textProvider; ValidationAware validationAware; Collection<Field> allFormFields; Object action, form; boolean modelDriven; action = invocation.getAction(); modelDriven = action instanceof ModelDriven; if (modelDriven) { form = ((ModelDriven<?>)action).getModel(); } else { form = action; } if (action instanceof TextProvider) { textProvider = (TextProvider)action; } else { textProvider = null; } if (action instanceof ValidationAware) { validationAware = (ValidationAware)action; } else { validationAware = null; } allFormFields = getProperties(form.getClass()); for (Field formField: allFormFields) { if (formField.getType().isAssignableFrom(String.class)) { processFormField(invocation, validationAware, textProvider, allFormFields, formField, form, modelDriven); } } return invocation.invoke(); }
As it's hypothetically possible for a Struts Action to not implement TextProvider
or
ValidationAware
, the second and third if statements are required. The getProperties
function returns all fields of a class, whether directly declared or inherited.
Core Algorithm
The core function, processFormField
, is private as it's the template of the interceptor.
It has the following steps.
- For each field annotation, create an instance of the policy linked to it and set its annotation to configure it.
- For each adjuster, run it.
- For each non-conversion validator, run it.
- If the result is rejection, set field rejection.
- If the result is rejection and annotation has short circuit set, abort execution.
- If field rejection not set and a converter exists, run it.
- If the result is success, for each post-conversion adjuster, run it
- If the result is success, for each post-conversion validator, run it
- If the result is rejection, set field rejection.
- If the result is rejection and annotation has short circuit set, abort execution.
To create flexibility, key functions are protected and can, thus, be overridden. A few are described in the sections below.
Policy Creation
The first key function, getAnnotationUsage
, creates a policy from a form field and annotation
and is the following.
/** * Returns validator (and formatter) that processes an annotated form field, or result of NA type if not recognised. */ protected <T> AnnotationUsageResult<T> getAnnotationUsage(Field unconvertedField, Annotation annotation) throws Exception { return ValidatorLibrary.getAnnotationUsage(annotation); }
The pertinent part of getAnnotationUsage
is below. It trawls the list of known annotations to
find its linked policy, creates an instance and sets it with the annotation.
result = null; policies = getPolicies(); for (PolicyEntry<?,?> entry: policies) { if (entry.getAnnotationClass().isAssignableFrom(annotation.getClass()) ) { policyClass = entry.getPolicyClass(); constructor = policyClass.getConstructor(); policy = (Policy<Annotation>)constructor.newInstance(new Object[] {}); policy.setAnnotation(annotation); if (policy instanceof Adjuster) { result = new AnnotationUsageResult<T>((Adjuster<Annotation>)policy); } else if (policy instanceof Converter) { result = new AnnotationUsageResult<T>((Converter<Annotation,T>)policy); } else if (policy instanceof NonConversionValidator) { result = new AnnotationUsageResult<T>((NonConversionValidator<Annotation>)policy); } else if (policy instanceof PostConversionValidator) { result = new AnnotationUsageResult<T>((PostConversionValidator<Annotation,T>)policy); } break; } } if (result == null) { result = new AnnotationUsageResult<T>(); } return result;
The getPolicies
function is partially shown below. The list of policies is a private static
field, making it hardcoded.
public synchronized static List<PolicyEntry<?,?>> getPolicies() { if (policies == null) { policies = new ArrayList<>(); policies.add(new PolicyEntry<>(BigDecimalConversion.class, BigDecimalConverter.class)); policies.add(new PolicyEntry<>(BooleanConversion.class, BooleanConverter.class)); ... } return policies; }
Conversion
The sequence for the conversion step is simple.
- Find recipient field for conversion
- Check recipient field is the correct type
- Invoke the converter
- Write the error message if needed
Checking the recipient field data type deserves examination.
protected boolean checkConversionRecipientDataType(Field unconvertedField, Annotation annotation, Field recipientField, Class<?> recipientClass) { return ValidatorLibrary.checkRecipientDataType(recipientField, recipientClass); } public static boolean checkRecipientDataType(Field recipientField, Class<?> recipientClass) { Class<?> recipientFieldClass; recipientFieldClass = recipientField.getType(); return checkFieldClass(recipientClass, recipientFieldClass); } public static boolean checkFieldClass(Class<?> conversionClass, Class<?> fieldClass) { if (conversionClass == Boolean.class) { return Boolean.class.isAssignableFrom(fieldClass) || boolean.class.isAssignableFrom(fieldClass); } else if (conversionClass == Byte.class) { return Byte.class.isAssignableFrom(fieldClass) || byte.class.isAssignableFrom(fieldClass); } else if (conversionClass == Character.class) { return Character.class.isAssignableFrom(fieldClass) || char.class.isAssignableFrom(fieldClass); } else if (conversionClass == Double.class) { return Double.class.isAssignableFrom(fieldClass) || double.class.isAssignableFrom(fieldClass); } else if (conversionClass == Float.class) { return Float.class.isAssignableFrom(fieldClass) || float.class.isAssignableFrom(fieldClass); } else if (conversionClass == Integer.class) { return Integer.class.isAssignableFrom(fieldClass) || int.class.isAssignableFrom(fieldClass); } else if (conversionClass == Long.class) { return Long.class.isAssignableFrom(fieldClass) || long.class.isAssignableFrom(fieldClass); } else if (conversionClass == Short.class) { return Short.class.isAssignableFrom(fieldClass) || short.class.isAssignableFrom(fieldClass); } else { return conversionClass.isAssignableFrom(fieldClass); } }
The checkFieldClass
function matches converter's (and post-conversion validator's) generic type
with the recipient field class, regarding wrapper classes as the same as its primitive type. It looks like a
hard to maintain kludge but the set of primitive types won't change for some time.
Next part
Continued in Alternate Validators (Custom).