Programming Thoughts
Struts 2 - Annotation-based Validation
Form Formatting Redesign

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.

Basic Algorithm

As stated in Form Formatting, form formatting happens after the view Action has created them, so the interceptor does the work in a pre-result listener. The parsing of form field annotations has already been done in the Interceptor Redesign, so for each form to format, unless it's a injected, rejected form, call the converter or collection converter for each form field and write the results. For pair conversions, the result is set in the formatted half. For auto and default conversions, the results are written as Value Stack expression overrides, overriding the form's unformatted fields in the page.

For manual parameter conversion, as it's the form class that manually converts, it also manually formats. The following is an example of this.

@Override public FormatResult format() { FormattedDateComponents formattedDateComponents, startDateComponents, endDateComponents; FormatResult result; result = new FormatResult(); formattedDateComponents = formatDate(dateOfBirth); result.getFormattedSingleFieldValues().put("dayOfBirth", formattedDateComponents.getDay()); result.getFormattedSingleFieldValues().put("monthOfBirth", formattedDateComponents.getMonth()); result.getFormattedSingleFieldValues().put("yearOfBirth", formattedDateComponents.getYear()); return result; }

For each formatted value, the following code is called. It makes use of the ValueStack.setExprOverrides function to create a fake property. Override values are OGNL expressions, so formatted values are wrapped in double quotes to make thema string literal.

propertyName = actionField.getName() + "." + fieldContext.getFieldName(); propertyOverrides = new HashMap<>(); value = "\"" + StringEscapeUtils.escapeJava(String.valueOf(formattedValue)) + "\""; propertyOverrides.put(propertyName, value); valueStack.setExprOverrides(propertyOverrides);

For multiple parameters with the same name, the write function writes the OGNL expression for a list, as shown below.

propertyName = actionField.getName() + "." + fieldContext.getFieldName(); propertyOverrides = new HashMap<>(); first = true; propertyValue = "{"; for (String formattedValue: formattedValues) { if (!first) { propertyValue = propertyValue + ","; } value = "\"" + StringEscapeUtils.escapeJava(String.valueOf(formattedValue)) + "\""; propertyValue = propertyValue + value; first = false; } propertyValue = propertyValue + "}"; propertyOverrides.put(propertyName, propertyValue); valueStack.setExprOverrides(propertyOverrides);

The Problem with OGNL Expression Overrides

This works fine for parameters with different names but is a problem where multiple have the same name. The first tag iterates over each parameter whereas the second tries to retrieve the first parameter but produces nothing.

<s:iterator var="dayOfOffice" value="form.startDayOfOffice" status="loop"> <s:property value="form.startDayOfOffice[0]" />

This puzzling behaviour is explained by how OGNL expression overrides work. Rather than the first part of an expression accessing the overriding value as if it's a node in the object graph and any later part accessing its properties, the overriding value is created if an expression matches exactly. form.startDayOfOffice[0] does not exactly match form.startDayOfOffice.

The override value can be placed in a s:set variable and properties of that accessed but this imposes an unintuitive requirement that will be frequently forgotten. This problem also applies to formatted values with unique names as combining with operators and functions does not match exactly.

Note this behaviour applies to conversion errors as they're also written as expression overrides. This isn't considered a problem as form fields typically use field values as is and are single value. Where multiple values apply, forms are better off using string fields to receive unconverted values.

Reworking Formatted Values

Use of expression overrides must be abandoned. Formatted values can be placed in a map with field name as key, which is placed in a map using form names as keys. That is, form.dayOfBirth finds the fake form named 'form' in the outer map, then finds the fake form field named 'dayOfBirth' from the inner map. However, finding a node finds the first in the Value Stack and if it doesn't contain the expected field, other nodes sharing the same form name are not searched. The fake forms must contain the string field values, which don't need formatting, as well as formatted values.

The code fragments above are replaced with the following.

fakeForm = interceptorContext.getFakeForms().get(formName); if (fakeForm == null) { fakeForm = new HashMap<>(); interceptorContext.getFakeForms().put(formName, fakeForm); } fakeForm.put(fieldName, fieldContext.getFormattedValue()); fakeForm = interceptorContext.getFakeForms().get(formName); if (fakeForm == null) { fakeForm = new HashMap<>(); interceptorContext.getFakeForms().put(formName, fakeForm); } fakeForm.put(fieldName, fieldContext.getFormattedMultipleValues());

The last step of the formatter writes the fake map to the Value Stack.

protected void processFormattedValues() { InterceptorContext interceptorContext; ValueStack valueStack; Map<String,Map<String,Object>> fakeForms; interceptorContext = InterceptorContext.getInstance(); fakeForms = interceptorContext.getFakeForms(); ValidatorCommonLibrary.getActionContextFormattedForms().putAll(fakeForms); valueStack = interceptorContext.getInvocation().getStack(); valueStack.push(fakeForms); }

It also writes a copy to the ActionContext so Actions can access friendlier fake forms using ValidatorCommonLibrary.getFormattedForms().

Next part

Continued in Display Formatting.