Form Field Annotations - Custom and Bespoke

Introduction

Custom and bespoke annotations and policies are client created in addition to the built-in ones. Custom policies are easier to create but the annotation isn't as easy to read and configure compared to bespoke ones, which each have their own dedicated annotation but a bit of application configuration is needed.

For example, the code below shows a custom validator and the annotation to use it.

public class UpdatePopupCountryForm extends AbstractForm {

    public static class SpecificLengthValidator extends AbstractCustomNonConversionValidatorSupport {



        @Override

        public ValidationResult validate(String formValue) {

            int length;

            

            length = Integer.parseInt(getAnnotation().param1());

            if (formValue.length() == length) {

                return ValidationResult.makeSuccessResult();

            } else {

                return ValidationResult.makeFailureResult();

            }

        }

        

    }

    ...

    @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;

    ...

}

The code below shows the same using a bespoke validator.

@Documented

@Inherited

@Target(ElementType.FIELD)

@Retention(RetentionPolicy.RUNTIME)

public @interface SpecificLength {

    public int length(); 

    public String message() default "";

    public String messageKey() default "";

    public MessageType messageType() default MessageType.ERROR;

    public boolean shortCircuit() default false;

}

...

public class SpecificLengthValidator extends AbstractNonConversionValidatorSupport<SpecificLength> {

    @Override

    public MessageType getMessageType() {

        return getAnnotation().messageType();

    }



    @Override

    public String getMessage() {

        return getAnnotation().message();

    }



    @Override

    public String getMessageKey() {

        return getAnnotation().messageKey();

    }



    @Override

    public boolean getShortCircuit() {

        return getAnnotation().shortCircuit();

    }



    @Override

    public boolean getProcessNoValue() {

        return false;

    }



    @Override

    public ValidationResult validate(String formValue) throws Exception {

        if (formValue.length() == getAnnotation().getLength()) {

            return ValidationResult.makeSuccessResult();

        } else {

            return ValidationResult.makeFailureResult();

        }

    }



}

...  

public class UpdatePopupCountryForm extends AbstractForm {

    ...

    @Required(message = "A 2 character shortcode is required.  See ISO 3166.")

    @SpecificLength(message = "2 character shortcode must be exactly 2 characters", length = 2)

    private String shortcode;

    ...

}

Options

Short circuit. If validation fails, consider if later validators should bother to run. This should generally default to false and be left to the form designer to override but it can default to true or be hardcoded. The Required validator defaults to true as if a user enters no value, there's little point telling him it's too short as well, for example.

Process no value. This generally means whether the policy is run at all if the user doesn't enter a value or conversion fails. As code typically assumes non-empty string or non-null value, this is typically missing from annotations and the policy implementation hardcodes false value. The exact meaning depends on policy type.

Policy type Different conditions
Adjuster Value can be empty string
Non-conversion validator Value can be empty string
Converter Value can be empty string or whitespace only
Post-conversion adjuster Conversion failed or field value is null
Post-conversion validator Conversion failed or field value is null
Collection converter Value can be empty string
Collection post-conversion adjuster Collection conversion failed or field value is null
Collection post-conversion adjuster Collection conversion failed or field value is null
No process value meaning

Different message options. Built-in policies only have one error message option but this is only done for simplicity and is not a design limitation. Policies are created every request, so are thread safe. Thus, the main adjust, convert, and validate functions may set member fields for getMessageType(), getMessage(), and getMessageKey(). This works best for bespoke annotations. For example:-

@Documented

@Inherited

@Target(ElementType.FIELD)

@Retention(RetentionPolicy.RUNTIME)

public @interface AgeRange {

    public int minAge(); 

    public int maxAge(); 

    public String minAgeMessage() default "";

    public String minAgeMessageKey() default "";

    public String maxAgeMessage() default "";

    public String maxAgeMessageKey() default "";

    public MessageType messageType() default MessageType.ERROR;

    public boolean shortCircuit() default false;

}



public class AgeRangeValidator extends AbstractPostConversionValidatorSupport<AgeRange,Date> {

    public enum AgeRangeRejection {NONE, MIN_AGE, MAX_AGE}

    

