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.

  1. For each field annotation, create an instance of the policy linked to it and set its annotation to configure it.
  2. For each adjuster, run it.
  3. For each non-conversion validator, run it.
    1. If the result is rejection, set field rejection.
    2. If the result is rejection and annotation has short circuit set, abort execution.
  4. If field rejection not set and a converter exists, run it.
    1. If the result is success, for each post-conversion adjuster, run it
    2. If the result is success, for each post-conversion validator, run it
      1. If the result is rejection, set field rejection.
      2. 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.

  1. Find recipient field for conversion
  2. Check recipient field is the correct type
  3. Invoke the converter
  4. 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).