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>
+#if>
+#list>
+#list>
+<#else>
+<#assign matchedFieldNames = fieldErrorFieldNames>
+#if>
+<#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/>
+ #if><#t/>
+ #list><#t/>
+ #list><#t/>
+ <#else><#t/>
+ <#assign matchedFieldNames = fieldErrorFieldNames><#t/>
+ #if><#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("");
+ 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>() {