This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24976 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7a208fb0910432904f819fca563afae16eb740b0 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 23 21:47:37 2026 +0200 CAMEL-24976: type converter - pick the nearest super type converter deterministically When no type converter exists for the exact type, the registry looked for a converter from a super type by scanning all converters and taking the first match in ConcurrentHashMap iteration order, which varies between JVM runs (CAMEL-21513). The type hierarchy of the value is now walked breadth-first, with interfaces before the super class at each level and Object last, and each candidate is looked up directly, which is also cheaper than scanning all converters. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/impl/converter/TypeResolverHelper.java | 60 +++++++++++++------ .../converter/CoreTypeConverterRegistryTest.java | 68 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/core/camel-base/src/main/java/org/apache/camel/impl/converter/TypeResolverHelper.java b/core/camel-base/src/main/java/org/apache/camel/impl/converter/TypeResolverHelper.java index ae8ada06924e..6a7e176fbb49 100644 --- a/core/camel-base/src/main/java/org/apache/camel/impl/converter/TypeResolverHelper.java +++ b/core/camel-base/src/main/java/org/apache/camel/impl/converter/TypeResolverHelper.java @@ -17,10 +17,16 @@ package org.apache.camel.impl.converter; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.apache.camel.TypeConverter; import org.apache.camel.spi.TypeConvertible; +import org.apache.camel.util.ObjectHelper; /** * Helper methods for resolving the type conversions. This is an internal API and not meant for public usages. @@ -136,7 +142,12 @@ final class TypeResolverHelper { } /** - * Try to resolve the TypeConverter by forcing a costly and slow recursive check. + * Try to resolve the TypeConverter by looking for a converter from a super type (super class or interface) of the + * "from" type. + * <p> + * The type hierarchy is traversed breadth-first, so the nearest super type wins, and at each level the interfaces + * are tried before the super class. {@link Object} is tried last. This makes the resolution deterministic, as it + * does not depend on the iteration order of the converters map. * * @param typeConvertible the type converter pair * @param converters the map of all known converters @@ -144,19 +155,12 @@ final class TypeResolverHelper { */ static TypeConverter tryMatch( TypeConvertible<?, ?> typeConvertible, Map<TypeConvertible<?, ?>, TypeConverter> converters) { - for (var entry : converters.entrySet()) { - if (entry.getValue() != CoreTypeConverterRegistry.MISS_CONVERTER && entry.getKey().matches(typeConvertible)) { - return entry.getValue(); - } - - } - - return null; + return tryHierarchy(typeConvertible.getFrom(), typeConvertible.getTo(), converters); } /** - * Try to resolve the TypeConverter by forcing a costly and slow recursive check that takes into consideration that - * the target type may have a primitive data type + * Try to resolve the TypeConverter by looking for a converter from a super type of the "from" type, taking into + * consideration that the target type may be a primitive type. * * @param typeConvertible the type converter pair * @param converters the map of all known converters @@ -164,15 +168,37 @@ final class TypeResolverHelper { */ static TypeConverter tryPrimitive( TypeConvertible<?, ?> typeConvertible, Map<TypeConvertible<?, ?>, TypeConverter> converters) { - for (var entry : converters.entrySet()) { - if (entry.getValue() != CoreTypeConverterRegistry.MISS_CONVERTER - && entry.getKey().matchesPrimitive(typeConvertible)) { - return entry.getValue(); - } + Class<?> to = ObjectHelper.convertPrimitiveTypeToWrapperType(typeConvertible.getTo()); + return tryHierarchy(typeConvertible.getFrom(), to, converters); + } + private static TypeConverter tryHierarchy( + Class<?> from, Class<?> to, Map<TypeConvertible<?, ?>, TypeConverter> converters) { + Deque<Class<?>> queue = new ArrayDeque<>(); + Set<Class<?>> visited = new HashSet<>(); + queue.add(from); + while (!queue.isEmpty()) { + Class<?> type = queue.poll(); + if (type == Object.class || !visited.add(type)) { + continue; + } + TypeConverter answer = getConverter(type, to, converters); + if (answer != null) { + return answer; + } + Collections.addAll(queue, type.getInterfaces()); + if (type.getSuperclass() != null) { + queue.add(type.getSuperclass()); + } } + // the least specific type is tried last + return from.isInterface() ? null : getConverter(Object.class, to, converters); + } - return null; + private static TypeConverter getConverter( + Class<?> from, Class<?> to, Map<TypeConvertible<?, ?>, TypeConverter> converters) { + TypeConverter answer = converters.get(new TypeConvertible<>(from, to)); + return answer != CoreTypeConverterRegistry.MISS_CONVERTER ? answer : null; } } diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/converter/CoreTypeConverterRegistryTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/converter/CoreTypeConverterRegistryTest.java index 6888c1a44382..35d4144a4b6d 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/converter/CoreTypeConverterRegistryTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/converter/CoreTypeConverterRegistryTest.java @@ -16,17 +16,25 @@ */ package org.apache.camel.impl.converter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import org.apache.camel.ContextTestSupport; import org.apache.camel.Exchange; import org.apache.camel.TypeConverter; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.spi.TypeConverterRegistry; +import org.apache.camel.spi.TypeConvertible; import org.apache.camel.support.TypeConverterSupport; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; public class CoreTypeConverterRegistryTest extends ContextTestSupport { @@ -129,6 +137,38 @@ public class CoreTypeConverterRegistryTest extends ContextTestSupport { assertTrue(registry.lookup(Foo.class).isEmpty()); } + @Test + public void testSuperTypeMatchIsDeterministic() { + TypeConverter fromFirst = new NamedConverter("first"); + TypeConverter fromSecond = new NamedConverter("second"); + TypeConverter fromBase = new NamedConverter("base"); + TypeConverter fromObject = new NamedConverter("object"); + + // regardless of the order the converters are registered, the nearest super type wins + for (boolean reverse : List.of(false, true)) { + Map<TypeConvertible<?, ?>, TypeConverter> converters = new LinkedHashMap<>(); + List<Object[]> entries = new ArrayList<>( + List.of(new Object[] { Object.class, fromObject }, new Object[] { Base.class, fromBase }, + new Object[] { SecondIface.class, fromSecond }, new Object[] { FirstIface.class, fromFirst })); + if (reverse) { + Collections.reverse(entries); + } + for (Object[] e : entries) { + converters.put(new TypeConvertible<>((Class<?>) e[0], Foo.class), (TypeConverter) e[1]); + } + + // interfaces are tried in declared order + assertSame(fromFirst, TypeResolverHelper.tryMatch(new TypeConvertible<>(TwoIfaces.class, Foo.class), converters)); + // the super class is nearer than the object converter + assertSame(fromBase, TypeResolverHelper.tryMatch(new TypeConvertible<>(Sub.class, Foo.class), converters)); + // the interface of the super class is nearer than object + assertSame(fromSecond, + TypeResolverHelper.tryMatch(new TypeConvertible<>(SubOfSecond.class, Foo.class), converters)); + // object is the last resort + assertSame(fromObject, TypeResolverHelper.tryMatch(new TypeConvertible<>(String.class, Foo.class), converters)); + } + } + @Override protected RouteBuilder createRouteBuilder() { return new RouteBuilder() { @@ -176,4 +216,32 @@ public class CoreTypeConverterRegistryTest extends ContextTestSupport { return null; } } + + public interface FirstIface { + } + + public interface SecondIface { + } + + public static class TwoIfaces implements FirstIface, SecondIface { + } + + public static class ImplOfSecond implements SecondIface { + } + + public static class SubOfSecond extends ImplOfSecond { + } + + private static class NamedConverter extends TypeConverterSupport { + private final String name; + + NamedConverter(String name) { + this.name = name; + } + + @Override + public <T> T convertTo(Class<T> type, Exchange exchange, Object value) { + return type.cast(new Foo(name)); + } + } }
