This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new c90bb70c2 feat(ui): WW-3429 add configurable checkbox hidden field
prefix (#1570)
c90bb70c2 is described below
commit c90bb70c250fac38a93832da1ecf872343e82c87
Author: Lukasz Lenart <[email protected]>
AuthorDate: Tue Feb 17 07:14:03 2026 +0100
feat(ui): WW-3429 add configurable checkbox hidden field prefix (#1570)
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 c7e1dd916..39b811068 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -707,6 +707,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 59165eca3..7f1e9e1a8 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