Programming Thoughts
Struts 2 - Annotation-based Validation
Custom Validation is Too Arcane

Simple workflow and pages or don't bother

Struts 2 is a popular MVC framework for Java-based web applications. Every organisation has its own methods and rules, so customisation and extension of the Struts 2 framework is inevitable. Allowing useful customisations that aren't complicated or difficult to understand is a fine art but Struts 2 is terrible for customised type conversion and validation.

Painful Custom Validators

At time of writing, the official documentation for validation is at the Core Developers Guide Validation page and the section for 'Writing custom validators' consists of only the following.

Writing custom validators

If you want to write custom validator use on of these classes as a starting point:

  • com.opensymphony.xwork2.validator.validators.ValidatorSupport
  • com.opensymphony.xwork2.validator.validators.FieldValidatorSupport
  • com.opensymphony.xwork2.validator.validators.RangeValidatorSupport
  • com.opensymphony.xwork2.validator.validators.RepopulateConversionErrorFieldValidatorSupport

The javadoc for ValidatorSupport is

/** * Abstract implementation of the Validator interface suitable for subclassing. * * @author Jason Carreira * @author tm_jee * @author Martin Gilday */

Gee! Thanks a bunch!

Looking at a simple validator, RequiredStringValidator, the code is mostly accepting annotation parameters and the following.

public void validate(Object object) throws ValidationException { Object fieldValue = this.getFieldValue(getFieldName(), object); if (fieldValue == null) { addFieldError(getFieldName(), object); return; } if (fieldValue.getClass().isArray()) { Object[] values = (Object[]) fieldValue; for (Object value : values) { validateValue(object, value); } } else if (Collection.class.isAssignableFrom(fieldValue.getClass())) { Collection values = (Collection) fieldValue; for (Object value : values) { validateValue(object, value); } } else { validateValue(object, fieldValue); } } protected void validateValue(Object object, Object fieldValue) { try { setCurrentValue(fieldValue); if (fieldValue == null) { addFieldError(getFieldName(), object); return; } if (fieldValue instanceof String) { String stingValue = (String) fieldValue; if (trim) { stingValue = stingValue.trim(); } if (stingValue.length() == 0) { addFieldError(getFieldName(), object); } } else { addFieldError(getFieldName(), object); } } finally { setCurrentValue(null); } }

This is far more complicated than just checking if a string length is greater than zero. As well as checking a single value form field, it also checks if it's an array or collection and checks each entry. Multiple entry form fields are unusual enough they can be ignored by default and left to manual validation. If they should be considered, that should be handled by a base template class.

Further, the form field value must be manually converted to the expected type. It would be better if a validator defined what types it required and a framework converted it or logged if the linked annotation was applied to the wrong type. This also looks like a candidate for type parameters.

Complicated Custom Type Converters

At time of writing, the official documentation for type conversion is at the Core Developers Guide Type Conversion page and the section for 'Creating a Type Converter' starts with the following skeleton example.

public class MyConverter extends StrutsTypeConverter { public Object convertFromString(Map context, String[] values, Class toClass) { ..... } public String convertToString(Map context, Object o) { ..... } }

The first function can handle multiple form fields with the same name, not just one, and can be applied to various types. If there are multiple values to be converted to an array, the function is called for each entry instead. For single values, the value parameter is an array of just one value. Multiple values converted to a single value is so unusual, it's not worth complicating the normal case for it. Multiple types processed by the same type converter is also strange and better handled by a type converter for each type.

Applying a custom type converter using annotations is like the following.

@TypeConversion(converterClass = MyConverter.class)

Conclusion

In short, basic Struts validation works but if unusual forms or page layouts requires abandoning Struts UI tags or using custom type converters, validation design falls apart or becomes painful to code. Also, Annotation-based validation doesn't work correctly with ModelDriven, which is necessary for Post/Redirect/Post. This requires a redesign of annotation-based validation.

Next part

Continued in Alternate Design.