You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/23 19:56:14 UTC
(camel) 02/06: CAMEL-24976: type converter - a recorded miss no longer blocks conversions that can succeed
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 <cl...@gmail.com>
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) <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../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;
+ }
+ }
}