Programming Thoughts
Struts 2 - Annotation-based Validation
Alternate Validators
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.
Policy Interfaces
Annotations are little more than highly restricted interfaces and cannot contain code, so each annotation class must have a complementary policy class, which is configured from the annotation instance. Struts 2 standard validators works in a similar way. One quirk of Java annotations is they cannot inherit, so there's no base interface for the core algorithm to read common attributes. Each policy must know its own particular annotation and read common attributes. The baseline definition of policy becomes:-
public interface Policy<A extends Annotation> { public A getAnnotation(); public void setAnnotation(A annotation); }
The simplest kind of policy are adjusters.
public interface Adjuster<A extends Annotation> extends Policy<A> {
public String adjust(String fieldValue);
public Class
public interface PostConversionAdjuster<A extends Annotation,T> extends Policy<A> { public T adjust(T formValue); public boolean getProcessNoValue(); }
Converters and validators write error messages, so they share a base interface.
public interface Validator<A extends Annotation> extends Policy<A> { public MessageType getMessageType(); public String getMessage(); public String getMessageKey(); }
Preliminary designs had converters and validators merely report success or failure and the framework read the message from the annotation if needed but the requirement to return various messages complicates this. The solution is the result class have static factory methods. These can have descriptive names and be as simple or complex as needed. If a member field is null, the policy didn't set it in its conversion or validation function, so they must be read later by the framework. Validator result is shown below. Getters and setters are omitted for brevity.
public class ValidationResult { public static ValidationResult makeSuccessResult() { ValidationResult result; result = new ValidationResult(); result.setSuccess(true); return result; } public static ValidationResult makeFailureResult() { ValidationResult result; result = new ValidationResult(); result.setSuccess(false); return new ValidationResult(false, null, null, null); } public static ValidationResult makeFailureWithMessageResult(String message, MessageType messageType) { ValidationResult result; result = new ValidationResult(); result.setSuccess(false); result.setMessage(message); result.setMessageType(messageType); return result; } public static ValidationResult makeFailureWithMessageKeyResult(String messageKey, MessageType messageType) { ValidationResult result; result = new ValidationResult(); result.setSuccess(false); result.setMessageKey(messageKey); result.setMessageType(messageType); return result; } private boolean success; private String message; private String messageKey; private MessageType messageType; private boolean shortCircuit; public ValidationResult() { super(); this.success = false; this.message = null; this.messageKey = null; this.messageType = null; this.shortCircuit = false; } public ValidationResult(boolean success, String message, String messageKey, MessageType messageType) { super(); this.success = success; this.message = message; this.messageKey = messageKey; this.messageType = messageType; this.shortCircuit = false; } }
The result of conversion is similar but has a generic type parameter of the converted type and includes the converted value, if successful. Only the key differences are shown below.
public class ConversionResult<T> { ... public static <T> ConversionResult<T> makeSuccessResult(T parsedValue) { ConversionResult<T> result; result = new ConversionResult<T>(); result.setSuccess(true); result.setParsedValue(parsedValue); return result; } ... private boolean success; private T parsedValue; public String message; public String messageKey; public MessageType messageType; }
The interfaces for validators and converters follow. The shortCircuit
property means failure
should stop further validation. The recipientClass
property is the data type the policy works
with and is necessary due to type erasure. Where the type is a wrapper class of a primitive type, fields
of the primitive type are also allowed.
public interface NonConversionValidator<A extends Annotation> extends Validator<A> { public boolean getShortCircuit(); public boolean getProcessNoValue(); public ValidationResult validate(String fieldValue); } public interface Converter<A extends Annotation,T> extends Validator<A> { public ConversionResult<T> convert(String fieldValue, Class<? extends T> recipientClass); public String format(T unformattedValue); public Class<T> getRecipientClass(); public String getRecipientFieldName(); public boolean getProcessNoValue(); } public interface PostConversionValidator<A extends Annotation,T> extends Validator<A> { public Class<T> getRecipientClass(); public boolean getShortCircuit(); public boolean getProcessNoValue(); public ValidationResult validate(T formValue); }
The recipientClass
parameter of the convert
function would seem redundant but it's
needed to the receive the enumerated type's class for enumerated type conversion. The code for it is shown below.
public ConversionResult<Enum> convert(String formValue, Class<? extends Enum> recipientClass) { Enum parsedValue; try { parsedValue = Enum.valueOf(recipientClass, formValue); return ConversionResult.makeSuccessResult(parsedValue); } catch (IllegalArgumentException e) { return ConversionResult.makeFailureResult(); } }
As an aside, the conversion function of the Struts 2 converter interface, StrutsTypeConverter
, is
shown below.
public abstract Object convertFromString(Map context, String[] values, Class toClass);
The first difference from the standard design is the context parameter is dropped. Its absence has never been
noticed and can be obtained using ActionContext.getContext()
anyway. For the second, the standard
design allows parsing of multiple form fields with the same name. This is just complicates code for the few,
very unusual form designs that do this. Where actually used, the client can drop to manual validation.
Finally, alternate design relies on type parameter to restrict results.
Validator Base Classes
The base implementation classes write themselves. They are here for the sake of completeness and can't do anything useful as there's no such thing as a base annotation. Of course, implementations need not derive from these.
public abstract class AbstractPolicySupport<A extends Annotation> implements Policy<A> { private A annotation; public AbstractPolicySupport() { super(); } public A getAnnotation() { return annotation; } public void setAnnotation(A annotation) { this.annotation = annotation; } } public abstract class AbstractAdjusterSupport extends AbstractAdjusterSupport<CustomAdjuster> { } public abstract class AbstractValidatorSupport<A extends Annotation> extends AbstractPolicySupport<A> implements Validator<A> { } public abstract class AbstractConverterSupport<A extends Annotation,T> extends AbstractValidatorSupport<A> implements Converter<A,T> { } public abstract class AbstractNonConversionValidatorSupport<A extends Annotation> extends AbstractValidatorSupport<A> implements NonConversionValidator<A> { } public abstract class AbstractPostConversionAdjusterSupport<A extends Annotation,T> extends AbstractPolicySupport<A> implements PostConversionAdjuster<A,T> { } public abstract class AbstractPostConversionValidatorSupport<A extends Annotation,T> extends AbstractValidatorSupport<A> implements PostConversionValidator<A,T> { }
Next part
Continued in Alternate Interceptor.