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@interfaceSpecificLength { 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> {@Overridepublic MessageType getMessageType() { return getAnnotation().messageType(); }@Overridepublic String getMessage() { return getAnnotation().message(); }@Overridepublic String getMessageKey() { return getAnnotation().messageKey(); }@Overridepublic boolean getShortCircuit() { return getAnnotation().shortCircuit(); }@Overridepublic boolean getProcessNoValue() { return false; }@Overridepublic 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 |
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@interfaceAgeRange { 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;@Overridepublic Class<Date> getRecipientClass() { return Date.class; }@Overridepublic 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(); } }@Overridepublic MessageType getMessageType() { return getAnnotation().messageType(); }@Overridepublic String getMessage() { switch (ageRangeRejection) { case MAX_AGE: return getAnnotation().maxAgeMessage(); case MIN_AGE: return getAnnotation().minAgeMessage(); case NONE: return ""; } return ""; }@Overridepublic String getMessageKey() { switch (ageRangeRejection) { case MAX_AGE: return getAnnotation().maxAgeMessageKey(); case MIN_AGE: return getAnnotation().minAgeMessageKey(); case NONE: return ""; } return ""; }@Overridepublic boolean getShortCircuit() { return getAnnotation().shortCircuit(); }@Overridepublic 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.
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 |
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 |
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 |
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.
@ThousandIntegerConversionprivate static boolean annotationPlaceholder;@Overrideprotected ThousandIntegerConversion makeDefaultAnnotation() { try { return ThousandIntegerConverter.class.getDeclaredField("annotationPlaceholder").getAnnotation(ThousandIntegerConversion.class); } catch (Exception e) { LOG.error("Creation of default annotation failed", e); return null; } }