This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch 
WW-5757-bean-validation-message-template-6x
in repository https://gitbox.apache.org/repos/asf/struts.git

commit a6cc073c020b4bfa6f8c8e1b9a908bbd418652d4
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Oct 5 16:13:01 2026 +0200

    WW-5757 fix(bean-validation): resolve the constraint message template as 
the text key
    
    Look up ConstraintViolation#getMessageTemplate() in the Struts bundles and 
use
    the provider's message as-is when no key matches. A template that only
    references a provider message key is not looked up.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../interceptor/BeanValidationInterceptor.java     | 41 +++++++++++----
 .../BeanValidationInterceptorTest.java             | 16 +++++-
 .../beanvalidation/actions/CheckDigitAction.java   | 39 ++++++++++++++
 .../BeanValidationInterceptorMessageTest.java      | 59 ++++++++++++++++++++++
 .../src/test/resources/bean-validation-test.xml    |  4 ++
 .../beanvalidation/actions/FieldAction.properties  | 19 +++++++
 6 files changed, 167 insertions(+), 11 deletions(-)

diff --git 
a/plugins/bean-validation/src/main/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptor.java
 
b/plugins/bean-validation/src/main/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptor.java
index e36e3c63c..90af06511 100644
--- 
a/plugins/bean-validation/src/main/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptor.java
+++ 
b/plugins/bean-validation/src/main/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptor.java
@@ -41,6 +41,7 @@ import javax.validation.groups.Default;
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 /**
  * <p>
@@ -56,6 +57,7 @@ import java.util.Set;
 public class BeanValidationInterceptor extends MethodFilterInterceptor {
 
     private static final Logger LOG = 
LogManager.getLogger(BeanValidationInterceptor.class);
+    private static final Pattern PROVIDER_MESSAGE_KEY = 
Pattern.compile("\\{[^{}]+}");
 
     protected BeanValidationManager beanValidationManager;
     protected TextProviderFactory textProviderFactory;
@@ -135,16 +137,7 @@ public class BeanValidationInterceptor extends 
MethodFilterInterceptor {
         if (constraintViolations != null) {
             ValidatorContext validatorContext = new 
DelegatingValidatorContext(action, textProviderFactory);
             for (ConstraintViolation<Object> constraintViolation : 
constraintViolations) {
-                String key = constraintViolation.getMessage();
-                String message = key;
-                try {
-                    message = validatorContext.getText(key);
-                    if (convertToUtf8 && StringUtils.isNotBlank(message)) {
-                        message = new 
String(message.getBytes(convertFromEncoding), "UTF-8");
-                    }
-                } catch (Exception e) {
-                    LOG.error("Error while trying to fetch message: {}", key, 
e);
-                }
+                String message = resolveMessage(constraintViolation, 
validatorContext);
 
                 if (isActionError(constraintViolation)) {
                     LOG.debug("Adding action error [{}]", message);
@@ -162,6 +155,34 @@ public class BeanValidationInterceptor extends 
MethodFilterInterceptor {
         }
     }
 
+    /**
+     * Resolves the constraint's message template as a Struts text key, or 
falls back to the message
+     * produced by the Bean Validation provider, which is used as-is. A 
template that only references
+     * a provider message key, like {@code 
{javax.validation.constraints.NotNull.message}}, is never
+     * looked up as a Struts text key.
+     *
+     * @param violation the constraint violation
+     * @param validatorContext the context used to look up the text key
+     * @return the error message
+     */
+    protected String resolveMessage(ConstraintViolation<Object> violation, 
ValidatorContext validatorContext) {
+        String key = violation.getMessageTemplate();
+        if (PROVIDER_MESSAGE_KEY.matcher(key).matches() || 
!validatorContext.hasKey(key)) {
+            return violation.getMessage();
+        }
+
+        String message = key;
+        try {
+            message = validatorContext.getText(key);
+            if (convertToUtf8 && StringUtils.isNotBlank(message)) {
+                message = new String(message.getBytes(convertFromEncoding), 
"UTF-8");
+            }
+        } catch (Exception e) {
+            LOG.error("Error while trying to fetch message: {}", key, e);
+        }
+        return message;
+    }
+
     protected ValidationError 
