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


Reply via email to