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

lukaszlenart pushed a commit to branch WW-3871-typeconversion-key-derivation
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 5c2a964a5c5f7ade95a9609d155d62ccab7747bf
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Jul 25 15:20:12 2026 +0200

    WW-3871 feat(core): derive conversion mapping keys through a single resolver
---
 .../struts2/conversion/impl/XWorkConverter.java    | 58 +++++++++++++++-------
 .../conversion/impl/XWorkConverterTest.java        | 46 +++++++++++++++++
 .../struts2/util/ExplicitKeyConversionAction.java  | 43 ++++++++++++++++
 3 files changed, 128 insertions(+), 19 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/conversion/impl/XWorkConverter.java 
b/core/src/main/java/org/apache/struts2/conversion/impl/XWorkConverter.java
index 8685e9d71..b3aa0e473 100644
--- a/core/src/main/java/org/apache/struts2/conversion/impl/XWorkConverter.java
+++ b/core/src/main/java/org/apache/struts2/conversion/impl/XWorkConverter.java
@@ -27,6 +27,8 @@ import org.apache.struts2.conversion.ConversionFileProcessor;
 import org.apache.struts2.conversion.TypeConverter;
 import org.apache.struts2.conversion.TypeConverterHolder;
 import org.apache.struts2.conversion.annotations.Conversion;
+import org.apache.struts2.conversion.annotations.ConversionRule;
+import org.apache.struts2.conversion.annotations.ConversionType;
 import org.apache.struts2.conversion.annotations.TypeConversion;
 import org.apache.struts2.inject.Inject;
 import org.apache.struts2.util.AnnotationUtils;
@@ -486,6 +488,28 @@ public class XWorkConverter extends DefaultTypeConverter {
         return (lastClass != null && lastProperty != null) ? new Object[] 
{lastClass, lastProperty} : null;
     }
 
