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 57c92f3d20dc211c36a25284568211cb876c04bd
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Aug 24 23:03:14 2026 +0200

    WW-5695 feat(components): add Form.getFieldValidators with per-render 
caching
    
    getValidators re-runs the action-mapping lookup and the validator-manager
    resolution on every call, so a twenty-field form would do twenty of them. 
The
    new method resolves once per form render and filters per field.
    
    Memoised on component fields rather than the attributes map: a Form 
component
    is built per render, and the attributes map is exposed to templates.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../java/org/apache/struts2/components/Form.java   | 45 +++++++++++++
 .../components/FormFieldValidatorsTest.java        | 77 ++++++++++++++++++++++
 2 files changed, 122 insertions(+)

diff --git a/core/src/main/java/org/apache/struts2/components/Form.java 
b/core/src/main/java/org/apache/struts2/components/Form.java
index f970efe56..a570ab3d7 100644
--- a/core/src/main/java/org/apache/struts2/components/Form.java
+++ b/core/src/main/java/org/apache/struts2/components/Form.java
@@ -122,6 +122,10 @@ public class Form extends ClosingUIBean {
     protected UrlRenderer urlRenderer;
     protected ActionValidatorManager actionValidatorManager;
 
+    private List<Validator> cachedActionValidators;
+    private String cachedActionName;
+    private boolean actionValidatorsResolved;
+
     public Form(ValueStack stack, HttpServletRequest request, 
HttpServletResponse response) {
         super(stack, request, response);
     }
@@ -305,6 +309,47 @@ public class Form extends ClosingUIBean {
         return validators;
     }
 
+    /**
+     * Returns the validators declared for a single field, resolving the 
action's validator list at
+     * most once per form render.
+     *
+     * @since 7.4.0
+     */
+    public List<Validator> getFieldValidators(String name) {
+        resolveActionValidators();
+        if (cachedActionValidators.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Class actionClass = (Class) getAttributes().get("actionClass");
+        List<Validator> validators = new ArrayList<>();
+        findFieldValidators(name, actionClass, cachedActionName, 
cachedActionValidators, validators, "");
+        return validators;
+    }
+
+    private void resolveActionValidators() {
+        if (actionValidatorsResolved) {
+            return;
+        }
+        actionValidatorsResolved = true;
+        cachedActionValidators = Collections.emptyList();
+
+        Class actionClass = (Class) getAttributes().get("actionClass");
+        if (actionClass == null) {
+            return;
+        }
+        ActionMapping mapping = 
actionMapper.getMappingFromActionName(findString(action));
+        if (mapping == null) {
+            mapping = actionMapper.getMappingFromActionName((String) 
getAttributes().get("actionName"));
+        }
+        if (mapping == null) {
+            return;
+        }
+        cachedActionName = mapping.getName();
+        String methodName = isValidateAnnotatedMethodOnly(cachedActionName) ? 
mapping.getMethod() : null;
+        cachedActionValidators =
+            actionValidatorManager.getValidators(actionClass, 
cachedActionName, methodName);
+    }
+
     private boolean isValidateAnnotatedMethodOnly(String actionName) {
         RuntimeConfiguration runtimeConfiguration = 
configuration.getRuntimeConfiguration();
         String actionNamespace = getNamespace(stack);
diff --git 
a/core/src/test/java/org/apache/struts2/components/FormFieldValidatorsTest.java 
b/core/src/test/java/org/apache/struts2/components/FormFieldValidatorsTest.java
new file mode 100644
index 000000000..b12df9293
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/components/FormFieldValidatorsTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.TestConfigurationProvider;
+import org.apache.struts2.mock.MockActionProxy;
+import org.apache.struts2.validator.Validator;
+import org.apache.struts2.views.jsp.AbstractUITagTest;
+import org.apache.struts2.views.jsp.ui.FormTag;
+
+import java.util.HashMap;
+import java.util.List;
+
+public class FormFieldValidatorsTest extends AbstractUITagTest {
+
+    public void testFindsTheFieldsValidators() throws Exception {
+        Form form = formForDoubleValidationAction();
+
+        List<Validator> validators = 
form.getFieldValidators("myUpDownSelectTag");
+
+        assertEquals(1, validators.size());
+        assertEquals("double", validators.get(0).getValidatorType());
+    }
+
+    public void testReturnsEmptyForAnUnvalidatedField() throws Exception {
+        Form form = formForDoubleValidationAction();
+
+        assertTrue(form.getFieldValidators("noSuchField").isEmpty());
+    }
+
+    public void testRepeatedCallsAreConsistent() throws Exception {
+        Form form = formForDoubleValidationAction();
+
+        assertEquals(form.getFieldValidators("myUpDownSelectTag").size(),
+            form.getFieldValidators("myUpDownSelectTag").size());
+    }
+
+    private Form formForDoubleValidationAction() throws Exception {
+        FormTag tag = new FormTag();
+        tag.setPageContext(pageContext);
+        tag.setName("myForm");
+        tag.setAction("doubleValidationAction");
+        tag.setNamespace("");
+        tag.doStartTag();
+        return (Form) tag.getComponent();
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        initDispatcher(new HashMap<String, String>() {{
+            put("configProviders", TestConfigurationProvider.class.getName());
+        }});
+        createMocks();
+        // AnnotationActionValidatorManager.buildValidatorKey() dereferences 
the current ActionInvocation's
+        // proxy config; at real runtime the Dispatcher always attaches one, 
but the mock proxy from
+        // createMocks() does not, so it has to be wired up explicitly here.
+        ((MockActionProxy) actionProxy).setConfig(
+            configuration.getRuntimeConfiguration().getActionConfig("", 
"doubleValidationAction"));
+    }
+}

Reply via email to