buildBeanValidationError(ConstraintViolation<Object> violation, String message) 
{
 
         if (violation.getPropertyPath().iterator().next().getName() != null) {
diff --git 
a/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationInterceptorTest.java
 
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationInterceptorTest.java
index 88b16d821..597a587b1 100644
--- 
a/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationInterceptorTest.java
+++ 
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationInterceptorTest.java
@@ -22,6 +22,7 @@ import com.opensymphony.xwork2.ActionProxy;
 import com.opensymphony.xwork2.XWorkTestCase;
 import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
 import com.opensymphony.xwork2.interceptor.ValidationAware;
+import org.apache.struts.beanvalidation.actions.CheckDigitAction;
 import org.apache.struts.beanvalidation.actions.FieldAction;
 import org.apache.struts.beanvalidation.actions.FieldMatchAction;
 import org.apache.struts.beanvalidation.actions.ModelDrivenAction;
@@ -128,7 +129,20 @@ public class BeanValidationInterceptorTest extends 
XWorkTestCase {
 
         assertNotNull(fieldErrors);
         assertEquals(1, fieldErrors.size());
-        assertTrue(fieldErrors.get("test").size() > 0);
+        assertEquals("The test field can not be blank", 
fieldErrors.get("test").get(0));
+    }
+
+    public void testProviderMessageIsReportedLiterally() throws Exception {
+        ActionProxy baseActionProxy = 
actionProxyFactory.createActionProxy("bean-validation", "checkDigitAction", 
null, null);
+        CheckDigitAction action = (CheckDigitAction) 
baseActionProxy.getAction();
+        action.setCardNumber("%{1+1}79927398711");
+        baseActionProxy.execute();
+
+        Map<String, List<String>> fieldErrors = ((ValidationAware) 
baseActionProxy.getAction()).getFieldErrors();
+
+        assertEquals(1, fieldErrors.get("cardNumber").size());
+        String message = fieldErrors.get("cardNumber").get(0);
+        assertTrue(message, message.contains("%{1+1}79927398711"));
     }
 
     public void testFieldMatchAction() throws Exception {
diff --git 
a/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/actions/CheckDigitAction.java
 
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/actions/CheckDigitAction.java
new file mode 100644
index 000000000..5a261b135
--- /dev/null
+++ 
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/actions/CheckDigitAction.java
@@ -0,0 +1,39 @@
+/*
+ * 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.struts.beanvalidation.actions;
+
+import com.opensymphony.xwork2.ActionSupport;
+import org.apache.struts2.interceptor.parameter.StrutsParameter;
+import org.hibernate.validator.constraints.LuhnCheck;
+
+public class CheckDigitAction extends ActionSupport {
+
+    @LuhnCheck
+    private String cardNumber;
+
+    public String getCardNumber() {
+        return cardNumber;
+    }
+
+    @StrutsParameter
+    public void setCardNumber(String cardNumber) {
+        this.cardNumber = cardNumber;
+    }
+
+}
diff --git 
a/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptorMessageTest.java
 
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptorMessageTest.java
new file mode 100644
index 000000000..cedfe8911
--- /dev/null
+++ 
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/validation/interceptor/BeanValidationInterceptorMessageTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.struts.beanvalidation.validation.interceptor;
+
+import com.opensymphony.xwork2.TextProviderFactory;
+import com.opensymphony.xwork2.XWorkTestCase;
+import com.opensymphony.xwork2.validator.DelegatingValidatorContext;
+import com.opensymphony.xwork2.validator.ValidatorContext;
+import org.apache.struts.beanvalidation.actions.CheckDigitAction;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BeanValidationInterceptorMessageTest extends XWorkTestCase {
+
+    public void testDefaultProviderMessageIsNotLookedUpAsTextKey() {
+        CheckDigitAction action = new CheckDigitAction();
+        action.setCardNumber("79927398711");
+        ConstraintViolation<Object> violation = 
Validation.buildDefaultValidatorFactory().getValidator()
+                .validate((Object) action).iterator().next();
+        List<String> lookups = new ArrayList<>();
+        ValidatorContext context = new DelegatingValidatorContext(action, 
container.getInstance(TextProviderFactory.class)) {
+            @Override
+            public boolean hasKey(String key) {
+                lookups.add(key);
+                return super.hasKey(key);
+            }
+
+            @Override
+            public String getText(String key) {
+                lookups.add(key);
+                return super.getText(key);
+            }
+        };
+
+        String message = new 
BeanValidationInterceptor().resolveMessage(violation, context);
+
+        assertEquals(violation.getMessage(), message);
+        assertTrue(lookups.toString(), lookups.isEmpty());
+    }
+}
diff --git 
a/plugins/bean-validation/src/test/resources/bean-validation-test.xml 
b/plugins/bean-validation/src/test/resources/bean-validation-test.xml
index 2f1728dd7..9faa2688e 100644
--- a/plugins/bean-validation/src/test/resources/bean-validation-test.xml
+++ b/plugins/bean-validation/src/test/resources/bean-validation-test.xml
@@ -58,6 +58,10 @@
             <interceptor-ref name="beanValidation"/>
             <result type="void"/>
         </action>
+        <action name="checkDigitAction" 
class="org.apache.struts.beanvalidation.actions.CheckDigitAction">
+            <interceptor-ref name="beanValidation"/>
+            <result type="void"/>
+        </action>
         <action name="fieldActionDoExecute" 
class="org.apache.struts.beanvalidation.actions.FieldActionDoExecute">
             <interceptor-ref name="beanValidation"/>
             <result type="void"/>
diff --git 
a/plugins/bean-validation/src/test/resources/org/apache/struts/beanvalidation/actions/FieldAction.properties
 
b/plugins/bean-validation/src/test/resources/org/apache/struts/beanvalidation/actions/FieldAction.properties
new file mode 100644
index 000000000..9f53b21fe
--- /dev/null
+++ 
b/plugins/bean-validation/src/test/resources/org/apache/struts/beanvalidation/actions/FieldAction.properties
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+canNotBeBlank=The test field can not be blank

Reply via email to