mirror of
https://github.com/apache/struts.git
synced 2026-08-05 06:36:58 +00:00
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:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-14
@@ -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");
|
||||
}
|
||||
|
||||
+3
-1
@@ -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
|
||||
Reference in New Issue
Block a user