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 073c6cf7ec20 CAMEL-24976: camel-core - Type converter: fix bugs found 
in a deep review (#26810)
073c6cf7ec20 is described below

commit 073c6cf7ec202ce5c9f619dab41d17df776dfd23
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 24 11:40:13 2026 +0200

    CAMEL-24976: camel-core - Type converter: fix bugs found in a deep review 
(#26810)
    
    A deep review of the core type converter found several bugs, each 
reproduced against 4.23.0-SNAPSHOT before it was fixed.
    
    Converting to a primitive returned the wrong wrapper type: the fast path 
returned an Integer or Long as-is for any primitive target, so 
convertTo(int.class, 5L) returned a Long and callers got a ClassCastException, 
such as a bean method with an int parameter failing with "argument type 
mismatch" for a Long body. tryConvertTo(boolean.class, ...) no longer throws 
when the value cannot be converted, and the last-resort Object converter lookup 
uses the wrapper type for a primitive target.
    
    A recorded miss blocked conversions that could have succeeded: misses are 
cached per pair of classes while fallback converters decide per value, so after 
one value failed every later value of the same type returned null. A converter 
or fallback added after a miss was never used, a miss on a super class blocked 
its sub classes, and lookup() returned the internal miss marker so callers 
checking for null believed a converter existed.
    
    Also fixes the root cause of CAMEL-21513.
    
    Closes #26810
---
 .../apache/camel/converter/DateTimeConverter.java  |   1 +
 .../org/apache/camel/converter/NIOConverter.java   |  23 +-
 .../apache/camel/converter/ObjectConverter.java    |  28 ++-
 .../camel/impl/converter/ArrayTypeConverter.java   |   8 +-
 .../impl/converter/CoreTypeConverterRegistry.java  |  53 +++--
 .../camel/impl/converter/EnumTypeConverter.java    |   7 +
 .../camel/impl/converter/FutureTypeConverter.java  |   3 +
 .../camel/impl/converter/TypeResolverHelper.java   |  68 ++++--
 .../camel/converter/DateTimeConverterTest.java     |   1 +
 .../apache/camel/converter/EnumConverterTest.java  |  10 +
 .../camel/converter/FutureConverterTest.java       |  19 ++
 .../apache/camel/converter/NIOConverterTest.java   |  10 +
 .../camel/converter/ObjectConverterTest.java       |  17 ++
 .../converter/CoreTypeConverterRegistryTest.java   | 260 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  27 +++
 15 files changed, 466 insertions(+), 69 deletions(-)

diff --git 
a/core/camel-base/src/main/java/org/apache/camel/converter/DateTimeConverter.java
 
b/core/camel-base/src/main/java/org/apache/camel/converter/DateTimeConverter.java
index b1714a9d17a5..e81c133000c4 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/converter/DateTimeConverter.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/converter/DateTimeConverter.java
@@ -59,6 +59,7 @@ public final class DateTimeConverter {
             case "MINUTES" -> TimeUnit.MINUTES;
             case "SECONDS" -> TimeUnit.SECONDS;
             case "MILLISECONDS" -> TimeUnit.MILLISECONDS;
+            case "MICROSECONDS" -> TimeUnit.MICROSECONDS;
             case "NANOSECONDS" -> TimeUnit.NANOSECONDS;
             default -> throw new IllegalStateException("Unexpected value: " + 
unit);
         };
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/converter/NIOConverter.java 
b/core/camel-base/src/main/java/org/apache/camel/converter/NIOConverter.java
index 55eb58eef253..5dd054764ecc 100644
--- a/core/camel-base/src/main/java/org/apache/camel/converter/NIOConverter.java
+++ b/core/camel-base/src/main/java/org/apache/camel/converter/NIOConverter.java
@@ -20,16 +20,13 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
 import java.nio.ByteBuffer;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
 import org.apache.camel.Converter;
 import org.apache.camel.Exchange;
-import org.apache.camel.ExchangePropertyKey;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.camel.support.ExchangeHelper;
 
 import static org.apache.camel.util.BufferCaster.cast;
 
@@ -39,8 +36,6 @@ import static org.apache.camel.util.BufferCaster.cast;
 @Converter(generateBulkLoader = true)
 public final class NIOConverter {
 
-    private static final Logger LOG = 
LoggerFactory.getLogger(NIOConverter.class);
-
     /**
      * Utility classes should not have a public constructor.
      */
@@ -88,21 +83,7 @@ public final class NIOConverter {
 
     @Converter(order = 7)
     public static ByteBuffer toByteBuffer(String value, Exchange exchange) {
-        byte[] bytes = null;
-        if (exchange != null) {
-            String charsetName = 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
-            if (charsetName != null) {
-                try {
-                    bytes = value.getBytes(charsetName);
-                } catch (UnsupportedEncodingException e) {
-                    LOG.warn("Cannot convert the byte to String with the 
charset {}", charsetName, e);
-                }
-            }
-        }
-        if (bytes == null) {
-            bytes = value.getBytes();
-        }
-        return ByteBuffer.wrap(bytes);
+        return 
ByteBuffer.wrap(value.getBytes(ExchangeHelper.getCharset(exchange)));
     }
 
     @Converter(order = 8)
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/converter/ObjectConverter.java 
b/core/camel-base/src/main/java/org/apache/camel/converter/ObjectConverter.java
index fc365e117c11..9c3bc24cfde7 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/converter/ObjectConverter.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/converter/ObjectConverter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.converter;
 
+import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Iterator;
 
@@ -133,8 +134,8 @@ public final class ObjectConverter {
         if (value.length != 1) {
             throw new IllegalArgumentException("byte[] must have exactly a 
length of 1: " + value.length);
         }
-        byte b = value[0];
-        return (char) b;
+        // a byte is signed, so mask to avoid sign extension of values above 
127
+        return (char) (value[0] & 0xFF);
     }
 
     @Converter(order = 14)
@@ -233,12 +234,15 @@ public final class ObjectConverter {
             return new BigInteger(str);
         }
 
-        Long num = null;
-        if (value instanceof Number number) {
-            num = number.longValue();
-        }
-        if (num != null) {
-            return BigInteger.valueOf(num);
+        if (value instanceof BigInteger bi) {
+            return bi;
+        } else if (value instanceof BigDecimal bd) {
+            return bd.toBigInteger();
+        } else if (value instanceof Double || value instanceof Float) {
+            // use big decimal to not lose precision for values larger than a 
long
+            return new BigDecimal(((Number) 
value).doubleValue()).toBigInteger();
+        } else if (value instanceof Number number) {
+            return BigInteger.valueOf(number.longValue());
         } else {
             return null;
         }
@@ -329,14 +333,14 @@ public final class ObjectConverter {
     @Converter(order = 39)
     public static Number toNumber(String text) {
         // what kind of numeric is it
-        boolean dot = text.indexOf('.') != -1;
-        if (dot) {
+        boolean decimal = text.indexOf('.') != -1 || text.indexOf('e') != -1 
|| text.indexOf('E') != -1;
+        if (decimal) {
             return Double.parseDouble(text);
         } else {
             // its either a long or integer value (lets just avoid bytes)
             long lon = Long.parseLong(text);
-            if (lon < Integer.MAX_VALUE) {
-                return Integer.valueOf(text);
+            if (lon >= Integer.MIN_VALUE && lon <= Integer.MAX_VALUE) {
+                return (int) lon;
             } else {
                 return lon;
             }
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/ArrayTypeConverter.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/ArrayTypeConverter.java
index 4716d0c62e3d..b053bed4ff52 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/ArrayTypeConverter.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/ArrayTypeConverter.java
@@ -54,10 +54,12 @@ public class ArrayTypeConverter extends 
TypeConverterSupport {
                 }
                 return (T) answer;
             }
-        } else if (Collection.class.isAssignableFrom(type)) {
+        } else if (Collection.class.isAssignableFrom(type) && 
type.isAssignableFrom(ArrayList.class)) {
+            // only for collection types a list can be assigned to (such as 
not a Set)
             if (value != null) {
-                if (value instanceof Object[]) {
-                    return (T) Arrays.asList((Object[]) value);
+                if (value instanceof Object[] arr) {
+                    List<Object> list = Arrays.asList(arr);
+                    return (T) (type.isInstance(list) ? list : new 
ArrayList<>(list));
                 } else if (value.getClass().isArray()) {
                     int size = Array.getLength(value);
                     List<Object> answer = new ArrayList<>(size);
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 736bca82c758..f6f29fa0f311 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
@@ -123,9 +123,8 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
                 return (T) parsedBoolean;
             }
         } else if (type.isPrimitive()) {
-            // okay its a wrapper -> primitive then return as-is for some 
common types
-            Class<?> cls = value.getClass();
-            if (cls == Integer.class || cls == Long.class) {
+            // okay its a wrapper -> primitive then return as-is when the 
wrapper matches the primitive type
+            if (isWrapperOfPrimitive(type, value)) {
                 return (T) value;
             }
         } else if (type == String.class) {
@@ -223,10 +222,8 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
                 return (T) value;
             }
             if (type == boolean.class) {
-                // primitive boolean which must return a value so throw 
exception if not possible
-                Object answer = ObjectConverter.toBoolean(value);
-                requireNonNullBoolean(type, value, answer);
-                return (T) answer;
+                // primitive boolean, but as we are only trying then return 
null if not possible
+                return (T) ObjectConverter.toBoolean(value);
             } else if (type == Boolean.class && value instanceof String str) {
                 // String -> Boolean
                 Boolean parsedBoolean = customParseBoolean(str);
@@ -234,9 +231,8 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
                     return (T) parsedBoolean;
                 }
             } else if (type.isPrimitive()) {
-                // okay its a wrapper -> primitive then return as-is for some 
common types
-                Class<?> cls = value.getClass();
-                if (cls == Integer.class || cls == Long.class) {
+                // okay its a wrapper -> primitive then return as-is when the 
wrapper matches the primitive type
+                if (isWrapperOfPrimitive(type, value)) {
                     return (T) value;
                 }
             } else if (type == String.class) {
@@ -268,6 +264,10 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
         return (T) answer;
     }
 
+    private static boolean isWrapperOfPrimitive(Class<?> type, Object value) {
+        return value.getClass() == 
ObjectHelper.convertPrimitiveTypeToWrapperType(type);
+    }
+
     private static <T> void requireNonNullBoolean(Class<T> type, Object value, 
Object answer) {
         if (answer == null) {
             throw new TypeConversionException(
@@ -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;
@@ -405,14 +412,15 @@ 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, type));
-        if (objConverter != null) {
+        final TypeConverter objConverter = converters.get(new 
TypeConvertible<>(Object.class, aClass));
+        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
@@ -421,7 +429,8 @@ public abstract class CoreTypeConverterRegistry extends 
ServiceSupport implement
 
     private Object tryCachedConverters(Class<?> type, Exchange exchange, 
Object value, TypeConvertible<?, ?> typeConvertible) {
         final TypeConverter typeConverter = converters.get(typeConvertible);
-        if (typeConverter != null) {
+        // a miss may have been recorded concurrently, which must not prevent 
trying the fallback converters
+        if (typeConverter != null && typeConverter != MISS_CONVERTER) {
             final Object ret = typeConverter.convertTo(type, exchange, value);
             if (ret != null) {
                 return ret;
@@ -482,12 +491,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 +519,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 +583,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 +595,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/EnumTypeConverter.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/EnumTypeConverter.java
index 0be4f5ef0727..432f7f920fb0 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/EnumTypeConverter.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/EnumTypeConverter.java
@@ -72,6 +72,13 @@ public class EnumTypeConverter extends TypeConverterSupport 
implements CamelCont
             String text = value.toString().trim();
             Class<Enum<?>> enumClass = (Class<Enum<?>>) type;
 
+            // prefer an exact match
+            for (Enum<?> enumValue : enumClass.getEnumConstants()) {
+                if (enumValue.name().equals(text)) {
+                    return type.cast(enumValue);
+                }
+            }
+
             // we want to match case insensitive for enums
             for (Enum<?> enumValue : enumClass.getEnumConstants()) {
                 if (enumValue.name().equalsIgnoreCase(text)) {
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/FutureTypeConverter.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/FutureTypeConverter.java
index 9da2fc178c4b..26ca7c8ee0be 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/FutureTypeConverter.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/FutureTypeConverter.java
@@ -44,6 +44,9 @@ public final class FutureTypeConverter extends 
TypeConverterSupport {
     public <T> T convertTo(Class<T> type, Exchange exchange, Object value) {
         try {
             return doConvertTo(type, exchange, value);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new TypeConversionException(value, type, e);
         } catch (Exception e) {
             throw new TypeConversionException(value, type, e);
         }
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..6a7e176fbb49 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
@@ -17,10 +17,16 @@
 
 package org.apache.camel.impl.converter;
 
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.camel.TypeConverter;
 import org.apache.camel.spi.TypeConvertible;
+import org.apache.camel.util.ObjectHelper;
 
 /**
  * Helper methods for resolving the type conversions. This is an internal API 
and not meant for public usages.
@@ -63,6 +69,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 +97,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 +125,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();
@@ -129,7 +142,12 @@ final class TypeResolverHelper {
     }
 
     /**
-     * Try to resolve the TypeConverter by forcing a costly and slow recursive 
check.
+     * Try to resolve the TypeConverter by looking for a converter from a 
super type (super class or interface) of the
+     * "from" type.
+     * <p>
+     * The type hierarchy is traversed breadth-first, so the nearest super 
type wins, and at each level the interfaces
+     * are tried before the super class. {@link Object} is tried last. This 
makes the resolution deterministic, as it
+     * does not depend on the iteration order of the converters map.
      *
      * @param  typeConvertible the type converter pair
      * @param  converters      the map of all known converters
@@ -137,19 +155,12 @@ final class TypeResolverHelper {
      */
     static TypeConverter tryMatch(
             TypeConvertible<?, ?> typeConvertible, Map<TypeConvertible<?, ?>, 
TypeConverter> converters) {
-        for (var entry : converters.entrySet()) {
-            if (entry.getKey().matches(typeConvertible)) {
-                return entry.getValue();
-            }
-
-        }
-
-        return null;
+        return tryHierarchy(typeConvertible.getFrom(), 
typeConvertible.getTo(), converters);
     }
 
     /**
-     * Try to resolve the TypeConverter by forcing a costly and slow recursive 
check that takes into consideration that
-     * the target type may have a primitive data type
+     * Try to resolve the TypeConverter by looking for a converter from a 
super type of the "from" type, taking into
+     * consideration that the target type may be a primitive type.
      *
      * @param  typeConvertible the type converter pair
      * @param  converters      the map of all known converters
@@ -157,14 +168,37 @@ final class TypeResolverHelper {
      */
     static TypeConverter tryPrimitive(
             TypeConvertible<?, ?> typeConvertible, Map<TypeConvertible<?, ?>, 
TypeConverter> converters) {
-        for (var entry : converters.entrySet()) {
-            if (entry.getKey().matchesPrimitive(typeConvertible)) {
-                return entry.getValue();
-            }
+        Class<?> to = 
ObjectHelper.convertPrimitiveTypeToWrapperType(typeConvertible.getTo());
+        return tryHierarchy(typeConvertible.getFrom(), to, converters);
+    }
 
+    private static TypeConverter tryHierarchy(
+            Class<?> from, Class<?> to, Map<TypeConvertible<?, ?>, 
TypeConverter> converters) {
+        Deque<Class<?>> queue = new ArrayDeque<>();
+        Set<Class<?>> visited = new HashSet<>();
+        queue.add(from);
+        while (!queue.isEmpty()) {
+            Class<?> type = queue.poll();
+            if (type == Object.class || !visited.add(type)) {
+                continue;
+            }
+            TypeConverter answer = getConverter(type, to, converters);
+            if (answer != null) {
+                return answer;
+            }
+            Collections.addAll(queue, type.getInterfaces());
+            if (type.getSuperclass() != null) {
+                queue.add(type.getSuperclass());
+            }
         }
+        // the least specific type is tried last
+        return from.isInterface() ? null : getConverter(Object.class, to, 
converters);
+    }
 
-        return null;
+    private static TypeConverter getConverter(
+            Class<?> from, Class<?> to, Map<TypeConvertible<?, ?>, 
TypeConverter> converters) {
+        TypeConverter answer = converters.get(new TypeConvertible<>(from, to));
+        return answer != CoreTypeConverterRegistry.MISS_CONVERTER ? answer : 
null;
     }
 
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/DateTimeConverterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/DateTimeConverterTest.java
index acb2d1e784d2..8c6204d3836b 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/DateTimeConverterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/DateTimeConverterTest.java
@@ -59,5 +59,6 @@ public class DateTimeConverterTest extends ContextTestSupport 
{
     public void testToTimeUnit() {
         assertEquals(TimeUnit.DAYS, 
context.getTypeConverter().convertTo(TimeUnit.class, "DAYS"));
         assertEquals(TimeUnit.MILLISECONDS, 
context.getTypeConverter().convertTo(TimeUnit.class, "MILLISECONDS"));
+        assertEquals(TimeUnit.MICROSECONDS, 
context.getTypeConverter().convertTo(TimeUnit.class, "MICROSECONDS"));
     }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/EnumConverterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/EnumConverterTest.java
index 9981c70f0f18..ccfaf995daeb 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/EnumConverterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/EnumConverterTest.java
@@ -139,4 +139,14 @@ public class EnumConverterTest extends ContextTestSupport {
         GET_USERS_BY_TOPIC
     }
 
+    @Test
+    public void testEnumPreferExactMatch() {
+        assertSame(CaseEnum.a, 
context.getTypeConverter().convertTo(CaseEnum.class, "a"));
+        assertSame(CaseEnum.A, 
context.getTypeConverter().convertTo(CaseEnum.class, "A"));
+    }
+
+    public enum CaseEnum {
+        a,
+        A
+    }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/FutureConverterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/FutureConverterTest.java
index d93a04e19023..a059ea5cbdcc 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/FutureConverterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/FutureConverterTest.java
@@ -17,11 +17,13 @@
 package org.apache.camel.converter;
 
 import java.sql.Timestamp;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.Exchange;
 import org.apache.camel.NoTypeConversionAvailableException;
+import org.apache.camel.TypeConversionException;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.support.DefaultExchange;
 import org.junit.jupiter.api.Test;
@@ -30,6 +32,7 @@ import org.junit.jupiter.api.parallel.Isolated;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @Isolated
 public class FutureConverterTest extends ContextTestSupport {
@@ -113,4 +116,20 @@ public class FutureConverterTest extends 
ContextTestSupport {
             }
         };
     }
+
+    @Test
+    public void testConvertInterruptedFuture() {
+        Future<String> future = new CompletableFuture<>() {
+            @Override
+            public String get() throws InterruptedException {
+                throw new InterruptedException("Interrupted");
+            }
+        };
+        Exchange exchange = new DefaultExchange(context);
+
+        assertThrows(TypeConversionException.class,
+                () -> context.getTypeConverter().convertTo(String.class, 
exchange, future));
+        // the interrupted flag should be restored (and interrupted() clears 
it again)
+        assertTrue(Thread.interrupted());
+    }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/NIOConverterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/NIOConverterTest.java
index a8bf1349b9de..2246d0e9a181 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/NIOConverterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/NIOConverterTest.java
@@ -223,4 +223,14 @@ class NIOConverterTest extends ContextTestSupport {
         assertEquals("Hello", IOConverter.toString(is, null));
     }
 
+    @Test
+    public void testToByteBufferWithCharsetHeader() {
+        Exchange exchange = 
context.getEndpoint("direct:start").createExchange();
+        exchange.getIn().setHeader(Exchange.CHARSET_NAME, "UTF-16BE");
+
+        ByteBuffer bb = NIOConverter.toByteBuffer("A", exchange);
+        assertEquals(2, bb.remaining());
+        assertEquals(0, bb.get());
+        assertEquals('A', bb.get());
+    }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/ObjectConverterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/ObjectConverterTest.java
index 7d76d07a217e..e818aee68263 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/ObjectConverterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/ObjectConverterTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.converter;
 
+import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.charset.StandardCharsets;
 import java.util.Date;
@@ -146,6 +147,10 @@ public class ObjectConverterTest {
         assertNull(ObjectConverter.toBigInteger(Float.NaN));
         assertEquals(BigInteger.valueOf(4), 
ObjectConverter.toBigInteger(Long.valueOf("4")));
         assertEquals(new BigInteger("14350442579497085228"), 
ObjectConverter.toBigInteger("14350442579497085228"));
+        assertEquals(new BigInteger("123456789012345678901234"),
+                ObjectConverter.toBigInteger(new 
BigDecimal("123456789012345678901234.56")));
+        assertEquals(new BigInteger("100000000000000000000"), 
ObjectConverter.toBigInteger(1e20));
+        assertEquals(BigInteger.ONE, ObjectConverter.toBigInteger(1.9d));
     }
 
     @Test
@@ -162,6 +167,7 @@ public class ObjectConverterTest {
         assertEquals('A', 
ObjectConverter.toChar("A".getBytes(StandardCharsets.UTF_8)));
         assertEquals(Character.valueOf('A'), ObjectConverter.toCharacter("A"));
         assertEquals(Character.valueOf('A'), 
ObjectConverter.toCharacter("A".getBytes(StandardCharsets.UTF_8)));
+        assertEquals('\u00e9', ObjectConverter.toChar(new byte[] { (byte) 0xE9 
}));
     }
 
     @Test
@@ -195,4 +201,15 @@ public class ObjectConverterTest {
         assertThrows(IllegalArgumentException.class, () -> 
ObjectConverter.toBool("yes"), "Should throw exception");
     }
 
+    @Test
+    public void testToNumber() {
+        assertEquals(123, ObjectConverter.toNumber("123"));
+        assertEquals(Integer.MAX_VALUE, 
ObjectConverter.toNumber("2147483647"));
+        assertEquals(Integer.MIN_VALUE, 
ObjectConverter.toNumber("-2147483648"));
+        assertEquals(2147483648L, ObjectConverter.toNumber("2147483648"));
+        assertEquals(-3000000000L, ObjectConverter.toNumber("-3000000000"));
+        assertEquals(1.5d, ObjectConverter.toNumber("1.5"));
+        assertEquals(1e5d, ObjectConverter.toNumber("1e5"));
+        assertEquals(2.5e-3d, ObjectConverter.toNumber("2.5E-3"));
+    }
 }
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
new file mode 100644
index 000000000000..b8e2906c90c0
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/converter/CoreTypeConverterRegistryTest.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.impl.converter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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.spi.TypeConvertible;
+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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CoreTypeConverterRegistryTest extends ContextTestSupport {
+
+    @Test
+    public void testConvertWrapperToOtherPrimitive() throws Exception {
+        TypeConverter tc = context.getTypeConverter();
+
+        assertInstanceOf(Integer.class, tc.convertTo(int.class, 5L));
+        assertInstanceOf(Long.class, tc.convertTo(long.class, 5));
+        assertInstanceOf(Double.class, tc.convertTo(double.class, 5));
+        assertInstanceOf(Integer.class, tc.mandatoryConvertTo(int.class, 5L));
+        assertInstanceOf(Long.class, tc.mandatoryConvertTo(long.class, 7));
+        assertInstanceOf(Double.class, tc.tryConvertTo(double.class, 7));
+        assertInstanceOf(Integer.class, tc.tryConvertTo(int.class, 7L));
+
+        int i = tc.convertTo(int.class, 5L);
+        assertEquals(5, i);
+        long l = tc.convertTo(long.class, 5);
+        assertEquals(5L, l);
+
+        // same wrapper type is returned as-is
+        assertInstanceOf(Integer.class, tc.convertTo(int.class, 5));
+        assertInstanceOf(Long.class, tc.convertTo(long.class, 5L));
+    }
+
+    @Test
+    public void testTryConvertToPrimitiveBoolean() {
+        TypeConverter tc = context.getTypeConverter();
+
+        assertNull(tc.tryConvertTo(boolean.class, "abc"));
+        assertNull(tc.tryConvertTo(boolean.class, new Object()));
+        assertEquals(Boolean.TRUE, tc.tryConvertTo(boolean.class, "true"));
+    }
+
+    @Test
+    public void testBeanWithIntParameterAndLongBody() {
+        assertEquals("int:5", template.requestBody("direct:int", 5L));
+        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());
+    }
+
+    @Test
+    public void testSuperTypeMatchIsDeterministic() {
+        TypeConverter fromFirst = new NamedConverter("first");
+        TypeConverter fromSecond = new NamedConverter("second");
+        TypeConverter fromBase = new NamedConverter("base");
+        TypeConverter fromObject = new NamedConverter("object");
+
+        // regardless of the order the converters are registered, the nearest 
super type wins
+        for (boolean reverse : List.of(false, true)) {
+            Map<TypeConvertible<?, ?>, TypeConverter> converters = new 
LinkedHashMap<>();
+            List<Object[]> entries = new ArrayList<>(
+                    List.of(new Object[] { Object.class, fromObject }, new 
Object[] { Base.class, fromBase },
+                            new Object[] { SecondIface.class, fromSecond }, 
new Object[] { FirstIface.class, fromFirst }));
+            if (reverse) {
+                Collections.reverse(entries);
+            }
+            for (Object[] e : entries) {
+                converters.put(new TypeConvertible<>((Class<?>) e[0], 
Foo.class), (TypeConverter) e[1]);
+            }
+
+            // interfaces are tried in declared order
+            assertSame(fromFirst, TypeResolverHelper.tryMatch(new 
TypeConvertible<>(TwoIfaces.class, Foo.class), converters));
+            // the super class is nearer than the object converter
+            assertSame(fromBase, TypeResolverHelper.tryMatch(new 
TypeConvertible<>(Sub.class, Foo.class), converters));
+            // the interface of the super class is nearer than object
+            assertSame(fromSecond,
+                    TypeResolverHelper.tryMatch(new 
TypeConvertible<>(SubOfSecond.class, Foo.class), converters));
+            // object is the last resort
+            assertSame(fromObject, TypeResolverHelper.tryMatch(new 
TypeConvertible<>(String.class, Foo.class), converters));
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:int").bean(MyNumberBean.class, "intArg");
+                from("direct:long").bean(MyNumberBean.class, "longArg");
+            }
+        };
+    }
+
+    public static class MyNumberBean {
+        public String intArg(int x) {
+            return "int:" + x;
+        }
+
+        public String longArg(long x) {
+            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;
+        }
+    }
+
+    public interface FirstIface {
+    }
+
+    public interface SecondIface {
+    }
+
+    public static class TwoIfaces implements FirstIface, SecondIface {
+    }
+
+    public static class ImplOfSecond implements SecondIface {
+    }
+
+    public static class SubOfSecond extends ImplOfSecond {
+    }
+
+    private static class NamedConverter extends TypeConverterSupport {
+        private final String name;
+
+        NamedConverter(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public <T> T convertTo(Class<T> type, Exchange exchange, Object value) 
{
+            return type.cast(new Foo(name));
+        }
+    }
+
+    @Test
+    public void testArrayToCollectionTypes() {
+        TypeConverter tc = context.getTypeConverter();
+
+        assertInstanceOf(List.class, tc.convertTo(List.class, new String[] { 
"a", "b" }));
+        assertInstanceOf(ArrayList.class, tc.convertTo(ArrayList.class, new 
String[] { "a", "b" }));
+        assertInstanceOf(List.class, tc.convertTo(List.class, new int[] { 1, 2 
}));
+        // a set is not a list
+        assertInstanceOf(Set.class, tc.convertTo(Set.class, new String[] { 
"a", "b" }));
+        assertNull(tc.convertTo(Set.class, new int[] { 1, 2 }));
+    }
+}
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 cfe47db7bc9a..7efe7a4035ba 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
@@ -564,6 +564,33 @@ set here, because that would reject input that parses 
today. Routes that genuine
 an external DTD or parameter entity through this converter must supply their 
own
 `SAXParserFactory`.
 
+=== camel-core - type converter
+
+Several bugs in the type converter have been fixed, and some of the fixes 
change behavior:
+
+* Converting to a primitive type (such as `int.class`) now returns the 
matching wrapper type.
+Before, an `Integer` or `Long` value was returned as-is for any primitive 
type, so
+`convertTo(int.class, 5L)` returned a `Long`. For example, a bean method with 
an `int` parameter
+failed with `argument type mismatch` when the message body was a `Long`.
+* `tryConvertTo(boolean.class, value)` returns `null` instead of throwing an 
exception when the value
+cannot be converted.
+* When no type converter can convert a pair of types, the registry records a 
miss so it does not
+search for a converter again. Fallback type converters are now still tried for 
such a pair,
+because a fallback converter can convert depending on the value. Recorded 
misses are also cleared when
+a type converter or fallback type converter is added.
+* When there is no type converter for the exact type of the value, the 
registry uses a type converter
+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.
+* 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.
+* Converting a `byte[]` to `char` no longer sign-extends bytes above 127.
+* Converting a `String` to `ByteBuffer` now uses the charset from the 
`CamelCharsetName` header or exchange
+property (like the other converters), and falls back to the default charset.
+* Converting an array to a `Set` no longer returns a `List`.
+* Enum conversion prefers a constant that matches exactly before matching 
case-insensitively.
+
 === camel-core - a failed route reload restores the previous routes
 
 When a route file is reloaded in dev mode (`camel run --dev`, or the route 
watcher reload strategy in general) and

Reply via email to