diff --git a/spring-webflow/src/main/java/org/springframework/webflow/validation/WebFlowMessageCodesResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/validation/WebFlowMessageCodesResolver.java index 46a08d17..0c2d46ca 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/validation/WebFlowMessageCodesResolver.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/validation/WebFlowMessageCodesResolver.java @@ -1,48 +1,140 @@ package org.springframework.webflow.validation; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.StringUtils; +import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; +/** + * Message Codes Resolver that implements the default Web Flow 2.x algorithm. The default algorithm differs from the + * Spring MVC {@link DefaultMessageCodesResolver} by appending the errorCode last instead of first. For + * example: myBean.myProperty.required instead of required.myBean.myProperty. + * + * Specifically: + *

+ * Will create two message codes for an object error, in the following order: + *

+ * + *

+ * Will create four message codes for a field error, in the following order: + *

+ * + *

+ * For example, in case of code "typeMismatch", object name "user", field "age" of type Integer: + *

+ * + * @author Keith Donald + */ public class WebFlowMessageCodesResolver implements MessageCodesResolver { - private String failureMessageCodePrefix = ""; + /** + * The separator that this implementation uses when resolving message codes. + */ + public static final String CODE_SEPARATOR = "."; + + private String prefix = ""; /** - * The prefix to prepend to all validation failure message codes. + * Specify a prefix to be applied to any code built by this resolver. + *

+ * Default is none. Specify, for example, "validation." to get error codes like "validation.name.typeMismatch". */ - public String getFailureMessageCodePrefix() { - return failureMessageCodePrefix; + public void setPrefix(String prefix) { + this.prefix = (prefix != null ? prefix : ""); } /** - * A prefix to prepend to all validation failure message codes; default if not set explicitly is "validation". - * @param failureMessageCodePrefix the failure message code prefix + * Return the prefix to be applied to any code built by this resolver. + *

+ * Returns an empty String in case of no prefix. */ - public void setFailureMessageCodePrefix(String failureMessageCodePrefix) { - this.failureMessageCodePrefix = failureMessageCodePrefix; + protected String getPrefix() { + return this.prefix; } public String[] resolveMessageCodes(String errorCode, String objectName) { - String constraintMessageCode = appendFailureMessageCodePrefix().append(codeSeparator()).append(errorCode) - .toString(); - String objectConstraintMessageCode = appendFailureMessageCodePrefix().append(codeSeparator()) - .append(objectName).append(codeSeparator()).append(errorCode).toString(); - return new String[] { objectConstraintMessageCode, constraintMessageCode }; + return new String[] { postProcessMessageCode(objectName + CODE_SEPARATOR + errorCode), + postProcessMessageCode(errorCode) }; } + /** + * Build the code list for the given code and field: an object/field-specific code, a field-specific code, a plain + * error code. + *

+ * Arrays, Lists and Maps are resolved both for specific elements and the whole collection. + *

+ * See the {@link DefaultMessageCodesResolver class level Javadoc} for details on the generated codes. + * @return the list of codes + */ public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class fieldType) { - String propertyConstraintMessageCode = appendFailureMessageCodePrefix().append(codeSeparator()).append( - objectName).append(codeSeparator()).append(field).append(codeSeparator()).append(errorCode).toString(); - String typeConstraintMessageCode = appendFailureMessageCodePrefix().append(codeSeparator()).append(fieldType) - .append(codeSeparator()).append(errorCode).toString(); - return new String[] { propertyConstraintMessageCode, typeConstraintMessageCode, propertyConstraintMessageCode }; + List codeList = new ArrayList(); + List fieldList = new ArrayList(); + buildFieldList(field, fieldList); + for (Iterator it = fieldList.iterator(); it.hasNext();) { + String fieldInList = (String) it.next(); + codeList + .add(postProcessMessageCode(objectName + CODE_SEPARATOR + fieldInList + CODE_SEPARATOR + errorCode)); + } + int dotIndex = field.lastIndexOf('.'); + if (dotIndex != -1) { + buildFieldList(field.substring(dotIndex + 1), fieldList); + } + for (Iterator it = fieldList.iterator(); it.hasNext();) { + String fieldInList = (String) it.next(); + codeList.add(postProcessMessageCode(fieldInList + CODE_SEPARATOR + errorCode)); + } + if (fieldType != null) { + codeList.add(postProcessMessageCode(fieldType.getName() + CODE_SEPARATOR + errorCode)); + } + codeList.add(postProcessMessageCode(errorCode)); + return StringUtils.toStringArray(codeList); } - protected StringBuilder appendFailureMessageCodePrefix() { - return new StringBuilder().append(failureMessageCodePrefix); + /** + * Add both keyed and non-keyed entries for the supplied field to the supplied field list. + */ + protected void buildFieldList(String field, List fieldList) { + fieldList.add(field); + String plainField = field; + int keyIndex = plainField.lastIndexOf('['); + while (keyIndex != -1) { + int endKeyIndex = plainField.indexOf(']', keyIndex); + if (endKeyIndex != -1) { + plainField = plainField.substring(0, keyIndex) + plainField.substring(endKeyIndex + 1); + fieldList.add(plainField); + keyIndex = plainField.lastIndexOf('['); + } else { + keyIndex = -1; + } + } } - protected char codeSeparator() { - return '.'; + /** + * Post-process the given message code, built by this resolver. + *

+ * The default implementation applies the specified prefix, if any. + * @param code the message code as built by this resolver + * @return the final message code to be returned + * @see #setPrefix + */ + protected String postProcessMessageCode(String code) { + return getPrefix() + code; } } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/validation/WebFlowMessageResolverTests.java b/spring-webflow/src/test/java/org/springframework/webflow/validation/WebFlowMessageResolverTests.java new file mode 100644 index 00000000..2ffb0ca4 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/validation/WebFlowMessageResolverTests.java @@ -0,0 +1,53 @@ +package org.springframework.webflow.validation; + +import junit.framework.TestCase; + +public class WebFlowMessageResolverTests extends TestCase { + private WebFlowMessageCodesResolver messageCodesResolver = new WebFlowMessageCodesResolver(); + + public void testResolveObjectMessageCodes() { + String[] codes = messageCodesResolver.resolveMessageCodes("required", "testBean"); + assertEquals(2, codes.length); + assertEquals("testBean.required", codes[0]); + assertEquals("required", codes[1]); + } + + public void testResolveObjectMessageCodesWithPrefix() { + messageCodesResolver.setPrefix("validation."); + String[] codes = messageCodesResolver.resolveMessageCodes("required", "testBean"); + assertEquals(2, codes.length); + assertEquals("validation.testBean.required", codes[0]); + assertEquals("validation.required", codes[1]); + } + + public void testResolveFieldMessageCodes() { + String[] codes = messageCodesResolver.resolveMessageCodes("required", "testBean", "foo", String.class); + assertEquals(4, codes.length); + assertEquals("testBean.foo.required", codes[0]); + assertEquals("foo.required", codes[1]); + assertEquals("java.lang.String.required", codes[2]); + assertEquals("required", codes[3]); + } + + public void testResolveFieldMessageCodesKeyedField() { + String[] codes = messageCodesResolver.resolveMessageCodes("required", "testBean", "foo[0]", String.class); + assertEquals(6, codes.length); + assertEquals("testBean.foo[0].required", codes[0]); + assertEquals("testBean.foo.required", codes[1]); + assertEquals("foo[0].required", codes[2]); + assertEquals("foo.required", codes[3]); + assertEquals("java.lang.String.required", codes[4]); + assertEquals("required", codes[5]); + } + + public void testResolveFieldMessageCodesWithPrefix() { + messageCodesResolver.setPrefix("validation."); + String[] codes = messageCodesResolver.resolveMessageCodes("required", "testBean", "foo", String.class); + assertEquals(4, codes.length); + assertEquals("validation.testBean.foo.required", codes[0]); + assertEquals("validation.foo.required", codes[1]); + assertEquals("validation.java.lang.String.required", codes[2]); + assertEquals("validation.required", codes[3]); + } + +}