Programming Thoughts
Struts 2 - Annotation-based Validation
Manual Parameter Conversion and Validation

Improving a fix to a Struts 2 feature

Struts 2 is a popular MVC framework for Java-based web applications but its annotation-based validation doesn't properly work. So, an alternative was created but it has design limitations. This article considers a redesign to overcome them.

Need for manual conversion and validation

A simple form, such as below, has multiple fields combine into a single value but annotations only apply to their own field. It's possible to redesign custom converters to read multiple fields but this would make them much more complex to write for a few cases.

Figure 1: Facebook birthday form

Manual parameter conversion

Form fields in the alternate design require an explicit converter annotation to avoid clashing with existing, manual conversion. Rewriting forms to remove the formatted field of a pair removes almost all such manual conversion, removing the need for an explicit annotation. Form fields of a recognised type but no conversion annotation can use a default converter. Where manual conversion of a recognised type is needed, a new @ManualParameterConversion annotation can skip automated conversion. Thus manual parameter conversion is needed for unrecognised data types with no custom converter, and where explicitly marked as manual.

The definition of a form's manual conversion function becomes the following.

public ManualParameterConversionResults manualParameterConvert(Map<String,String> unprocessedParameters, ValidationAware validationAware, TextProvider textProvider) public class ManualParameterConversionResults implements Serializable { private Set<String> processedParameterNames = new HashSet<>(); private Set<String> failedParameterNames = new HashSet<>(); public Set<String> getProcessedParameterNames() { return processedParameterNames; } public Set<String> getFailedParameterNames() { return failedParameterNames; } }

Failed request parameters are added to conversion errors, so they can be redisplayed.

This appears in the form's class, not the Action's class. This is incongruous with Strut's Action-centric design, especially as the validate function applies to Actions, but it should pair with form formatting functions. Form formatting applies to view Actions, not form processing Actions, and they can display multiple forms, so manual form formatting functions must be on the form and, thus, the manual conversion function with it.

View helpers

Abandoning reliance of Struts UI tags means complex HTML tags, such as SELECT, use view helpers in the view Action, which read form field values. They typically only present allowed values, so any conversion failure is a hacked value, not entered by a genuine user. However, manual parameter conversion forms may combine and convert multiple request parameters into a single value, such as seen in figure 1 above. If a combination, such as 31st of February, is rejected, the rejected values exist only as conversion errors, not a form field value.

Action code can access such conversion errors and example code is shown below.

formattedDateComponents = DateOfBirth2ManualParameterConversionForm.formatDate(value); actionContext = ActionContext.getContext(); conversionErrors = actionContext.getConversionErrors(); dayConversionData = conversionErrors.get("dateOfBirth2Form.day"); if (dayConversionData != null) { formattedDateComponents.setDay((String)dayConversionData.getValue()); } … dayOfMonthSelectBoxDisplay = new DayOfMonthSelectBoxDisplay(); dayOfMonthSelectBoxDisplay.setSelectedFormattedValue(formattedDateComponents.getDay());

This needs to be made easier, which can be done by moving conversion error checking code into the view helper, such as below.

public void setSelectedFormattedValueWithConversionError(String value, String conversionErrorName) { ActionContext actionContext; ConversionData conversionData; Map<String, ConversionData> conversionErrors; String rejectedValue; rejectedValue = null; actionContext = ActionContext.getContext(); if (actionContext != null) { conversionErrors = actionContext.getConversionErrors(); conversionData = conversionErrors.get(conversionErrorName); if (conversionData != null) { rejectedValue = (String)conversionData.getValue(); setSelectedFormattedValue(rejectedValue); } } if (rejectedValue == null) { setSelectedFormattedValue(value); } }

This turns client code into the following.

dayOfMonthSelectBoxDisplay = new DayOfMonthSelectBoxDisplay(); dayOfMonthSelectBoxDisplay.setSelectedFormattedValueWithConversionError(dateOfBirth2Components.getDay(), "dateOfBirth2Form.day");

Next part

Continued in Redesign Example.