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.