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 1dd6da2f0aa76bdc68bfa2e88646f3cabb7addb1
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 21:46:01 2026 +0200

    CAMEL-24976: type converter - a recorded miss no longer blocks conversions 
that can succeed
    
    The registry records a miss for a pair of types when no converter could
    convert, but fallback converters decide per value, and the miss was also
    found by the super-type scans and returned from lookup(). Now:
    
    - fallback converters are still tried for a pair that previously missed
    - misses are cleared when a type converter or fallback converter is added
    - the super-type scans and lookup() skip misses, so a miss for a super
      class no longer blocks a sub class, and lookup() returns null
    - a miss no longer overwrites a type converter added concurrently
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../impl/converter/CoreTypeConverterRegistry.java  | 28 ++++++-
 .../camel/impl/converter/TypeResolverHelper.java   | 14 +++-
 .../converter/CoreTypeConverterRegistryTest.java   | 92 ++++++++++++++++++++++
 3 files changed, 127 insertions(+), 7 deletions(-)

diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
index d175c828a593..fa1018a9df0e 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
@@ -387,6 +387,13 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
         final Class<?> aClass = type.isPrimitive() ? 
ObjectHelper.convertPrimitiveTypeToWrapperType(type) : type;
         final TypeConvertible<?, ?> typeConvertible = new 
TypeConvertible<>(value.getClass(), aClass);
 
