Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions core/src/main/java/org/apache/struts2/components/FieldError.java
Original file line number Diff line number Diff line change
Expand Up @@ -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>
*
* <!-- END SNIPPET: example -->
* </pre>
*
Expand All @@ -78,6 +83,7 @@
*
* Example 1: display all field errors<br>
* Example 2: display field errors only for 'field1' and 'field2'<br>
* Example 3: display field errors of all fields of one iterated row, matched by a regular expression<br>
*
* <!-- END SNIPPET: description -->
* </pre>
Expand All @@ -89,6 +95,7 @@ public class FieldError extends UIBean implements UnnamedParametric {

private final List<String> errorFieldNames = new ArrayList<>();
private boolean escape = true;
private boolean regex = false;

public FieldError(ValueStack stack, HttpServletRequest request, HttpServletResponse response) {
super(stack, request, response);
Expand All @@ -108,6 +115,7 @@ protected void evaluateExtraParams() {
addParameter("errorFieldNames", errorFieldNames);

addParameter("escape", escape);
addParameter("regex", regex);
}

public void addParameter(Object value) {
Expand All @@ -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;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class FieldErrorTag extends AbstractUITag {

protected String fieldName;
protected boolean escape = true;
protected boolean regex = false;


@Override
Expand All @@ -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) {
Expand All @@ -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.
*/
Expand All @@ -76,6 +82,7 @@ protected void clearTagStateForTagPoolingServers() {
super.clearTagStateForTagPoolingServers();
this.fieldName = null;
this.escape = true;
this.regex = false;
}

}
14 changes: 13 additions & 1 deletion core/src/main/resources/template/html5/fielderror.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
14 changes: 13 additions & 1 deletion core/src/main/resources/template/simple/fielderror.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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/>
Expand Down
8 changes: 8 additions & 0 deletions core/src/site/resources/tags/fielderror-attributes.html
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@
<td class="tag-attribute">Boolean</td>
<td class="tag-attribute">Whether to clear all tag state during doEndTag() processing (if applicable)</td>
</tr>
<tr>
<td class="tag-attribute">regex</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Boolean</td>
<td class="tag-attribute">Whether field names are regular expressions matched against the whole name of a field error</td>
</tr>
<tr>
<td class="tag-attribute">requiredLabel</td>
<td class="tag-attribute">false</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<ul class=\"errorMessage\"><li><span>amount 1</span></li><li><span>date 1</span></li></ul>", true),
normalize(output, true));
}

public void testRegexMatchesAllFieldsOfOneRow_html5() throws Exception {
String output = renderLedgerErrors("html5", true, "ledger\\.records\\[1\\]\\..*");

assertEquals(normalize("<ul class=\"errorMessage\"><li><span>amount 1</span></li><li><span>date 1</span></li></ul>", 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("<ul class=\"errorMessage\"><li><span>amount 0</span></li><li><span>amount 1</span></li><li><span>date 1</span></li></ul>", 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,8 +36,12 @@ public void generate() throws IOException {
if (fieldErrorFieldNames != null && !fieldErrorFieldNames.isEmpty() && errors != null && !errors.isEmpty()) {
startUL(params);

List<String> 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<String> fieldErrors = errors.get(fieldErrorFieldName);
if (fieldErrors != null) {
for (String fieldError : fieldErrors) {
Expand All @@ -58,6 +64,13 @@ public void generate() throws IOException {
}
}

private List<String> matchFieldNames(List<String> fieldNamePatterns, Set<String> fieldNames) {
List<Pattern> 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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<ul class='errorMessage'><li><span>not good</span></li><li><span>bad</span></li><li><span>too long</span></li></ul>");
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("<ul class='errorMessage'></ul>"), output);
}

public void testRenderFieldErrorWithoutRegexMatchesLiterally() {
tag.setFieldName("field.");

tag.evaluateParams();
map.putAll(tag.getAttributes());
theme.renderTag(getTagName(), context);
String output = writer.getBuffer().toString();
assertEquals(s("<ul class='errorMessage'></ul>"), output);
}

@Override
protected void setUp() throws Exception {
this.errors = new LinkedHashMap<String, List<String>>() {
Expand Down
Loading