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.