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