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

Reply via email to