This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feature/WW-5695-html5-constraint-validation in repository https://gitbox.apache.org/repos/asf/struts.git
commit 6eec8eb6a5265db55ab1617b765378faa1719868 Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Aug 24 23:21:05 2026 +0200 WW-5695 feat(components): derive constraint attributes during tag evaluation Adds struts.ui.html5.constraints, default false, and wires UIBean to the constraint provider behind it. The hook sits at the end of evaluateParams rather than beside the tagNames block, because evaluateExtraParams is where TextField resolves attributes.type and it runs last; hooking earlier would make every text field look like OTHER. Co-Authored-By: Claude Opus 5 <[email protected]> --- .../java/org/apache/struts2/StrutsConstants.java | 8 +++ .../java/org/apache/struts2/components/UIBean.java | 38 ++++++++++ .../org/apache/struts2/default.properties | 5 ++ .../apache/struts2/TestConfigurationProvider.java | 9 +++ .../struts2/components/ConstraintAction.java | 36 ++++++++++ .../components/ConstraintAttributesTest.java | 80 ++++++++++++++++++++++ .../components/ConstraintAction-validation.xml | 31 +++++++++ 7 files changed, 207 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index 3276ca7c2..105c02a62 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -203,6 +203,14 @@ public final class StrutsConstants { */ public static final String STRUTS_UI_STATIC_CONTENT_PATH = "struts.ui.staticContentPath"; + /** + * Whether the html5 theme emits HTML5 constraint attributes derived from the action's validators. + * Defaults to false in 7.4.0; the default becomes true in 8.0.0. + * + * @since 7.4.0 + */ + public static final String STRUTS_UI_HTML5_CONSTRAINTS = "struts.ui.html5.constraints"; + /** * Whether WebJars support is enabled (serving and URL building) */ diff --git a/core/src/main/java/org/apache/struts2/components/UIBean.java b/core/src/main/java/org/apache/struts2/components/UIBean.java index 9d095095f..b5c2d6e2a 100644 --- a/core/src/main/java/org/apache/struts2/components/UIBean.java +++ b/core/src/main/java/org/apache/struts2/components/UIBean.java @@ -25,6 +25,7 @@ import org.apache.struts2.util.ValueStack; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -531,6 +532,9 @@ public abstract class UIBean extends Component { protected CspNonceReader cspNonceReader; + protected HtmlConstraintProvider htmlConstraintProvider; + protected boolean html5ConstraintsEnabled; + @Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR) public void setDefaultTemplateDir(String dir) { this.defaultTemplateDir = dir; @@ -561,6 +565,16 @@ public abstract class UIBean extends Component { this.cspNonceReader = cspNonceReader; } + @Inject + public void setHtmlConstraintProvider(HtmlConstraintProvider htmlConstraintProvider) { + this.htmlConstraintProvider = htmlConstraintProvider; + } + + @Inject(value = StrutsConstants.STRUTS_UI_HTML5_CONSTRAINTS, required = false) + public void setHtml5ConstraintsEnabled(String html5ConstraintsEnabled) { + this.html5ConstraintsEnabled = BooleanUtils.toBoolean(html5ConstraintsEnabled); + } + @Override public boolean end(Writer writer, String body) { evaluateParams(); @@ -903,6 +917,30 @@ public abstract class UIBean extends Component { } evaluateExtraParams(); + + // must run after evaluateExtraParams(): that is where TextField resolves attributes.type, + // and the control type decides which constraints are legal + addConstraintAttributes(form); + } + + /** + * Derives HTML5 constraint attributes for this field from the action's validators. + * + * @since 7.4.0 + */ + protected void addConstraintAttributes(Form form) { + if (!html5ConstraintsEnabled || form == null || htmlConstraintProvider == null) { + return; + } + String fieldName = (String) getAttributes().get("name"); + if (fieldName == null) { + return; + } + Map<String, String> constraints = htmlConstraintProvider.constraintsFor( + form.getFieldValidators(fieldName), getControlType(), stack.peek()); + if (!constraints.isEmpty()) { + addParameter("constraints", constraints); + } } /** diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index d4f5fccf8..956862835 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -171,6 +171,11 @@ struts.ui.theme.expansion.token=~~~ ### Sets the default template type. Either ftl, vm, or jsp struts.ui.templateSuffix=ftl +### Whether the html5 theme emits HTML5 constraint attributes (required, minlength, +### maxlength, pattern, min, max) derived from the action's validators. +### Defaults to false so existing html5-theme forms render unchanged; becomes true in 8.0.0. +struts.ui.html5.constraints=false + ### Sets a global flag which will escape html body of Anchor, Submit and Component tag ### You can control this flag per tag, e.g.: <s:a ... escapeHtmlTag="true">...</s:a> ### and this take precedence over the global flag diff --git a/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java b/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java index 34d1ecd5b..c6fabf742 100644 --- a/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java +++ b/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java @@ -38,6 +38,7 @@ import org.apache.struts2.interceptor.TokenInterceptor; import org.apache.struts2.interceptor.TokenSessionStoreInterceptor; import org.apache.struts2.interceptor.parameter.ParametersInterceptor; import org.apache.struts2.result.ServletDispatcherResult; +import org.apache.struts2.components.ConstraintAction; import org.apache.struts2.views.jsp.ui.DoubleValidationAction; import java.util.HashMap; @@ -94,6 +95,13 @@ public class TestConfigurationProvider implements ConfigurationProvider { .addInterceptor(new InterceptorMapping("validation", validationInterceptor)) .build(); + ActionConfig constraintActionConfig = new ActionConfig.Builder("", "constraintAction", ConstraintAction.class.getName()) + .addResultConfig(new ResultConfig.Builder(Action.SUCCESS, ServletDispatcherResult.class.getName()) + .addParam("location", "success.jsp") + .build()) + .addInterceptor(new InterceptorMapping("validation", validationInterceptor)) + .build(); + ActionConfig testActionConfig = new ActionConfig.Builder("", "", TestAction.class.getName()) .addResultConfig(new ResultConfig.Builder(Action.SUCCESS, ServletDispatcherResult.class.getName()) .addParam("location", "success.jsp") @@ -119,6 +127,7 @@ public class TestConfigurationProvider implements ConfigurationProvider { .addActionConfig(EXECUTION_COUNT_ACTION_NAME, executionCountActionConfig) .addActionConfig(TEST_ACTION_NAME, testActionConfig) .addActionConfig("doubleValidationAction", doubleValidationActionConfig) + .addActionConfig("constraintAction", constraintActionConfig) .addActionConfig(TOKEN_ACTION_NAME, tokenActionConfig) .addActionConfig(TOKEN_SESSION_ACTION_NAME, tokenSessionActionConfig) .addActionConfig("testActionTagAction", new ActionConfig.Builder("", "", TestAction.class.getName()) diff --git a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java new file mode 100644 index 000000000..ce02d21f8 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.components; + +import org.apache.struts2.ActionSupport; +import org.apache.struts2.interceptor.parameter.StrutsParameter; + +public class ConstraintAction extends ActionSupport { + + private String username; + + public String getUsername() { + return username; + } + + @StrutsParameter + public void setUsername(String username) { + this.username = username; + } +} diff --git a/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java b/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java new file mode 100644 index 000000000..d596d34e5 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.components; + +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.TestConfigurationProvider; +import org.apache.struts2.mock.MockActionProxy; +import org.apache.struts2.views.jsp.AbstractUITagTest; +import org.apache.struts2.views.jsp.ui.FormTag; +import org.apache.struts2.views.jsp.ui.TextFieldTag; + +import java.util.HashMap; +import java.util.Map; + +public class ConstraintAttributesTest extends AbstractUITagTest { + + public void testNoConstraintsWhenTheConstantIsOff() throws Exception { + initDispatcherWith("false"); + + assertNull(renderFieldAndReturnConstraints()); + } + + public void testConstraintsWhenTheConstantIsOn() throws Exception { + initDispatcherWith("true"); + + Map<String, String> constraints = renderFieldAndReturnConstraints(); + assertNotNull("expected constraints to be populated", constraints); + assertEquals("3", constraints.get("minlength")); + } + + @SuppressWarnings("unchecked") + private Map<String, String> renderFieldAndReturnConstraints() throws Exception { + FormTag form = new FormTag(); + form.setPageContext(pageContext); + form.setAction("constraintAction"); + form.setNamespace(""); + form.doStartTag(); + + TextFieldTag field = new TextFieldTag(); + field.setPageContext(pageContext); + field.setName("username"); + field.doStartTag(); + + Map<String, Object> attributes = + ((UIBean) field.getComponent()).getAttributes(); + + field.doEndTag(); + form.doEndTag(); + + return (Map<String, String>) attributes.get("constraints"); + } + + private void initDispatcherWith(String constraintsEnabled) throws Exception { + initDispatcher(new HashMap<String, String>() {{ + put("configProviders", TestConfigurationProvider.class.getName()); + put(StrutsConstants.STRUTS_UI_HTML5_CONSTRAINTS, constraintsEnabled); + }}); + createMocks(); + // createMocks() never sets a config on the MockActionProxy it builds; without one, + // AnnotationActionValidatorManager.buildValidatorKey NPEs dereferencing proxy.getConfig(). + ((MockActionProxy) actionProxy).setConfig( + configuration.getRuntimeConfiguration().getActionConfig("", "constraintAction")); + } +} diff --git a/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml b/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml new file mode 100644 index 000000000..4301d01bc --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +--> +<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0//EN" "https://struts.apache.org/dtds/xwork-validator-1.0.dtd"> +<validators> + <field name="username"> + <field-validator type="stringlength"> + <param name="trim">false</param> + <param name="minLength">3</param> + <message>username must be at least ${minLength} characters</message> + </field-validator> + </field> +</validators>
