Programming Thoughts
Struts 2 - Annotation-based Validation
Alternate Validators (Custom)

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.

Annotations

A custom validator or converter should be as easy to write as possible, so programmers shouldn't write an annotation for it, just use an existing one. This creates a dilemma as it's impossible to anticipate the names and types of parameters to configure custom validators, so it's harder too see what each parameter means and some need conversion from string. In practice, validators usually don't need parameters, so generic annotations are used. There are:-

@Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface CustomConversion { public String message() default ""; public String messageKey() default ""; public MessageType messageType() default MessageType.ERROR; public String param1() default ""; public String param2() default ""; public String param3() default ""; public String param4() default ""; public String param5() default ""; public String parsedFieldName() default ""; public boolean processNoValue() default false; public Class<? extends Converter<CustomConversion>> validatorClass(); } @Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface CustomPostConversion { public String message() default ""; public String messageKey() default ""; public MessageType messageType() default MessageType.ERROR; public String param1() default ""; public String param2() default ""; public String param3() default ""; public String param4() default ""; public String param5() default ""; public boolean processNoValue() default false; public Class<? extends ConversionValidator<CustomPostConversion>> validatorClass(); } @Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface CustomValidation { public String message() default ""; public String messageKey() default ""; public MessageType messageType() default MessageType.ERROR; public String param1() default ""; public String param2() default ""; public String param3() default ""; public String param4() default ""; public String param5() default ""; public boolean processNoValue() default false; public Class<? extends ConversionValidator<CustomValidation>> validatorClass(); }

Validator Base Classes

The base implementation classes also write themselves and the base for custom converters shown below.

public abstract class AbstractCustomConverterSupport<T> extends AbstractConverterSupport<CustomConversion,T> { @Override public String getRecipientFieldName() { return getAnnotation().parsedFieldName(); } @Override public MessageType getMessageType() { return getAnnotation().messageType(); } @Override public String getMessage() { return getAnnotation().message(); } @Override public String getMessageKey() { return getAnnotation().messageKey(); } @Override public boolean getProcessNoValue() { return getAnnotation().processNoValue(); } }

Custom Converter Example

As one of the goals is to make custom converters and validators as easy as possible, an example allows us to finally assess this.

@CustomConversion(message = "Value for Custom Conversion (IP Address) Field 1 should be in n.n.n.n format", validatorClass=IPAddressValidator.class) private String field1; private InetAddress parsedField1; public class IPAddressValidator extends AbstractCustomConverterSupport<InetAddress> { @Override public Class<InetAddress> getRecipientClass() { return InetAddress.class; } @Override public String format(InetAddress unformattedValue) { return unformattedValue.getHostAddress(); } @Override public ConversionResult<InetAddress> convert(String formValue, Class<InetAddress> recipientClass) { InetAddress inetAddress; try { inetAddress = InetAddress.getByName(formValue); return ConversionResult.makeSuccessResult(inetAddress); } catch (UnknownHostException e) { return ConversionResult.makeFailureResult(); } } }

Custom Non-Conversion Validator Example

@CustomValidation(message = "Custom Valiator (SWIFT Characters) Field 1 can only use SWIFT compliant characters (alphanumeric, space and . , - ( ) / ' + :)", validatorClass = SWIFTValidator.class) private String field1; public static class SWIFTValidator extends AbstractCustomNonConversionValidatorSupport { @Override protected boolean isAllowed(String formValue, CustomValidation annotation) { return Pattern.matches("[a-zA-Z0-9/\\?\\-\\:\\(\\)\\.\\,\\'\\+ ]*", formValue); } }

Custom Post-Conversion Validator Example

Whereas the two examples above are from a test page, the following was retrofitted to existing code and sanity checks a date of birth. It re-uses an existing library function that reads the allowed age range from run-time parameters. To be needlessly user friendly, the validator ignores the annotation's message in favour of a calculated one.

@DateConversion(message = "The date of birth must be in the form dd/mm/yyyy.", format = "dd/MM/yyyy", parsedFieldName = "parsedDob") @CustomPostConversion(message = "Date of birth is absurd", validatorClass=DOBRangeValidator.class) private String dateOfBirth; private Date parsedDob = null; public static class DOBRangeValidator extends AbstractCustomPostConversionValidatorSupport<Date> { private Date dob; @Override protected boolean isAllowed(Date formValue, CustomPostConversion annotation) { dob = (Date)formValue; return MiscLibrary.isDOBAllowed(dob); } @Override public Class<Date> getRecipientClass() { return Date.class; } @Override public String getMessage() { int minAge, maxAge, age; minAge = ParameterLibrary.getAgeMin(); maxAge = ParameterLibrary.getAgeMax(); // Not strictly accurate, considering leap years, but it's for sanity checking age = (int)((System.currentTimeMillis() - dob.getTime()) / 31556952000L); if (age < 0) { return "The date of birth is in the future"; } else if (age < minAge) { return "Age of " + age + " is too young"; } else if (age > maxAge) { return "Age of " + age + " is too old"; } else { return "You should not see this message"; } } }

Next part

Continued in Alternate Validators (Collections).