This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feat/WW-3429-configurable-checkbox-prefix in repository https://gitbox.apache.org/repos/asf/struts.git
commit 8b8d8e6c1fe37e52812f586b437704ae1790a366 Author: Lukasz Lenart <[email protected]> AuthorDate: Fri Feb 6 15:44:40 2026 +0100 feat(ui): WW-3429 add configurable checkbox hidden field prefix Add struts.ui.checkbox.hiddenPrefix constant to allow configuring the checkbox hidden field prefix, addressing HTML validation warnings about double underscores while maintaining backward compatibility. Changes: - Add STRUTS_UI_CHECKBOX_HIDDEN_PREFIX constant to StrutsConstants - Add default value __checkbox_ to default.properties - Update Checkbox component to inject and pass prefix to templates - Update CheckboxInterceptor to use configurable prefix - Update simple/checkbox.ftl and html5/checkbox.ftl templates - Update CheckboxHandler in javatemplates plugin - Add tests for configurable prefix functionality - Fix bug in CheckboxHandler where value was incorrectly prefixed Configuration example: struts.ui.checkbox.hiddenPrefix=struts_checkbox_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --- .../java/org/apache/struts2/StrutsConstants.java | 9 + .../org/apache/struts2/components/Checkbox.java | 21 +- .../struts2/interceptor/CheckboxInterceptor.java | 29 +- .../org/apache/struts2/default.properties | 4 + .../src/main/resources/template/html5/checkbox.ftl | 2 +- .../main/resources/template/simple/checkbox.ftl | 2 +- .../interceptor/CheckboxInterceptorTest.java | 320 ++++++++++++--------- .../struts2/views/java/simple/CheckboxHandler.java | 30 +- .../struts2/views/java/simple/CheckboxTest.java | 4 +- .../2026-02-06-WW-3429-checkbox-prefix-constant.md | 196 +++++++++++++ 10 files changed, 456 insertions(+), 161 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index a66478a97..4feefd0d8 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -701,6 +701,15 @@ public final class StrutsConstants { */ public static final String STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED = "struts.ui.checkbox.submitUnchecked"; + /** + * The prefix used for hidden checkbox fields to track unchecked values. + * Default is "__checkbox_" for backward compatibility. + * Set to "struts_checkbox_" to avoid HTML validation warnings about double underscores. + * + * @since 7.2.0 + */ + public static final String STRUTS_UI_CHECKBOX_HIDDEN_PREFIX = "struts.ui.checkbox.hiddenPrefix"; + /** * See {@link org.apache.struts2.interceptor.exec.ExecutorProvider} */ diff --git a/core/src/main/java/org/apache/struts2/components/Checkbox.java b/core/src/main/java/org/apache/struts2/components/Checkbox.java index 87c400d01..2720377dc 100644 --- a/core/src/main/java/org/apache/struts2/components/Checkbox.java +++ b/core/src/main/java/org/apache/struts2/components/Checkbox.java @@ -49,17 +49,19 @@ import jakarta.servlet.http.HttpServletResponse; * </pre> */ @StrutsTag( - name = "checkbox", - tldTagClass = "org.apache.struts2.views.jsp.ui.CheckboxTag", - description = "Render a checkbox input field", - allowDynamicAttributes = true) + name = "checkbox", + tldTagClass = "org.apache.struts2.views.jsp.ui.CheckboxTag", + description = "Render a checkbox input field", + allowDynamicAttributes = true) public class Checkbox extends UIBean { private static final String ATTR_SUBMIT_UNCHECKED = "submitUnchecked"; + private static final String ATTR_HIDDEN_PREFIX = "hiddenPrefix"; public static final String TEMPLATE = "checkbox"; private String submitUncheckedGlobal; + private String hiddenPrefixGlobal = "__checkbox_"; protected String fieldValue; protected String submitUnchecked; @@ -87,6 +89,8 @@ public class Checkbox extends UIBean { } else { addParameter(ATTR_SUBMIT_UNCHECKED, false); } + + addParameter(ATTR_HIDDEN_PREFIX, hiddenPrefixGlobal); } @Override @@ -99,14 +103,19 @@ public class Checkbox extends UIBean { this.submitUncheckedGlobal = submitUncheckedGlobal; } + @Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false) + public void setHiddenPrefixGlobal(String hiddenPrefixGlobal) { + this.hiddenPrefixGlobal = hiddenPrefixGlobal; + } + @StrutsTagAttribute(description = "The actual HTML value attribute of the checkbox.", defaultValue = "true") public void setFieldValue(String fieldValue) { this.fieldValue = fieldValue; } @StrutsTagAttribute(description = "If set to true, unchecked elements will be submitted with the form. " + - "Since Struts 6.1.1 you can use a constant \"" + StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + "\" to set this attribute globally", - type = "Boolean", defaultValue = "false") + "Since Struts 6.1.1 you can use a constant \"" + StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + "\" to set this attribute globally", + type = "Boolean", defaultValue = "false") public void setSubmitUnchecked(String submitUnchecked) { this.submitUnchecked = submitUnchecked; } diff --git a/core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java index 58f273456..655d43781 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java @@ -19,6 +19,8 @@ package org.apache.struts2.interceptor; import org.apache.struts2.ActionInvocation; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.inject.Inject; import org.apache.struts2.interceptor.AbstractInterceptor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,24 +41,27 @@ import java.util.Set; * of 'false'. * </p> * <!-- END SNIPPET: description --> - * + * <p> * <!-- START SNIPPET: parameters --> * <ul> * <li>setUncheckedValue - The default value of an unchecked box can be overridden by setting the 'uncheckedValue' property.</li> * </ul> * <!-- END SNIPPET: parameters --> - * + * <p> * <!-- START SNIPPET: extending --> - * + * <p> * <!-- END SNIPPET: extending --> */ public class CheckboxInterceptor extends AbstractInterceptor { - /** Auto-generated serialization id */ + /** + * Auto-generated serialization id + */ @Serial private static final long serialVersionUID = -586878104807229585L; private String uncheckedValue = Boolean.FALSE.toString(); + private String hiddenPrefix = "__checkbox_"; private static final Logger LOG = LogManager.getLogger(CheckboxInterceptor.class); @@ -68,8 +73,8 @@ public class CheckboxInterceptor extends AbstractInterceptor { Set<String> checkboxParameters = new HashSet<>(); for (Map.Entry<String, Parameter> parameter : parameters.entrySet()) { String name = parameter.getKey(); - if (name.startsWith("__checkbox_")) { - String checkboxName = name.substring("__checkbox_".length()); + if (name.startsWith(hiddenPrefix)) { + String checkboxName = name.substring(hiddenPrefix.length()); Parameter value = parameter.getValue(); checkboxParameters.add(name); @@ -100,4 +105,16 @@ public class CheckboxInterceptor extends AbstractInterceptor { public void setUncheckedValue(String uncheckedValue) { this.uncheckedValue = uncheckedValue; } + + /** + * Sets the prefix used for hidden checkbox fields. + * Default is "__checkbox_" for backward compatibility. + * + * @param hiddenPrefix The prefix to use for hidden checkbox fields + * @since 7.2.0 + */ + @Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false) + public void setHiddenPrefix(String hiddenPrefix) { + this.hiddenPrefix = hiddenPrefix; + } } diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index a980196a6..6f9f56704 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -319,4 +319,8 @@ struts.url.decoder=strutsUrlDecoder ### Defines source to read nonce value from, possible values are: request, session struts.csp.nonceSource=session +### Checkbox hidden field prefix +### Default prefix for backward compatibility. Change to "struts_checkbox_" for HTML5 validation. +struts.ui.checkbox.hiddenPrefix=__checkbox_ + ### END SNIPPET: complete_file diff --git a/core/src/main/resources/template/html5/checkbox.ftl b/core/src/main/resources/template/html5/checkbox.ftl index b042e457b..8588da618 100644 --- a/core/src/main/resources/template/html5/checkbox.ftl +++ b/core/src/main/resources/template/html5/checkbox.ftl @@ -41,7 +41,7 @@ <#include "/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl" /><#rt/> /><#rt/> <#if attributes.submitUnchecked!false> -<input type="hidden" id="__checkbox_${attributes.id}" name="__checkbox_${attributes.name}" value="${attributes.fieldValue}"<#rt/> +<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/> <#if attributes.disabled!false> disabled="disabled"<#rt/> </#if> diff --git a/core/src/main/resources/template/simple/checkbox.ftl b/core/src/main/resources/template/simple/checkbox.ftl index c1839f3f4..420abcbc1 100644 --- a/core/src/main/resources/template/simple/checkbox.ftl +++ b/core/src/main/resources/template/simple/checkbox.ftl @@ -40,7 +40,7 @@ <#include "/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl" /> /><#rt/> <#if attributes.submitUnchecked!false> -<input type="hidden" id="__checkbox_${attributes.id}" name="__checkbox_${attributes.name}" value="${attributes.fieldValue}"<#rt/> +<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/> <#if attributes.disabled!false> disabled="disabled"<#rt/> </#if> diff --git a/core/src/test/java/org/apache/struts2/interceptor/CheckboxInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/CheckboxInterceptorTest.java index d9f1c0aa4..4fc6005f5 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/CheckboxInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/CheckboxInterceptorTest.java @@ -38,171 +38,227 @@ public class CheckboxInterceptorTest extends StrutsInternalTestCase { private Map<String, Object> param; protected void setUp() throws Exception { - super.setUp(); - param = new HashMap<>(); + super.setUp(); + param = new HashMap<>(); - interceptor = new CheckboxInterceptor(); - ai = new MockActionInvocation(); - ai.setInvocationContext(ActionContext.getContext()); + interceptor = new CheckboxInterceptor(); + ai = new MockActionInvocation(); + ai.setInvocationContext(ActionContext.getContext()); } - private void prepare(ActionInvocation ai) { - ai.getInvocationContext().withParameters(HttpParameters.create(param).build()); - } + private void prepare(ActionInvocation ai) { + ai.getInvocationContext().withParameters(HttpParameters.create(param).build()); + } - public void testNoParam() throws Exception { - prepare(ai); + public void testNoParam() throws Exception { + prepare(ai); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - assertEquals(0, param.size()); - } + assertEquals(0, param.size()); + } - public void testPassthroughOne() throws Exception { - param.put("user", "batman"); + public void testPassthroughOne() throws Exception { + param.put("user", "batman"); - prepare(ai); + prepare(ai); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - assertEquals(1, ai.getInvocationContext().getParameters().keySet().size()); - } + assertEquals(1, ai.getInvocationContext().getParameters().size()); + } - public void testPassthroughTwo() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); + public void testPassthroughTwo() throws Exception { + param.put("user", "batman"); + param.put("email", "[email protected]"); - prepare(ai); + prepare(ai); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - assertEquals(2, ai.getInvocationContext().getParameters().keySet().size()); - } + assertEquals(2, ai.getInvocationContext().getParameters().size()); + } - public void testOneCheckboxTrue() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); - param.put("superpower", "true"); - param.put("__checkbox_superpower", "true"); - assertTrue(param.containsKey("__checkbox_superpower")); + public void testOneCheckboxTrue() throws Exception { + param.put("user", "batman"); + param.put("email", "[email protected]"); + param.put("superpower", "true"); + param.put("__checkbox_superpower", "true"); + assertTrue(param.containsKey("__checkbox_superpower")); - prepare(ai); + prepare(ai); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - HttpParameters parameters = ai.getInvocationContext().getParameters(); - assertFalse(parameters.contains("__checkbox_superpower")); - assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed - assertEquals("true", parameters.get("superpower").getValue()); - } + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("__checkbox_superpower")); + assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed + assertEquals("true", parameters.get("superpower").getValue()); + } - public void testOneCheckboxNoValue() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); - param.put("__checkbox_superpower", "false"); - assertTrue(param.containsKey("__checkbox_superpower")); + public void testOneCheckboxNoValue() throws Exception { + param.put("user", "batman"); + param.put("email", "[email protected]"); + param.put("__checkbox_superpower", "false"); + assertTrue(param.containsKey("__checkbox_superpower")); - prepare(ai); + prepare(ai); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - HttpParameters parameters = ai.getInvocationContext().getParameters(); - assertFalse(parameters.contains("__checkbox_superpower")); - assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed - assertEquals("false", parameters.get("superpower").getValue()); - } + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("__checkbox_superpower")); + assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed + assertEquals("false", parameters.get("superpower").getValue()); + } - public void testOneCheckboxNoValueDifferentDefault() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); - param.put("__checkbox_superpower", "false"); - assertTrue(param.containsKey("__checkbox_superpower")); + public void testOneCheckboxNoValueDifferentDefault() throws Exception { + param.put("user", "batman"); + param.put("email", "[email protected]"); + param.put("__checkbox_superpower", "false"); + assertTrue(param.containsKey("__checkbox_superpower")); - prepare(ai); + prepare(ai); - interceptor.setUncheckedValue("off"); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.setUncheckedValue("off"); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - HttpParameters parameters = ai.getInvocationContext().getParameters(); - assertFalse(parameters.contains("__checkbox_superpower")); - assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed - assertEquals("off", parameters.get("superpower").getValue()); - } + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("__checkbox_superpower")); + assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed + assertEquals("off", parameters.get("superpower").getValue()); + } public void testTwoCheckboxNoValue() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); - param.put("__checkbox_superpower", new String[]{"true", "true"}); + param.put("user", "batman"); + param.put("email", "[email protected]"); + param.put("__checkbox_superpower", new String[]{"true", "true"}); - prepare(ai); + prepare(ai); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); - HttpParameters parameters = ai.getInvocationContext().getParameters(); - assertFalse(parameters.contains("__checkbox_superpower")); - assertEquals(2, parameters.keySet().size()); // should be 2 as __checkbox_ should be removed - assertFalse(parameters.get("superpower").isDefined()); + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("__checkbox_superpower")); + assertEquals(2, parameters.size()); // should be 2 as __checkbox_ should be removed + assertFalse(parameters.get("superpower").isDefined()); } public void testTwoCheckboxMixed() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); - param.put("__checkbox_superpower", "true"); - param.put("superpower", "yes"); - param.put("__checkbox_cool", "no"); - assertTrue(param.containsKey("__checkbox_superpower")); - assertTrue(param.containsKey("__checkbox_cool")); - - prepare(ai); - - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); - - HttpParameters parameters = ai.getInvocationContext().getParameters(); - assertFalse(parameters.contains("__checkbox_superpower")); - assertFalse(parameters.contains("__checkbox_cool")); - assertEquals(4, parameters.keySet().size()); // should be 4 as __checkbox_ should be removed - assertEquals("yes", parameters.get("superpower").getValue()); - assertEquals("false", parameters.get("cool").getValue()); // will use false as default and not 'no' - } - - public void testTwoCheckboxMixedWithDifferentDefault() throws Exception { - param.put("user", "batman"); - param.put("email", "[email protected]"); - param.put("__checkbox_superpower", "true"); - param.put("superpower", "yes"); - param.put("__checkbox_cool", "no"); - assertTrue(param.containsKey("__checkbox_superpower")); - assertTrue(param.containsKey("__checkbox_cool")); - - prepare(ai); - - interceptor.setUncheckedValue("no"); - interceptor.init(); - interceptor.intercept(ai); - interceptor.destroy(); - - HttpParameters parameters = ai.getInvocationContext().getParameters(); - assertFalse(parameters.contains("__checkbox_superpower")); - assertFalse(parameters.contains("__checkbox_cool")); - assertEquals(4, parameters.keySet().size()); // should be 4 as __checkbox_ should be removed - assertEquals("yes", parameters.get("superpower").getValue()); - assertEquals("no", parameters.get("cool").getValue()); - } + param.put("user", "batman"); + param.put("email", "[email protected]"); + param.put("__checkbox_superpower", "true"); + param.put("superpower", "yes"); + param.put("__checkbox_cool", "no"); + assertTrue(param.containsKey("__checkbox_superpower")); + assertTrue(param.containsKey("__checkbox_cool")); + + prepare(ai); + + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); + + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("__checkbox_superpower")); + assertFalse(parameters.contains("__checkbox_cool")); + assertEquals(4, parameters.size()); // should be 4 as __checkbox_ should be removed + assertEquals("yes", parameters.get("superpower").getValue()); + assertEquals("false", parameters.get("cool").getValue()); // will use false as default and not 'no' + } + + public void testTwoCheckboxMixedWithDifferentDefault() throws Exception { + param.put("user", "batman"); + param.put("email", "[email protected]"); + param.put("__checkbox_superpower", "true"); + param.put("superpower", "yes"); + param.put("__checkbox_cool", "no"); + assertTrue(param.containsKey("__checkbox_superpower")); + assertTrue(param.containsKey("__checkbox_cool")); + + prepare(ai); + + interceptor.setUncheckedValue("no"); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); + + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("__checkbox_superpower")); + assertFalse(parameters.contains("__checkbox_cool")); + assertEquals(4, parameters.size()); // should be 4 as __checkbox_ should be removed + assertEquals("yes", parameters.get("superpower").getValue()); + assertEquals("no", parameters.get("cool").getValue()); + } + + public void testCustomHiddenPrefixChecked() throws Exception { + param.put("user", "batman"); + param.put("struts_checkbox_superpower", "true"); + param.put("superpower", "yes"); + assertTrue(param.containsKey("struts_checkbox_superpower")); + + prepare(ai); + + interceptor.setHiddenPrefix("struts_checkbox_"); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); + + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("struts_checkbox_superpower")); + assertEquals(2, parameters.size()); + assertEquals("yes", parameters.get("superpower").getValue()); + } + + public void testCustomHiddenPrefixUnchecked() throws Exception { + param.put("user", "batman"); + param.put("struts_checkbox_superpower", "true"); + assertTrue(param.containsKey("struts_checkbox_superpower")); + + prepare(ai); + + interceptor.setHiddenPrefix("struts_checkbox_"); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); + + HttpParameters parameters = ai.getInvocationContext().getParameters(); + assertFalse(parameters.contains("struts_checkbox_superpower")); + assertEquals(2, parameters.size()); + assertEquals("false", parameters.get("superpower").getValue()); + } + + public void testCustomHiddenPrefixIgnoresDefaultPrefix() throws Exception { + param.put("user", "batman"); + param.put("__checkbox_superpower", "true"); + assertTrue(param.containsKey("__checkbox_superpower")); + + prepare(ai); + + interceptor.setHiddenPrefix("struts_checkbox_"); + interceptor.init(); + interceptor.intercept(ai); + interceptor.destroy(); + + HttpParameters parameters = ai.getInvocationContext().getParameters(); + // With custom prefix, the default __checkbox_ prefix should be ignored + assertTrue(parameters.contains("__checkbox_superpower")); + assertEquals(2, parameters.size()); + assertFalse(parameters.get("superpower").isDefined()); + } } diff --git a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java index 6fe4a8bb7..cf7b51c13 100644 --- a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java +++ b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java @@ -39,16 +39,16 @@ public class CheckboxHandler extends AbstractTagHandler implements TagGenerator boolean submitUnchecked = Boolean.parseBoolean(Objects.toString(params.get("submitUnchecked"), "false")); attrs.add("type", "checkbox") - .add("name", name) - .add("value", fieldValue) - .addIfTrue("checked", params.get("nameValue")) - .addIfTrue("readonly", params.get("readonly")) - .addIfTrue("disabled", disabled) - .addIfExists("tabindex", params.get("tabindex")) - .addIfExists("id", id) - .addIfExists("class", params.get("cssClass")) - .addIfExists("style", params.get("cssStyle")) - .addIfExists("title", params.get("title")); + .add("name", name) + .add("value", fieldValue) + .addIfTrue("checked", params.get("nameValue")) + .addIfTrue("readonly", params.get("readonly")) + .addIfTrue("disabled", disabled) + .addIfExists("tabindex", params.get("tabindex")) + .addIfExists("id", id) + .addIfExists("class", params.get("cssClass")) + .addIfExists("style", params.get("cssStyle")) + .addIfExists("title", params.get("title")); start("input", attrs); end("input"); @@ -56,11 +56,13 @@ public class CheckboxHandler extends AbstractTagHandler implements TagGenerator //hidden input attrs = new Attributes(); + String hiddenPrefix = Objects.toString(params.get("hiddenPrefix"), "__checkbox_"); + attrs.add("type", "hidden") - .add("id", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(id))) - .add("name", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(name))) - .add("value", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(fieldValue))) - .addIfTrue("disabled", disabled); + .add("id", hiddenPrefix + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(id))) + .add("name", hiddenPrefix + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(name))) + .add("value", StringUtils.defaultString(StringEscapeUtils.escapeHtml4(fieldValue))) + .addIfTrue("disabled", disabled); start("input", attrs); end("input"); } diff --git a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java index b73c125d0..acc811704 100644 --- a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java +++ b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java @@ -62,7 +62,9 @@ public class CheckboxTest extends AbstractCommonAttributesTest { map.putAll(tag.getAttributes()); theme.renderTag(getTagName(), context); String output = writer.getBuffer().toString(); - String expected = s("<input type='checkbox' name='name_' value='xyz' disabled='disabled' tabindex='1' id='id_' class='class' style='style' title='title'></input><input type='hidden' id='__checkbox_id_' name='__checkbox_name_' value='__checkbox_xyz' disabled='disabled'></input>"); + // Note: The hidden field's value should be the same as fieldValue, not prefixed with __checkbox_ + // This matches the FreeMarker template behavior in simple/checkbox.ftl + String expected = s("<input type='checkbox' name='name_' value='xyz' disabled='disabled' tabindex='1' id='id_' class='class' style='style' title='title'></input><input type='hidden' id='__checkbox_id_' name='__checkbox_name_' value='xyz' disabled='disabled'></input>"); assertEquals(expected, output); } diff --git a/thoughts/shared/research/2026-02-06-WW-3429-checkbox-prefix-constant.md b/thoughts/shared/research/2026-02-06-WW-3429-checkbox-prefix-constant.md new file mode 100644 index 000000000..bfb9a7770 --- /dev/null +++ b/thoughts/shared/research/2026-02-06-WW-3429-checkbox-prefix-constant.md @@ -0,0 +1,196 @@ +--- +date: 2026-02-06T12:00:00+01:00 +topic: "WW-3429 - Configurable Checkbox Hidden Field Prefix" +tags: [research, codebase, checkbox, interceptor, freemarker, constants, backward-compatibility] +status: complete +jira_ticket: WW-3429 +--- + +# Research: WW-3429 - Configurable Checkbox Hidden Field Prefix + +**Date**: 2026-02-06 + +## Research Question + +How to add a Struts constant to steer backward compatibility with the prefix used in checkbox.ftl template and CheckboxInterceptor.java, addressing the HTML validation issue with `__checkbox_` prefix. + +## Summary + +The JIRA ticket WW-3429 reports that the `__checkbox_` prefix violates HTML standards (double underscores in attribute names). The solution requires: + +1. Adding a new constant `STRUTS_UI_CHECKBOX_HIDDEN_PREFIX` to `StrutsConstants.java` +2. Setting default value in `default.properties` (use `__checkbox_` for backward compatibility) +3. Injecting the constant into `Checkbox.java` component and `CheckboxInterceptor.java` +4. Passing the prefix to FreeMarker templates via component parameters +5. Updating all checkbox templates to use the configurable prefix + +## Detailed Findings + +### Current Hardcoded Prefix Locations + +The prefix `__checkbox_` is hardcoded in multiple files: + +| File | Line | Usage | +|------|------|-------| +| `core/src/main/resources/template/simple/checkbox.ftl` | 43 | Hidden field generation | +| `core/src/main/resources/template/html5/checkbox.ftl` | 44 | Hidden field generation | +| `core/src/main/resources/template/css_xhtml/checkbox.ftl` | - | Hidden field generation | +| `core/src/main/resources/template/xhtml/checkbox.ftl` | - | Hidden field generation | +| `core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java` | 71-72 | Prefix matching | +| `plugins/javatemplates/.../CheckboxHandler.java` | - | Java template rendering | +| `core/src/test/java/.../CheckboxInterceptorTest.java` | - | Test assertions | + +### Pattern for Adding Configuration Constant + +Based on existing patterns (e.g., `STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED`): + +#### Step 1: Add to StrutsConstants.java + +```java +// core/src/main/java/org/apache/struts2/StrutsConstants.java +/** + * The prefix used for hidden checkbox fields to track unchecked values. + * Default is "__checkbox_" for backward compatibility. + * Set to "struts_checkbox_" to avoid HTML validation warnings about double underscores. + * @since 7.2.0 + */ +public static final String STRUTS_UI_CHECKBOX_HIDDEN_PREFIX = "struts.ui.checkbox.hiddenPrefix"; +``` + +#### Step 2: Add default value in default.properties + +```properties +# core/src/main/resources/org/apache/struts2/default.properties +### Checkbox hidden field prefix (WW-3429) +# Default prefix for backward compatibility. Change to "struts_checkbox_" for HTML5 validation. +struts.ui.checkbox.hiddenPrefix = __checkbox_ +``` + +#### Step 3: Inject into Checkbox.java component + +```java +// core/src/main/java/org/apache/struts2/components/Checkbox.java +public static final String ATTR_HIDDEN_PREFIX = "hiddenPrefix"; +private String hiddenPrefixGlobal = "__checkbox_"; + +@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false) +public void setHiddenPrefixGlobal(String hiddenPrefixGlobal) { + this.hiddenPrefixGlobal = hiddenPrefixGlobal; +} + +@Override +protected void evaluateExtraParams() { + super.evaluateExtraParams(); + // ... existing code ... + addParameter(ATTR_HIDDEN_PREFIX, hiddenPrefixGlobal); +} +``` + +#### Step 4: Inject into CheckboxInterceptor.java + +```java +// core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java +private String hiddenPrefix = "__checkbox_"; + +@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false) +public void setHiddenPrefix(String hiddenPrefix) { + this.hiddenPrefix = hiddenPrefix; +} + +@Override +public String intercept(ActionInvocation ai) throws Exception { + // Replace hardcoded "__checkbox_" with this.hiddenPrefix + if (name.startsWith(hiddenPrefix)) { + String checkboxName = name.substring(hiddenPrefix.length()); + // ... + } +} +``` + +#### Step 5: Update checkbox.ftl templates + +```freemarker +<#-- core/src/main/resources/template/simple/checkbox.ftl --> +<#if attributes.submitUnchecked!false> +<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/> +<#if attributes.disabled!false> + disabled="disabled"<#rt/> +</#if> + /><#rt/> +</#if> +``` + +### Existing Example: STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + +This constant shows the pattern already in use: + +**StrutsConstants.java** (line 702): +```java +public static final String STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED = "struts.ui.checkbox.submitUnchecked"; +``` + +**Checkbox.java** - Injection: +```java +@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED, required = false) +public void setSubmitUncheckedGlobal(String submitUncheckedGlobal) { + this.submitUncheckedGlobal = submitUncheckedGlobal; +} +``` + +**Checkbox.java** - Usage in evaluateExtraParams(): +```java +if (submitUnchecked != null) { + Object parsedValue = findValue(submitUnchecked, Boolean.class); + addParameter(ATTR_SUBMIT_UNCHECKED, parsedValue == null ? Boolean.valueOf(submitUnchecked) : parsedValue); +} else if (submitUncheckedGlobal != null) { + addParameter(ATTR_SUBMIT_UNCHECKED, Boolean.parseBoolean(submitUncheckedGlobal)); +} else { + addParameter(ATTR_SUBMIT_UNCHECKED, false); +} +``` + +## Code References + +- `core/src/main/java/org/apache/struts2/StrutsConstants.java` - Constant definitions +- `core/src/main/resources/org/apache/struts2/default.properties` - Default values +- `core/src/main/java/org/apache/struts2/components/Checkbox.java` - UI component +- `core/src/main/java/org/apache/struts2/interceptor/CheckboxInterceptor.java:71-72` - Prefix matching +- `core/src/main/resources/template/simple/checkbox.ftl:43` - Template hidden field + +## Architecture Insights + +1. **Constant injection pattern**: Use `@Inject(value = CONSTANT, required = false)` with setter method +2. **Template access**: Components pass values via `addParameter()` method, templates access via `${attributes.paramName}` +3. **Backward compatibility**: Default value should preserve existing behavior (`__checkbox_`) +4. **Multiple templates**: All 4 theme templates (simple, html5, css_xhtml, xhtml) need updating + +## Files to Modify + +1. **StrutsConstants.java** - Add new constant +2. **default.properties** - Add default value +3. **Checkbox.java** - Inject constant, add parameter +4. **CheckboxInterceptor.java** - Inject constant, use in prefix matching +5. **checkbox.ftl** (simple) - Use `${attributes.hiddenPrefix}` +6. **checkbox.ftl** (html5) - Use `${attributes.hiddenPrefix}` +7. **checkbox.ftl** (css_xhtml) - Use `${attributes.hiddenPrefix}` +8. **checkbox.ftl** (xhtml) - Use `${attributes.hiddenPrefix}` +9. **CheckboxHandler.java** (javatemplates plugin) - Update if applicable +10. **CheckboxInterceptorTest.java** - Add tests for configurable prefix + +## Suggested Configuration Values + +| Value | Description | +|-------|-------------| +| `__checkbox_` | Default, backward compatible (current behavior) | +| `struts_checkbox_` | HTML5 compliant (recommended for new projects) | +| `sc_` | Minimal prefix (short form) | + +## Open Questions + +1. Should there be a per-tag `hiddenPrefix` attribute in addition to the global constant? +2. Should the interceptor support multiple prefixes simultaneously during migration? +3. Need to verify the javatemplates plugin `CheckboxHandler.java` implementation + +## Related JIRA Issues + +- [WW-3429](https://issues.apache.org/jira/browse/WW-3429) - Original issue about HTML validation warnings \ No newline at end of file