+        if (converters.get(typeConvertible) == MISS_CONVERTER) {
+            // we have previously found no type converter for this pair of 
types, but fallback converters
+            // can convert depending on the given value, so we must still let 
them try
+            final Object fallBackRet = tryFallback(type, exchange, value, 
tryConvert, typeConvertible);
+            return fallBackRet != null ? fallBackRet : 
TypeConverter.MISS_VALUE;
+        }
+
         final Object ret = tryCachedConverters(type, exchange, value, 
typeConvertible);
         if (ret != null) {
             return ret;
@@ -406,13 +413,14 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
 
         // This is the last resort: if nothing else works, try to find 
something that converts from an Object to the target type
         final TypeConverter objConverter = converters.get(new 
TypeConvertible<>(Object.class, aClass));
-        if (objConverter != null) {
+        if (objConverter != null && objConverter != MISS_CONVERTER) {
             converters.put(typeConvertible, objConverter);
             return objConverter.convertTo(type, exchange, value);
         }
 
         if (!tryConvert) {
-            converters.put(typeConvertible, MISS_CONVERTER);
+            // only mark as a miss if no type converter was added in the 
meantime
+            converters.putIfAbsent(typeConvertible, MISS_CONVERTER);
         }
 
         // Could not find suitable conversion, so return Void to indicate not 
found
@@ -482,12 +490,22 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
     }
 
     public TypeConverter getTypeConverter(Class<?> toType, Class<?> fromType) {
-        return converters.get(new TypeConvertible<>(fromType, toType));
+        TypeConverter answer = converters.get(new TypeConvertible<>(fromType, 
toType));
+        return answer != MISS_CONVERTER ? answer : null;
     }
 
     @Override
     public void addConverter(TypeConvertible<?, ?> typeConvertible, 
TypeConverter typeConverter) {
         converters.put(typeConvertible, typeConverter);
+        clearMisses();
+    }
+
+    /**
+     * Clears the previously recorded misses, as a type converter that is 
added may now be able to convert, such as from
+     * a subclass of the type it is added for.
+     */
+    private void clearMisses() {
+        converters.values().removeIf(tc -> tc == MISS_CONVERTER);
     }
 
     @Override
@@ -500,6 +518,7 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
         final TypeConvertible<?, ?> typeConvertible = new 
TypeConvertible<>(fromType, toType);
 
         addOrReplaceTypeConverter(typeConverter, typeConvertible);
+        clearMisses();
     }
 
     private void addOrReplaceTypeConverter(TypeConverter typeConverter, 
TypeConvertible<?, ?> typeConvertible) {
@@ -563,6 +582,7 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
         // add in top of fallback as the toString() fallback will nearly 
always be able to convert
         // the last one which is add to the FallbackTypeConverter will be 
called at the first place
         fallbackConverters.add(0, new FallbackTypeConverter(typeConverter, 
canPromote));
+        clearMisses();
     }
 
     public TypeConverter lookup(Class<?> toType, Class<?> fromType) {
@@ -574,7 +594,7 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
         Map<Class<?>, TypeConverter> answer = new LinkedHashMap<>();
         for (var e : converters.entrySet()) {
             Class<?> target = e.getKey().getTo();
-            if (target == toType) {
+            if (target == toType && e.getValue() != MISS_CONVERTER) {
                 answer.put(e.getKey().getFrom(), e.getValue());
             }
         }
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 fad4dab1926a..ae8ada06924e 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
@@ -63,6 +63,10 @@ final class TypeResolverHelper {
 
         // try with base converters first
         final TypeConverter typeConverter = converters.get(typeConvertible);
+        if (typeConverter == CoreTypeConverterRegistry.MISS_CONVERTER) {
+            // we have previously found no type converter for this pair of 
types
+            return null;
+        }
         if (typeConverter != null) {
             return typeConverter;
         }
@@ -87,7 +91,7 @@ final class TypeResolverHelper {
             }
 
             final TypeConverter objConverter = converters.get(new 
TypeConvertible<>(Object.class, typeConvertible.getTo()));
-            if (objConverter != null) {
+            if (objConverter != null && objConverter != 
CoreTypeConverterRegistry.MISS_CONVERTER) {
                 return objConverter;
             }
         }
@@ -115,6 +119,9 @@ final class TypeResolverHelper {
          matching both the "from type" and the "to type" which are NOT Object 
(we usually try this later).
          */
         for (var entry : converters.entrySet()) {
+            if (entry.getValue() == CoreTypeConverterRegistry.MISS_CONVERTER) {
+                continue;
+            }
             final TypeConvertible<?, ?> key = entry.getKey();
             if (key.isAssignableMatch(typeConvertible)) {
                 return entry.getValue();
@@ -138,7 +145,7 @@ final class TypeResolverHelper {
     static TypeConverter tryMatch(
             TypeConvertible<?, ?> typeConvertible, Map<TypeConvertible<?, ?>, 
TypeConverter> converters) {
         for (var entry : converters.entrySet()) {
-            if (entry.getKey().matches(typeConvertible)) {
+            if (entry.getValue() != CoreTypeConverterRegistry.MISS_CONVERTER 
&& entry.getKey().matches(typeConvertible)) {
                 return entry.getValue();
             }
 
@@ -158,7 +165,8 @@ final class TypeResolverHelper {
     static TypeConverter tryPrimitive(
             TypeConvertible<?, ?> typeConvertible, Map<TypeConvertible<?, ?>, 
TypeConverter> converters) {
         for (var entry : converters.entrySet()) {
-            if (entry.getKey().matchesPrimitive(typeConvertible)) {
+            if (entry.getValue() != CoreTypeConverterRegistry.MISS_CONVERTER
+                    && entry.getKey().matchesPrimitive(typeConvertible)) {
                 return entry.getValue();
             }
 
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 cbcdd7cc8d09..6888c1a44382 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
@@ -17,13 +17,17 @@
 package org.apache.camel.impl.converter;
 
 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.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.assertTrue;
 
 public class CoreTypeConverterRegistryTest extends ContextTestSupport {
 
@@ -64,6 +68,67 @@ public class CoreTypeConverterRegistryTest extends 
ContextTestSupport {
         assertEquals("long:5", template.requestBody("direct:long", 5));
     }
 
+    @Test
+    public void testFallbackStillTriedAfterMiss() {
+        context.getTypeConverterRegistry().addFallbackTypeConverter(new 
FooFallback(), false);
+        TypeConverter tc = context.getTypeConverter();
+
+        // the fallback cannot convert this value, which is recorded as a miss
+        assertNull(tc.convertTo(Foo.class, "bar"));
+        // but it can convert this value of the same type
+        assertEquals("b", tc.convertTo(Foo.class, "foo:b").value);
+        assertNull(tc.convertTo(Foo.class, "baz"));
+        assertEquals("c", tc.tryConvertTo(Foo.class, "foo:c").value);
+    }
+
+    @Test
+    public void testFallbackAddedAfterMiss() {
+        TypeConverter tc = context.getTypeConverter();
+
+        assertNull(tc.convertTo(Foo.class, "foo:x"));
+        context.getTypeConverterRegistry().addFallbackTypeConverter(new 
FooFallback(), false);
+        assertEquals("x", tc.convertTo(Foo.class, "foo:x").value);
+    }
+
+    @Test
+    public void testConverterAddedAfterMiss() {
+        TypeConverter tc = context.getTypeConverter();
+
+        assertNull(tc.convertTo(Foo.class, new Sub()));
+        context.getTypeConverterRegistry().addTypeConverter(Foo.class, 
Base.class, new TypeConverterSupport() {
+            @Override
+            public <T> T convertTo(Class<T> type, Exchange exchange, Object 
value) {
+                return type.cast(new Foo("base"));
+            }
+        });
+        // converter for the super class is used for the sub class that 
previously missed
+        assertEquals("base", tc.convertTo(Foo.class, new Sub()).value);
+    }
+
+    @Test
+    public void testMissOnSuperClassDoesNotAffectSubClass() {
+        context.getTypeConverterRegistry().addFallbackTypeConverter(new 
TypeConverterSupport() {
+            @Override
+            public <T> T convertTo(Class<T> type, Exchange exchange, Object 
value) {
+                return value instanceof Sub && type == Foo.class ? 
type.cast(new Foo("sub")) : null;
+            }
+        }, false);
+        TypeConverter tc = context.getTypeConverter();
+
+        assertNull(tc.convertTo(Foo.class, new Base()));
+        assertEquals("sub", tc.convertTo(Foo.class, new Sub()).value);
+    }
+
+    @Test
+    public void testLookupDoesNotReturnMiss() {
+        TypeConverterRegistry registry = context.getTypeConverterRegistry();
+
+        assertNull(context.getTypeConverter().convertTo(Foo.class, new 
Base()));
+        assertNull(registry.lookup(Foo.class, Base.class));
+        assertNull(registry.lookup(Foo.class, Sub.class));
+        assertTrue(registry.lookup(Foo.class).isEmpty());
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
@@ -84,4 +149,31 @@ public class CoreTypeConverterRegistryTest extends 
ContextTestSupport {
             return "long:" + x;
         }
     }
+
+    public static class Foo {
+        private final String value;
+
+        public Foo(String value) {
+            this.value = value;
+        }
+    }
+
+    public static class Base {
+    }
+
+    public static class Sub extends Base {
+    }
+
+    /**
+     * Fallback that depends on the value: it can only convert strings that 
start with foo:
+     */
+    private static class FooFallback extends TypeConverterSupport {
+        @Override
+        public <T> T convertTo(Class<T> type, Exchange exchange, Object value) 
{
+            if (type == Foo.class && value instanceof String s && 
s.startsWith("foo:")) {
+                return type.cast(new Foo(s.substring(4)));
+            }
+            return null;
+        }
+    }
 }

Reply via email to