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>

Reply via email to