    private AgeRangeRejection ageRangeRejection = AgeRangeRejection.NONE;

    

    @Override

    public Class<Date> getRecipientClass() {

        return Date.class;

    }

    

    @Override

    public ValidationResult validate(Date fieldValue) {

        int minAge, maxAge, age;

        

        minAge = getAnnotation().minAge();

        maxAge = getAnnotation().maxAge();

        // Not strictly accurate, considering leap years, but it's for sanity checking

        age = (int)((System.currentTimeMillis() - fieldValue.getTime()) / 31556952000L);

        if (age < minAge) {

            ageRangeRejection = AgeRangeRejection.MIN_AGE;

            return ValidationResult.makeFailureResult();

        } else if (age > maxAge) {

            ageRangeRejection = AgeRangeRejection.MAX_AGE;

            return ValidationResult.makeFailureResult();

        } else {

            return ValidationResult.makeSuccessResult();

        }

    }



    @Override

    public MessageType getMessageType() {

        return getAnnotation().messageType();

    }



    @Override

    public String getMessage() {

        switch (ageRangeRejection) {

        case MAX_AGE:   return getAnnotation().maxAgeMessage();

        case MIN_AGE:   return getAnnotation().minAgeMessage();

        case NONE:      return "";

        }

        return "";

    }



    @Override

    public String getMessageKey() {

        switch (ageRangeRejection) {

        case MAX_AGE:   return getAnnotation().maxAgeMessageKey();

        case MIN_AGE:   return getAnnotation().minAgeMessageKey();

        case NONE:      return "";

        }

        return "";

    }



    @Override

    public boolean getShortCircuit() {

        return getAnnotation().shortCircuit();

    }



    @Override

    public boolean getProcessNoValue() {

        return false;

    }



}

...

@DateConversion(messageKey = "Date of Birth, if set, must be in dd/mm/yyyy format")

@AgeRange(minAge = 30, minAgeMessage = "Date of birth is unrealistically young for a Prime Minister", maxAge = 150, maxAgeMessage = "Date of birth is too old for a Prime Minister of the late 20th/early 21st century")

private Date dateOfBirth;

...

Custom policies

Custom policies are easiest to create. Depending on the type, the policy implementation should derive from the appropriate Template class and have a public default constructor. Alternatively, implementations can directly implement the interface but this is not recommended.

Policy type Base Template class Form field annotation Alternative, direct interface
Adjuster AbstractCustomAdjusterSupport CustomAdjuster Adjuster
Non-conversion validator AbstractCustomNonConversionValidatorSupport CustomValidation NonConversionValidator
Converter AbstractCustomConverterSupport CustomConverter Converter
Post-conversion adjuster AbstractCustomPostConversionAdjusterSupport CustomPostConversionAdjuster PostConversionAdjuster
Post-conversion validator AbstractCustomPostConversionValidatorSupport CustomPostConversionValidation PostConversionValidator
Collection converter AbstractCustomCollectionConverterSupport CustomCollectionConversion CollectionConverter
Collection post-conversion adjuster AbstractCustomCollectionPostConversionAdjusterSupport CustomCollectionPostConversionAdjuster CollectionPostConversionAdjuster
Collection post-conversion validator AbstractCustomCollectionPostConversionValidatorSupport CustomCollectionPostConversionValidation CollectionPostConversionValidator
Custom policy base Template classes

If creating a custom converter or collection converter for only formatting values, simpler base Template classes are available. Implementing the interface directly is not recommended even more.

Policy type Base Template class Form field annotation Alternative, direct interface
Converter AbstractCustomFormatterSupport CustomConverter Converter
Collection converter AbstractCustomCollectionFormatterSupport CustomCollectionConversion CollectionConverter
Custom policy base Template classes for formatting only

Bespoke policies

Whereas custom annotations refer to their implementing policy, bespoke annotations do not and their policies must be known beforehand. As scanning an entire classpath to find them can create a noticeable, roughly three second delay, this is disabled by default and it's recommended to only enable it restricted to known packages. In the application's struts.xml, add or edit properties like the following.

