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")); + } +}
