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