+    /**
+     * Resolves the conversion mapping key for an annotation: the given name 
carrying the
+     * {@link ConversionRule}'s prefix. A name that already starts with that 
prefix is returned
+     * unchanged, so annotations that spell the prefix out keep working.
+     *
+     * @param type the annotation's {@link ConversionType}; APPLICATION keys 
are class names and are never prefixed
+     * @param rule the annotation's {@link ConversionRule}
+     * @param name an explicit key or a property name derived from a method or 
field
+     * @return the mapping key, or null when no name is available and the 
entry must be skipped
+     * @since 7.3.0
+     */
+    static String resolveKey(ConversionType type, ConversionRule rule, String 
name) {
+        if (StringUtils.isEmpty(name)) {
+            return null;
+        }
+        if (type == ConversionType.APPLICATION) {
+            return name;
+        }
+        String prefix = rule.prefix();
+        return name.startsWith(prefix) ? name : prefix + name;
+    }
+
     /**
      * Looks for converter mappings for the specified class and adds it to an 
existing map.  Only new converters are
      * added.  If a converter is defined on a key that already exists, the 
converter is ignored.
@@ -532,26 +556,22 @@ public class XWorkConverter extends DefaultTypeConverter {
     private void processMethodAnnotations(Map<String, Object> mapping, Class 
clazz) {
         for (Method method : clazz.getMethods()) {
             for (Annotation annotation : method.getAnnotations()) {
-                if (annotation instanceof TypeConversion tc) {
-                    String key = tc.key();
-                    // Default to the property name with prefix
-                    if (StringUtils.isEmpty(key)) {
-                        key = AnnotationUtils.resolvePropertyName(method);
-                        key = switch (tc.rule()) {
-                            case COLLECTION -> 
DefaultObjectTypeDeterminer.DEPRECATED_ELEMENT_PREFIX + key;
-                            case CREATE_IF_NULL -> 
DefaultObjectTypeDeterminer.CREATE_IF_NULL_PREFIX + key;
-                            case ELEMENT -> 
DefaultObjectTypeDeterminer.ELEMENT_PREFIX + key;
-                            case KEY -> DefaultObjectTypeDeterminer.KEY_PREFIX 
+ key;
-                            case KEY_PROPERTY -> 
DefaultObjectTypeDeterminer.KEY_PROPERTY_PREFIX + key;
-                            default -> key;
-                        };
-                        LOG.debug("Retrieved key [{}] from method name [{}]", 
key, method.getName());
-                    }
-                    if (mapping.containsKey(key)) {
-                        break;
-                    }
-                    annotationProcessor.process(mapping, tc, key);
+                if (!(annotation instanceof TypeConversion tc)) {
+                    continue;
+                }
+                String name = StringUtils.isEmpty(tc.key()) ? 
AnnotationUtils.resolvePropertyName(method) : tc.key();
+                String key = resolveKey(tc.type(), tc.rule(), name);
+                if (key == null) {
+                    LOG.warn("Ignoring @TypeConversion on [{}#{}]: no key was 
given and no property name could be derived from the method",
+                            clazz.getName(), method.getName());
+                    continue;
+                }
+                if (mapping.containsKey(key)) {
+                    continue;
                 }
+                LOG.debug("TypeConversion [{}/{}] on method [{}] resolved to 
key [{}]",
+                        tc.converter(), tc.converterClass(), method.getName(), 
key);
+                annotationProcessor.process(mapping, tc, key);
             }
         }
     }
diff --git 
a/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java 
b/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java
index 9be7c8c48..5b3fea454 100644
--- 
a/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java
+++ 
b/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java
@@ -39,6 +39,9 @@ import 
org.apache.struts2.util.reflection.ReflectionContextState;
 import ognl.OgnlRuntime;
 import org.apache.struts2.conversion.TypeConverter;
 import org.apache.struts2.conversion.StrutsTypeConverterHolder;
+import org.apache.struts2.conversion.annotations.ConversionRule;
+import org.apache.struts2.conversion.annotations.ConversionType;
+import org.apache.struts2.util.ExplicitKeyConversionAction;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -814,6 +817,49 @@ public class XWorkConverterTest extends XWorkTestCase {
         assertEquals(converted, Arrays.asList(1, 2, 3));
     }
 
+    public void testResolveKeyPrependsTheRulePrefix() {
+        assertEquals("KeyProperty_annotatedBeanMap",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.KEY_PROPERTY, "annotatedBeanMap"));
+        assertEquals("Element_annotatedBeanList",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.ELEMENT, "annotatedBeanList"));
+        assertEquals("CreateIfNull_users",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.CREATE_IF_NULL, "users"));
+    }
+
+    public void testResolveKeyLeavesAnAlreadyPrefixedKeyAlone() {
+        assertEquals("KeyProperty_annotatedBeanMap",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.KEY_PROPERTY, "KeyProperty_annotatedBeanMap"));
+        assertEquals("Key_beanMap",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.KEY, "Key_beanMap"));
+    }
+
+    public void testResolveKeyDoesNotPrefixPropertyOrMapRules() {
+        assertEquals("someProperty",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.PROPERTY, "someProperty"));
+        assertEquals("keyValues",
+                XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.MAP, "keyValues"));
+    }
+
+    public void testResolveKeyNeverPrefixesApplicationScopedKeys() {
+        assertEquals("java.util.Date",
+                XWorkConverter.resolveKey(ConversionType.APPLICATION, 
ConversionRule.PROPERTY, "java.util.Date"));
+        assertEquals("java.util.Date",
+                XWorkConverter.resolveKey(ConversionType.APPLICATION, 
ConversionRule.ELEMENT, "java.util.Date"));
+    }
+
+    public void testResolveKeyReturnsNullWhenNoNameIsAvailable() {
+        assertNull(XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.PROPERTY, null));
+        assertNull(XWorkConverter.resolveKey(ConversionType.CLASS, 
ConversionRule.KEY, ""));
+    }
+
+    public void testExplicitMethodKeyGetsTheRulePrefix() {
+        XWorkConverter freshConverter = container.inject(XWorkConverter.class);
+        freshConverter.setTypeConverterHolder(new StrutsTypeConverterHolder());
+
+        assertEquals("true", 
freshConverter.getConverter(ExplicitKeyConversionAction.class, 
"CreateIfNull_bareList"));
+        
assertNull(freshConverter.getConverter(ExplicitKeyConversionAction.class, 
"bareList"));
+    }
+
     public static class CountingXWorkConverter extends XWorkConverter {
         final AtomicInteger builds = new AtomicInteger();
 
diff --git 
a/core/src/test/java/org/apache/struts2/util/ExplicitKeyConversionAction.java 
b/core/src/test/java/org/apache/struts2/util/ExplicitKeyConversionAction.java
new file mode 100644
index 000000000..8997d0696
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/util/ExplicitKeyConversionAction.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import org.apache.struts2.conversion.annotations.ConversionRule;
+import org.apache.struts2.conversion.annotations.TypeConversion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Declares a method level {@link TypeConversion} with an explicit, unprefixed 
key. Before WW-3871
+ * this registered a bare {@code bareList} mapping that nothing ever read.
+ */
+public class ExplicitKeyConversionAction {
+
+    private List bareList = new ArrayList();
+
+    public List getBareList() {
+        return bareList;
+    }
+
+    @TypeConversion(key = "bareList", rule = ConversionRule.CREATE_IF_NULL, 
value = "true")
+    public void setBareList(List bareList) {
+        this.bareList = bareList;
+    }
+}

Reply via email to