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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 4faa92ecfac6 CAMEL-21513: camel-core - Type converter: the last-resort 
assignable match no longer depends on map order (#27526)
4faa92ecfac6 is described below

commit 4faa92ecfac6ba5ef56698151982ce1534937ab0
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 8 18:36:13 2026 +0530

    CAMEL-21513: camel-core - Type converter: the last-resort assignable match 
no longer depends on map order (#27526)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/impl/converter/TypeResolverHelper.java   | 68 ++++++++++++++++++++--
 .../converter/CoreTypeConverterRegistryTest.java   | 54 +++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  4 ++
 3 files changed, 121 insertions(+), 5 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 6a7e176fbb49..450be9df838a 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
@@ -18,9 +18,11 @@
 package org.apache.camel.impl.converter;
 
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Deque;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -46,6 +48,9 @@ import org.apache.camel.util.ObjectHelper;
  *
  */
 final class TypeResolverHelper {
+    // larger than any real distance, and small enough that two of them can be 
added
+    private static final int UNREACHED_DISTANCE = Integer.MAX_VALUE / 4;
+
     private TypeResolverHelper() {
 
     }
@@ -123,22 +128,75 @@ final class TypeResolverHelper {
         /*
          Let's try classes derived from this toType: basically it traverses 
the entries looking for assignable types
          matching both the "from type" and the "to type" which are NOT Object 
(we usually try this later).
+
+         More than one entry can match, so the nearest one wins: the one with 
the fewest levels between its types and
+         the requested types in the type hierarchy. Equally near entries are 
ordered by the names of their types. This
+         makes the result independent of the iteration order of the converters 
map, which can change between JVM runs.
          */
+        TypeConvertible<?, ?> bestKey = null;
+        TypeConverter best = null;
+        int bestDistance = 0;
         for (var entry : converters.entrySet()) {
             if (entry.getValue() == CoreTypeConverterRegistry.MISS_CONVERTER) {
                 continue;
             }
             final TypeConvertible<?, ?> key = entry.getKey();
+            final int keyDistance;
             if (key.isAssignableMatch(typeConvertible)) {
-                return entry.getValue();
+                keyDistance = distance(typeConvertible.getFrom(), 
key.getFrom())
+                              + distance(typeConvertible.getTo(), key.getTo());
+            } else if (typeConvertible.isAssignableMatch(key)) {
+                keyDistance = distance(key.getFrom(), 
typeConvertible.getFrom())
+                              + distance(key.getTo(), typeConvertible.getTo());
             } else {
-                if (typeConvertible.isAssignableMatch(key)) {
-                    return entry.getValue();
-                }
+                continue;
+            }
+            if (best == null || keyDistance < bestDistance
+                    || keyDistance == bestDistance && compareByTypeNames(key, 
bestKey) < 0) {
+                bestKey = key;
+                best = entry.getValue();
+                bestDistance = keyDistance;
             }
         }
 
-        return null;
+        return best;
+    }
+
+    private static int compareByTypeNames(TypeConvertible<?, ?> a, 
TypeConvertible<?, ?> b) {
+        int answer = a.getFrom().getName().compareTo(b.getFrom().getName());
+        return answer != 0 ? answer : 
a.getTo().getName().compareTo(b.getTo().getName());
+    }
+
+    /**
+     * The number of levels from a type up to one of its super types, walking 
the interfaces and the super class
+     * breadth-first. A super type that this walk does not reach (such as 
{@link Object} for an interface, or
+     * {@code Object[]} for {@code String[]}) counts as the farthest.
+     */
+    private static int distance(Class<?> type, Class<?> superType) {
+        if (type == superType) {
+            return 0;
+        }
+        List<Class<?>> level = List.of(type);
+        Set<Class<?>> visited = new HashSet<>();
+        for (int levels = 1; !level.isEmpty(); levels++) {
+            List<Class<?>> next = new ArrayList<>();
+            for (Class<?> t : level) {
+                for (Class<?> i : t.getInterfaces()) {
+                    if (visited.add(i)) {
+                        next.add(i);
+                    }
+                }
+                Class<?> superClass = t.getSuperclass();
+                if (superClass != null && visited.add(superClass)) {
+                    next.add(superClass);
+                }
+            }
+            if (next.contains(superType)) {
+                return levels;
+            }
+            level = next;
+        }
+        return UNREACHED_DISTANCE;
     }
 
     /**
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 b8e2906c90c0..8f7a57e19508 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
@@ -170,6 +170,40 @@ public class CoreTypeConverterRegistryTest extends 
ContextTestSupport {
         }
     }
 
+    @Test
+    void testAssignableMatchIsDeterministic() {
+        TypeConverter fromCache = new NamedConverter("cache");
+        TypeConverter fromPayload = new NamedConverter("payload");
+        TypeConverter fromSecond = new NamedConverter("second");
+        TypeConverter fromImplOfSecond = new NamedConverter("implOfSecond");
+
+        // CAMEL-21513 / CAMEL-25231: like CachedCxfPayload (a CxfPayload that 
implements StreamCache) to SAXSource
+        // with converters from StreamCache and from CxfPayload to Source, 
more than one converter matches, and the
+        // one chosen must not depend on the order the converters map is 
iterated
+        for (boolean reverse : List.of(false, true)) {
+            Map<TypeConvertible<?, ?>, TypeConverter> converters = new 
LinkedHashMap<>();
+            List<Object[]> entries = new ArrayList<>(
+                    List.of(new Object[] { Cache.class, fromCache }, new 
Object[] { Payload.class, fromPayload },
+                            new Object[] { SecondIface.class, fromSecond },
+                            new Object[] { ImplOfSecond.class, 
fromImplOfSecond }));
+            if (reverse) {
+                Collections.reverse(entries);
+            }
+            for (Object[] e : entries) {
+                converters.put(new TypeConvertible<>((Class<?>) e[0], 
Bar.class), (TypeConverter) e[1]);
+            }
+
+            // both are one level away, so the class names decide
+            assertSame(fromCache,
+                    TypeResolverHelper.tryAssignableFrom(new 
TypeConvertible<>(CachedPayload.class, SubBar.class),
+                            converters));
+            // the super class is nearer than the interface of the super class
+            assertSame(fromImplOfSecond,
+                    TypeResolverHelper.tryAssignableFrom(new 
TypeConvertible<>(SubOfSecond.class, SubBar.class),
+                            converters));
+        }
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
@@ -233,6 +267,21 @@ public class CoreTypeConverterRegistryTest extends 
ContextTestSupport {
     public static class SubOfSecond extends ImplOfSecond {
     }
 
+    public static class Bar {
+    }
+
+    public static class SubBar extends Bar {
+    }
+
+    public interface Cache {
+    }
+
+    public static class Payload {
+    }
+
+    public static class CachedPayload extends Payload implements Cache {
+    }
+
     private static class NamedConverter extends TypeConverterSupport {
         private final String name;
 
@@ -244,6 +293,11 @@ public class CoreTypeConverterRegistryTest extends 
ContextTestSupport {
         public <T> T convertTo(Class<T> type, Exchange exchange, Object value) 
{
             return type.cast(new Foo(name));
         }
+
+        @Override
+        public String toString() {
+            return name;
+        }
     }
 
     @Test
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index cf9dfdc7c058..034bcf893831 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -1455,6 +1455,10 @@ a type converter or fallback type converter is added.
 for a super type. This used to depend on the iteration order of an internal 
map, which could change
 between JVM restarts. The nearest super type now wins: the type hierarchy is 
walked breadth-first,
 interfaces are tried before the super class at each level, and 
`java.lang.Object` is tried last.
+* As a last resort (after the fallback type converters), the registry uses a 
type converter whose types are
+assignable to or from the requested types. When more than one type converter 
matched, this also depended on
+the iteration order of the internal map. The nearest one now wins (the fewest 
levels between its types and the
+requested types), and the class names of its types decide between equally near 
ones.
 * Converting a `String` to a `Number` now returns an `Integer` for all values 
within the `Integer` range
 (including `Integer.MAX_VALUE`, which returned a `Long` before), and supports 
exponent notation such as `1e5`.
 * Converting a `BigDecimal`, or a `double` larger than a `long`, to 
`BigInteger` no longer loses precision.

Reply via email to