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