<constant name="name.matthewgreet.strutscommons.accept_classes" value="" />

<constant name="name.matthewgreet.strutscommons.accept_packages" value="name.matthewgreet.example11.policy" />

<constant name="name.matthewgreet.strutscommons.classpath_scanning_replace_built_in" value="false" />

<constant name="name.matthewgreet.strutscommons.enable_classpath_scanning" value="true" />

<constant name="name.matthewgreet.strutscommons.reject_classes" value="" />

<constant name="name.matthewgreet.strutscommons.reject_packages" value="" />

If the classpath_scanning_replace_built_in property is set to true, bespoke default converters found by classpath scanning will replace built-in default converters of the same field type. This is not recommended unless every use of the default converter is checked.

Alternatively, policies can be manually added with a servlet startup listener, such as below, but this is tedious.

<web-app>

    ...

    <listener>

      <listener-class>name.matthewgreet.experiment3.util.StartupListener</listener-class>

    </listener>

    ...

</web-app>



public class StartupListener implements ServletContextListener {

    @Override

    public void contextInitialized(ServletContextEvent sce) {

        DefaultPolicyLookup defaultPolicyLookup;

        

        defaultPolicyLookup = DefaultPolicyLookup.getInstance();

        try {

            defaultPolicyLookup.putPolicy(CurrencyConverter.class);

            defaultPolicyLookup.putPolicy(ThousandIntegerConverter.class);

            

            defaultPolicyLookup.putDefaultConverter(ThousandIntegerConverter.class);

        }

        catch (PolicyLookupRejectionException e) {

            e.printStackTrace();

        }

    }

}

Unlike custom policies, bespoke policies have their own, dedicated annotation. Design the annotation first, based on the example in the Introduction. Depending on the type, the policy implementation should derive from the appropriate Template class and have a public default constructor. Alternatively, implementations can directly implement the interface but this is not recommended.

Policy type Base Template class Alternative, direct interface
Adjuster AbstractAdjusterSupport Adjuster
Non-conversion validator AbstractNonConversionValidatorSupport NonConversionValidator
Converter AbstractConverterSupport Converter
Post-conversion adjuster AbstractPostConversionAdjusterSupport PostConversionAdjuster
Post-conversion validator AbstractPostConversionValidatorSupport PostConversionValidator
Collection converter AbstractCollectionConverterSupport CollectionConverter
Collection post-conversion adjuster AbstractCollectionPostConversionAdjusterSupport CollectionPostConversionAdjuster
Collection post-conversion validator AbstractCollectionPostConversionValidatorSupport CollectionPostConversionValidator
Bespoke policy base Template classes

Default converters and collection converters

Default converters and collection converters are built-in and bespoke converters and collection converters that are used where a field (except single value string) lacks an explicit converter annotation. They must be registered for its field type and only one can apply per field type. They are automatically registered if classpath scanning is enabled and manually by calling DefaultPolicyLookup.putDefaultConverter and putDefaultCollectionConverter. If multiple are automatically found for a field type, the one registered is arbitrary.

Unlike custom policies, bespoke policies have their own, dedicated annotation. Design the annotation first, based on the example in the Introduction. Depending on the type, the policy implementation should derive from the appropriate Template class and have a public default constructor. Alternatively, implementations can directly implement the interface but this is not recommended.

Policy type Base Template class Alternative, direct interfaces
Converter AbstractDefaultConverterSupport Converter and DefaultPolicy
Collection converter AbstractDefaultCollectionConverterSupport CollectionConverter and DefaultPolicy
Default converter base Template classes

To be a default converter or collection converter, it must implement the function to return the default version of its annotation. As annotations cannot be constructed at run--time, only compile time versions referenced, use code like the following.

@ThousandIntegerConversion

private static boolean annotationPlaceholder;



@Override

protected ThousandIntegerConversion makeDefaultAnnotation() {

    try {

        return ThousandIntegerConverter.class.getDeclaredField("annotationPlaceholder").getAnnotation(ThousandIntegerConversion.class);

    }

    catch (Exception e) {

        LOG.error("Creation of default annotation failed", e);

        return null;

    }

}