Programming Thoughts
Struts 2 - Annotation-based Validation
Alternate Design

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.

Design Objectives

  • Interceptor based. Like the standard validators, adding to interceptor stacks adds to Actions without changing existing Action code.
  • Interceptor should be extensible. Odd and unanticipated requirements happen, so developers should be able to use a derived version of the interceptor instead.
  • Actual adjustment, validation and conversion should be separate classes. A statement of the obvious. The interceptor becomes the core framework with various adjusters, validators, and converters, collectively known as policies (the alternate name of the Strategy design pattern), attached to it. This is like the standard validator's design.
  • Annotation should use defaults where possible. As much boilerplate code should be removed as possible.
  • Policies don't apply if the form field value isn't set, unless overridden by annotation setting. The @Required annotation is the expected indicator where a field must be set.
  • Receiving form fields are character strings. This shifts formatting code to the form, where it's much more easily seen, and eliminates the per-class property files. It also eliminates the use of Struts data tags, besides property, and their inability to recognise failed conversion values.
  • Converters should have a reverse conversion function for form formatting. The standard validators also do this.
  • Form formatting should be automatic.
  • Compatible with manual conversion and validation. There are always weird exceptions to every rule, so it should not be required to write custom type converters or validator classes. Annotations can be omitted for a form field and manual code written in the Action's validate function.
  • Recognise ModelDriven. Use of ModelDriven allows a form to be stored in session as a POJO and recreated despite a refresh or redirection.
  • Annotation can configure that messages write to field errors, action errors or even action messages.
  • Abandon reliance on Struts UI tags. As HTML evolves, use of new features depends on support by the Struts UI tags, which is lagging at best. The FORM attribute for INPUT tags are still not supported, for example.
  • Configuration only in the form field annotations. Separate configuration files impedes readability, especially for others who don't understand Struts as well. This doesn't apply references to resource bundles, locale settings and the like as they already exist.
  • Custom conversion and validator annotations should even be able to use code in the same form. Referring to code in the same file aids readability.
  • A block of code in converters and validators should be able to report one of many error messages, whether from annotations or hardcoded. The standard validators have a single error condition but a few complex, custom ones can have multiple conditions and a different message for each.

Annotation Types

Experience has shown there are five types of annotation for form fields.

  • Adjuster - modifies value, such as trimming whitespace, before any validation or conversion.
  • Non-conversion validation - checks value, such as maximum length. Though typically for string values, runs before conversion for non-strings.
  • Conversion - converts string value into another data type and only one exists for a field. Does not run if any non-conversion validation rejects. Typically skipped for empty string values.
  • Post conversion validation - checks converted value, such as minimum value, and typically not run if conversion fails or is skipped.
  • N/A - annotations unrelated to validation.

Annotation Example Usages

As an objective is to reduce boilerplate code and arcane configuration, example designs of the annotations are considered first, such as below. The converted results are placed in a field of the same name but capitalised and prefixed with 'parsed'.

@Required(message = "An id is required.") @IntegerConversion(message = "Id must be a number") private String id; @Trim @Required(message = "A name is required.") private String name; @Required(message = "A 2 character shortcode is required. See ISO 3166.") @CustomValidation(message = "2 character shortcode must be exactly 2 characters", param1 = "2", validatorClass = SpecificLengthValidator.class) private String shortcode; private int parsedId;

Adjuster Annotation Example Specifications

A Trim annotation indicates a form field should have whitespace removed from the beginning and end, and has no settings. The specifications are simple.

@Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Trim { }

Non-conversion Validator Annotation Example Specifications

The Required annotation should obviously use the same localisation mechanism used by Struts and where to write should be configurable, defaulting to action errors. The shortCircuit attribute, if set to true and validation fails, stops further validation. The definition of the annotation becomes:-

@Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Required { public enum MessageType {ERROR, FIELD, MESSAGE} public String message() default ""; public String messageKey() default ""; public MessageType messageType() default MessageType.ERROR; public boolean shortCircuit() default true; }

Converter Annotation Example Specifications

A converter annotation indicates the form field value should be parsed into another field. The name of the target field is normally the source field name, capitalised, and prepended with 'parsed' but this can be overridden. The IntegerConversion annotation is below.

@Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface IntegerConversion { public String message() default ""; public String messageKey() default ""; public MessageType messageType() default MessageType.ERROR; public String parsedFieldName() default ""; }

Post-conversion Adjustor Annotation Example Specifications

Post-conversion adjusters are run after conversion and alter the source field. An example that sets the date to the last millisecond of the day is shown below:-

@Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ToEndOfDay { // Empty }

Post-conversion Validator Annotation Example Specifications

Post-conversion validators are run after conversion but are still applied to the source field. An example is shown below:-

@Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface IntegerRange { public int max(); public int min(); public String message() default ""; public String messageKey() default ""; public MessageType messageType() default MessageType.ERROR; public boolean shortCircuit() default false; }

Next part

Continued in Alternate Validators.