diff --git a/core/src/main/java/org/apache/struts2/components/FieldError.java b/core/src/main/java/org/apache/struts2/components/FieldError.java index 65a742026c..8011bd875b 100644 --- a/core/src/main/java/org/apache/struts2/components/FieldError.java +++ b/core/src/main/java/org/apache/struts2/components/FieldError.java @@ -67,6 +67,11 @@ * * <s:fielderror fieldName="field1" /> * + * <!-- example 3 --> + * <s:fielderror regex="true"> + * <s:param>ledger\.records\[<s:property value="#row.index" />\]\..*</s:param> + * </s:fielderror> + * * * * @@ -78,6 +83,7 @@ * * Example 1: display all field errors
* Example 2: display field errors only for 'field1' and 'field2'
+ * Example 3: display field errors of all fields of one iterated row, matched by a regular expression
* * * @@ -89,6 +95,7 @@ public class FieldError extends UIBean implements UnnamedParametric { private final List errorFieldNames = new ArrayList<>(); private boolean escape = true; + private boolean regex = false; public FieldError(ValueStack stack, HttpServletRequest request, HttpServletResponse response) { super(stack, request, response); @@ -108,6 +115,7 @@ protected void evaluateExtraParams() { addParameter("errorFieldNames", errorFieldNames); addParameter("escape", escape); + addParameter("regex", regex); } public void addParameter(Object value) { @@ -129,5 +137,11 @@ public void setFieldName(String fieldName) { public void setEscape(boolean escape) { this.escape = escape; } + + @StrutsTagAttribute(description="Whether field names are regular expressions matched against the whole name of a field error", + type="Boolean", defaultValue="false") + public void setRegex(boolean regex) { + this.regex = regex; + } } diff --git a/core/src/main/java/org/apache/struts2/views/jsp/ui/FieldErrorTag.java b/core/src/main/java/org/apache/struts2/views/jsp/ui/FieldErrorTag.java index 0de0ba953c..b87f323d4e 100644 --- a/core/src/main/java/org/apache/struts2/views/jsp/ui/FieldErrorTag.java +++ b/core/src/main/java/org/apache/struts2/views/jsp/ui/FieldErrorTag.java @@ -36,6 +36,7 @@ public class FieldErrorTag extends AbstractUITag { protected String fieldName; protected boolean escape = true; + protected boolean regex = false; @Override @@ -50,6 +51,7 @@ protected void populateParams() { FieldError fieldError = ((FieldError) component); fieldError.setFieldName(this.fieldName); fieldError.setEscape(escape); + fieldError.setRegex(regex); } public void setFieldName(String fieldName) { @@ -60,6 +62,10 @@ public void setEscape(boolean escape) { this.escape = escape; } + public void setRegex(boolean regex) { + this.regex = regex; + } + /** * Must declare the setter at the descendant Tag class level in order for the tag handler to locate the method. */ @@ -76,6 +82,7 @@ protected void clearTagStateForTagPoolingServers() { super.clearTagStateForTagPoolingServers(); this.fieldName = null; this.escape = true; + this.regex = false; } } diff --git a/core/src/main/resources/template/html5/fielderror.ftl b/core/src/main/resources/template/html5/fielderror.ftl index 4b43045aaf..536214a161 100644 --- a/core/src/main/resources/template/html5/fielderror.ftl +++ b/core/src/main/resources/template/html5/fielderror.ftl @@ -25,7 +25,19 @@ <#assign doneEndUlTag=false><#t/> <#assign haveMatchedErrorField=false> <#if (fieldErrorFieldNames?size > 0) > -<#list fieldErrorFieldNames as fieldErrorFieldName> +<#if attributes.regex> +<#assign matchedFieldNames = []> +<#list eKeys as eKey> +<#list fieldErrorFieldNames as fieldNamePattern> +<#if eKey?matches(fieldNamePattern)> +<#assign matchedFieldNames = matchedFieldNames + [eKey]><#break> + + + +<#else> +<#assign matchedFieldNames = fieldErrorFieldNames> + +<#list matchedFieldNames as fieldErrorFieldName> <#list eKeys as eKey> <#if (eKey = fieldErrorFieldName)> <#assign haveMatchedErrorField=true> diff --git a/core/src/main/resources/template/simple/fielderror.ftl b/core/src/main/resources/template/simple/fielderror.ftl index 1a70e1f8c3..e303bce876 100644 --- a/core/src/main/resources/template/simple/fielderror.ftl +++ b/core/src/main/resources/template/simple/fielderror.ftl @@ -25,7 +25,19 @@ <#assign doneEndUlTag=false><#t/> <#assign haveMatchedErrorField=false><#t/> <#if (fieldErrorFieldNames?size > 0) ><#t/> - <#list fieldErrorFieldNames as fieldErrorFieldName><#t/> + <#if attributes.regex><#t/> + <#assign matchedFieldNames = []><#t/> + <#list eKeys as eKey><#t/> + <#list fieldErrorFieldNames as fieldNamePattern><#t/> + <#if eKey?matches(fieldNamePattern)><#t/> + <#assign matchedFieldNames = matchedFieldNames + [eKey]><#break><#t/> + <#t/> + <#t/> + <#t/> + <#else><#t/> + <#assign matchedFieldNames = fieldErrorFieldNames><#t/> + <#t/> + <#list matchedFieldNames as fieldErrorFieldName><#t/> <#list eKeys as eKey><#t/> <#if (eKey = fieldErrorFieldName)><#t/> <#assign haveMatchedErrorField=true><#t/> diff --git a/core/src/site/resources/tags/fielderror-attributes.html b/core/src/site/resources/tags/fielderror-attributes.html index c086b6e833..a653e5c861 100644 --- a/core/src/site/resources/tags/fielderror-attributes.html +++ b/core/src/site/resources/tags/fielderror-attributes.html @@ -261,6 +261,14 @@ Boolean Whether to clear all tag state during doEndTag() processing (if applicable) + + regex + false + false + false + Boolean + Whether field names are regular expressions matched against the whole name of a field error + requiredLabel false diff --git a/core/src/test/java/org/apache/struts2/views/jsp/ui/FieldErrorTagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/ui/FieldErrorTagTest.java index 4891fcfcbb..eb418c0d24 100644 --- a/core/src/test/java/org/apache/struts2/views/jsp/ui/FieldErrorTagTest.java +++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/FieldErrorTagTest.java @@ -898,6 +898,86 @@ public void testWithSpecificFieldName_html5() throws Exception { strutsBodyTagsAreReflectionEqual(tag, freshTag)); } + public void testRegexMatchesAllFieldsOfOneRow() throws Exception { + String output = renderLedgerErrors(null, true, "ledger\\.records\\[1\\]\\..*"); + + assertEquals(normalize("", true), + normalize(output, true)); + } + + public void testRegexMatchesAllFieldsOfOneRow_html5() throws Exception { + String output = renderLedgerErrors("html5", true, "ledger\\.records\\[1\\]\\..*"); + + assertEquals(normalize("", true), + normalize(output, true)); + } + + public void testRegexWithoutMatchRendersNothing() throws Exception { + assertEquals("", normalize(renderLedgerErrors(null, true, "ledger\\.records\\[7\\]\\..*"), true)); + assertEquals("", normalize(renderLedgerErrors("html5", true, "ledger\\.records\\[7\\]\\..*"), true)); + } + + public void testRegexMatchesWholeFieldName() throws Exception { + assertEquals("", normalize(renderLedgerErrors(null, true, "ledger\\.records\\[1\\]"), true)); + assertEquals("", normalize(renderLedgerErrors("html5", true, "ledger\\.records\\[1\\]"), true)); + } + + public void testRegexFieldMatchedByTwoPatternsIsRenderedOnce() throws Exception { + String expected = normalize("", true); + + assertEquals(expected, normalize(renderLedgerErrors(null, true, ".*\\.amount", "ledger\\.records\\[1\\]\\..*"), true)); + assertEquals(expected, normalize(renderLedgerErrors("html5", true, ".*\\.amount", "ledger\\.records\\[1\\]\\..*"), true)); + } + + public void testWithoutRegexPatternIsMatchedLiterally() throws Exception { + assertEquals("", normalize(renderLedgerErrors(null, false, "ledger\\.records\\[1\\]\\..*"), true)); + assertEquals("", normalize(renderLedgerErrors("html5", false, "ledger\\.records\\[1\\]\\..*"), true)); + } + + public void testRegexIsClearedForTagPoolingServers() throws Exception { + FieldErrorTag tag = new FieldErrorTag(); + tag.setPerformClearTagStateForTagPoolingServers(true); + tag.setRegex(true); + tag.setPageContext(pageContext); + tag.doStartTag(); + setComponentTagClearTagState(tag, true); + tag.doEndTag(); + + FieldErrorTag freshTag = new FieldErrorTag(); + freshTag.setPerformClearTagStateForTagPoolingServers(true); + freshTag.setPageContext(pageContext); + assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", + strutsBodyTagsAreReflectionEqual(tag, freshTag)); + } + + private String renderLedgerErrors(String theme, boolean regex, String... fieldNames) throws Exception { + TestAction testAction = new TestAction(); + testAction.addFieldError("ledger.records[0].amount", "amount 0"); + testAction.addFieldError("ledger.records[1].amount", "amount 1"); + testAction.addFieldError("ledger.records[1].date", "date 1"); + stack.pop(); + stack.push(testAction); + writer.getBuffer().setLength(0); + + FieldErrorTag tag = new FieldErrorTag(); + if (theme != null) { + tag.setTheme(theme); + } + tag.setRegex(regex); + tag.setPageContext(pageContext); + tag.doStartTag(); + for (String fieldName : fieldNames) { + ParamTag paramTag = new ParamTag(); + paramTag.setPageContext(pageContext); + paramTag.setValue("%{'" + fieldName.replace("\\", "\\\\") + "'}"); + paramTag.doStartTag(); + paramTag.doEndTag(); + } + tag.doEndTag(); + return writer.toString(); + } + @Override public Action getAction() { return new InternalAction(); diff --git a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/FieldErrorHandler.java b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/FieldErrorHandler.java index aae61fa353..b32da9cead 100644 --- a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/FieldErrorHandler.java +++ b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/FieldErrorHandler.java @@ -24,6 +24,8 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; public class FieldErrorHandler extends AbstractTagHandler implements TagGenerator { public void generate() throws IOException { @@ -34,8 +36,12 @@ public void generate() throws IOException { if (fieldErrorFieldNames != null && !fieldErrorFieldNames.isEmpty() && errors != null && !errors.isEmpty()) { startUL(params); + List matchedFieldNames = Boolean.TRUE.equals(params.get("regex")) + ? matchFieldNames(fieldErrorFieldNames, errors.keySet()) + : fieldErrorFieldNames; + //iterate over field error names - for (String fieldErrorFieldName : fieldErrorFieldNames) { + for (String fieldErrorFieldName : matchedFieldNames) { List fieldErrors = errors.get(fieldErrorFieldName); if (fieldErrors != null) { for (String fieldError : fieldErrors) { @@ -58,6 +64,13 @@ public void generate() throws IOException { } } + private List matchFieldNames(List fieldNamePatterns, Set fieldNames) { + List patterns = fieldNamePatterns.stream().map(Pattern::compile).toList(); + return fieldNames.stream() + .filter(fieldName -> patterns.stream().anyMatch(pattern -> pattern.matcher(fieldName).matches())) + .toList(); + } + private void endUL() throws IOException { end("ul"); } diff --git a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java index 686fe047a7..cd83acb68b 100644 --- a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java +++ b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java @@ -87,6 +87,41 @@ public void testRenderActionErrorNoErrors() { assertEquals("", output); } + public void testRenderFieldErrorWithRegex() { + this.errors.put("field10", Arrays.asList("too long")); + tag.setRegex(true); + tag.setFieldName("field1.*"); + tag.setFieldName("field1"); + + tag.evaluateParams(); + map.putAll(tag.getAttributes()); + theme.renderTag(getTagName(), context); + String output = writer.getBuffer().toString(); + String expected = s("
  • not good
  • bad
  • too long
"); + assertEquals(expected, output); + } + + public void testRenderFieldErrorWithRegexWithoutMatch() { + tag.setRegex(true); + tag.setFieldName("field"); + + tag.evaluateParams(); + map.putAll(tag.getAttributes()); + theme.renderTag(getTagName(), context); + String output = writer.getBuffer().toString(); + assertEquals(s("
    "), output); + } + + public void testRenderFieldErrorWithoutRegexMatchesLiterally() { + tag.setFieldName("field."); + + tag.evaluateParams(); + map.putAll(tag.getAttributes()); + theme.renderTag(getTagName(), context); + String output = writer.getBuffer().toString(); + assertEquals(s("
      "), output); + } + @Override protected void setUp() throws Exception { this.errors = new LinkedHashMap>() {