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 <noreply@anthropic.com>
This commit is contained in:
Lukasz Lenart
2026-02-17 07:14:03 +01:00
committed by GitHub
parent a0a213f7c5
commit c90bb70c25
10 changed files with 449 additions and 154 deletions
@@ -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}
*/
@@ -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;
}
@@ -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;
}
}
@@ -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
@@ -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>
@@ -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>
@@ -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", "batman@comic.org");
public void testPassthroughTwo() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
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", "batman@comic.org");
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", "batman@comic.org");
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", "batman@comic.org");
param.put("__checkbox_superpower", "false");
assertTrue(param.containsKey("__checkbox_superpower"));
public void testOneCheckboxNoValue() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
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", "batman@comic.org");
param.put("__checkbox_superpower", "false");
assertTrue(param.containsKey("__checkbox_superpower"));
public void testOneCheckboxNoValueDifferentDefault() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
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", "batman@comic.org");
param.put("__checkbox_superpower", new String[]{"true", "true"});
param.put("user", "batman");
param.put("email", "batman@comic.org");
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", "batman@comic.org");
param.put("__checkbox_superpower", "true");
param.put("superpower", "yes");
param.put("__checkbox_cool", "no");
assertTrue(param.containsKey("__checkbox_superpower"));
assertTrue(param.containsKey("__checkbox_cool"));
param.put("user", "batman");
param.put("email", "batman@comic.org");
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);
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"));
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'
}
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", "batman@comic.org");
param.put("__checkbox_superpower", "true");
param.put("superpower", "yes");
param.put("__checkbox_cool", "no");
assertTrue(param.containsKey("__checkbox_superpower"));
assertTrue(param.containsKey("__checkbox_cool"));
public void testTwoCheckboxMixedWithDifferentDefault() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
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);
prepare(ai);
interceptor.setUncheckedValue("no");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
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());
}
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());
}
}
@@ -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");
}
@@ -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);
}
@@ -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