This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25009 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2da9b18297f57569d0f9c80f9d4cab6588c14d0b Author: Claus Ibsen <[email protected]> AuthorDate: Thu Sep 24 19:20:22 2026 +0200 CAMEL-25009: camel-core - Bean binding: @PropertyInject hides an invalid value, and overloaded constructors are not found - @PropertyInject used the default value also when the property existed but its value could not be converted, which hid the mistake. The default value is now only used when the property cannot be resolved. - When more than one constructor or factory method matched the given parameters (such as BigDecimal(5), or int and long overloads), none was chosen. The most specific is now chosen, as Java would. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../impl/engine/CamelPostProcessorHelper.java | 59 +++++++-------- .../impl/engine/CamelPostProcessorHelperTest.java | 17 +++++ .../PropertyBindingSupportOverloadTest.java | 87 ++++++++++++++++++++++ .../camel/support/PropertyBindingSupport.java | 64 ++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 7 ++ 5 files changed, 205 insertions(+), 29 deletions(-) diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java index a927ac2f8017..fd242b6ab0ac 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java @@ -279,40 +279,41 @@ public class CamelPostProcessorHelper implements CamelContextAware { public Object getInjectionPropertyValue( Class<?> type, Type genericType, String propertyName, String propertyDefaultValue, String separator) { + String key; + String prefix = PropertiesComponent.PREFIX_TOKEN; + String suffix = PropertiesComponent.SUFFIX_TOKEN; + if (!propertyName.contains(prefix)) { + // must enclose the property name with prefix/suffix to have it resolved + key = prefix + propertyName + suffix; + } else { + // key has already prefix/suffix so use it as-is as it may be a compound key + key = propertyName; + } + + String value; try { - String key; - String prefix = PropertiesComponent.PREFIX_TOKEN; - String suffix = PropertiesComponent.SUFFIX_TOKEN; - if (!propertyName.contains(prefix)) { - // must enclose the property name with prefix/suffix to have it resolved - key = prefix + propertyName + suffix; + value = getCamelContext().resolvePropertyPlaceholders(key); + } catch (Exception e) { + // the property could not be resolved (such as not existing), so use the default value if any + if (ObjectHelper.isNotEmpty(propertyDefaultValue)) { + value = propertyDefaultValue; } else { - // key has already prefix/suffix so use it as-is as it may be a compound key - key = propertyName; + throw RuntimeCamelException.wrapRuntimeCamelException(e); } - String value = getCamelContext().resolvePropertyPlaceholders(key); - if (value != null) { - if (separator != null && !separator.isBlank()) { - Object values = convertValueUsingSeparator(camelContext, type, genericType, value, separator); - return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values); - } - return getCamelContext().getTypeConverter().mandatoryConvertTo(type, value); - } else { - return null; + } + if (value == null) { + return null; + } + + // a value that cannot be converted to the type is an error (the default value is not used instead, as that + // would hide a mistake in the configured value) + try { + if (separator != null && !separator.isBlank()) { + Object values = convertValueUsingSeparator(camelContext, type, genericType, value, separator); + return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values); } + return getCamelContext().getTypeConverter().mandatoryConvertTo(type, value); } catch (Exception e) { - if (ObjectHelper.isNotEmpty(propertyDefaultValue)) { - try { - if (separator != null && !separator.isBlank()) { - Object values - = convertValueUsingSeparator(camelContext, type, genericType, propertyDefaultValue, separator); - return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values); - } - return getCamelContext().getTypeConverter().mandatoryConvertTo(type, propertyDefaultValue); - } catch (Exception e2) { - throw RuntimeCamelException.wrapRuntimeCamelException(e2); - } - } throw RuntimeCamelException.wrapRuntimeCamelException(e); } } diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java index 63b26c88fdad..15a0283a9ecd 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java @@ -406,6 +406,23 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport { assertEquals("Hello Camel", value); } + @Test + public void testPropertyFieldInvalidValueNotDefaultValue() throws Exception { + // the property exists, but its value is not a number + myProp.put("myTimeout", "abc"); + + CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context); + + MyPropertyFieldBean bean = new MyPropertyFieldBean(); + + Field field = bean.getClass().getField("timeout"); + PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); + Class<?> type = field.getType(); + // the default value must not hide the invalid value + assertThrows(RuntimeCamelException.class, + () -> helper.getInjectionPropertyValue(type, null, propertyInject.value(), "5000", "")); + } + @Test public void testPropertyFieldSeparatorArrayInject() throws Exception { myProp.put("serverPorts", "4444;5555"); // test with semicolon as separator diff --git a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportOverloadTest.java b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportOverloadTest.java new file mode 100644 index 000000000000..16ee49d221ed --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportOverloadTest.java @@ -0,0 +1,87 @@ +/* + * 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.support; + +import java.math.BigDecimal; + +import org.apache.camel.ContextTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Constructors and factory methods that are overloaded for the given parameters use the most specific, as Java does. + */ +public class PropertyBindingSupportOverloadTest extends ContextTestSupport { + + @Override + public boolean isUseRouteBuilder() { + return false; + } + + @Test + public void testConstructorOverloadedWithNumberTypes() throws Exception { + // BigDecimal has constructors with int, long and BigInteger (among others) + Object answer = PropertyBindingSupport.newInstanceConstructorParameters(context, BigDecimal.class, "5"); + assertEquals(new BigDecimal(5), answer); + } + + @Test + public void testFactoryMethodOverloadedWithIntAndLong() throws Exception { + Object answer = PropertyBindingSupport.newInstanceFactoryParameters(context, MyFactory.class, "create", "5"); + assertEquals("int:5", answer); + } + + @Test + public void testConstructorOverloadedWithBeanTypes() throws Exception { + context.getRegistry().bind("myBean", new StringBuilder("Camel")); + Object answer = PropertyBindingSupport.newInstanceConstructorParameters(context, MyBeanUser.class, "#bean:myBean"); + assertEquals("StringBuilder:Camel", answer.toString()); + } + + public static final class MyFactory { + + private MyFactory() { + } + + public static String create(int value) { + return "int:" + value; + } + + public static String create(long value) { + return "long:" + value; + } + } + + public static final class MyBeanUser { + + private final String text; + + public MyBeanUser(CharSequence value) { + this.text = "CharSequence:" + value; + } + + public MyBeanUser(StringBuilder value) { + this.text = "StringBuilder:" + value; + } + + @Override + public String toString() { + return text; + } + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java index a7c4d037a237..330aef236984 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java @@ -18,6 +18,7 @@ package org.apache.camel.support; import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -1630,6 +1631,13 @@ public final class PropertyBindingSupport { } } + if (candidates.size() > 1) { + // more than one matches (such as overloaded with int and long), so choose the most specific + Constructor<?> best = mostSpecific(camelContext, candidates, params); + if (best != null) { + return best; + } + } return candidates.size() == 1 ? candidates.get(0) : fallbackCandidate; } @@ -1756,9 +1764,65 @@ public final class PropertyBindingSupport { } } + if (candidates.size() > 1) { + // more than one matches (such as overloaded with int and long), so choose the most specific + Method best = mostSpecific(camelContext, candidates, params); + if (best != null) { + return best; + } + } return candidates.size() == 1 ? candidates.get(0) : fallbackCandidate; } + /** + * Chooses the most specific of the matching constructors or factory methods, the same way as Java would choose for + * the given parameters: a whole number is an int (and then a long), a boolean is a boolean, and a bean is of its + * own type (rather than a super type). + * + * @return the most specific, or <tt>null</tt> if there is no single most specific + */ + private static <T extends Executable> T mostSpecific(CamelContext camelContext, List<T> candidates, String[] params) { + T best = null; + int bestScore = -1; + boolean tie = false; + for (T candidate : candidates) { + int score = 0; + Class<?>[] types = candidate.getParameterTypes(); + for (int i = 0; i < types.length; i++) { + String parameter = params[i] != null ? params[i].trim() : null; + score += specificity(getValidParameterType(camelContext, parameter), types[i]); + } + if (score > bestScore) { + best = candidate; + bestScore = score; + tie = false; + } else if (score == bestScore) { + tie = true; + } + } + return tie ? null : best; + } + + private static int specificity(Class<?> parameterType, Class<?> expectedType) { + if (parameterType == null) { + // unknown type of parameter, so it does not prefer any candidate + return 0; + } + if (Number.class.equals(parameterType)) { + if (int.class.equals(expectedType) || Integer.class.equals(expectedType)) { + return 3; + } + if (long.class.equals(expectedType) || Long.class.equals(expectedType)) { + return 2; + } + return 1; + } + if (Boolean.class.equals(parameterType)) { + return boolean.class.equals(expectedType) || Boolean.class.equals(expectedType) ? 3 : 1; + } + return parameterType.equals(expectedType) ? 3 : 1; + } + /** * Determines and maps the given value is valid according to the supported values by the bean component. * <p/> 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 f70b258eedc1..d8ce3c351f39 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 @@ -97,6 +97,13 @@ counted as inflight and its unit of work not done. The same now happens when the An exception thrown from the `after` method of an advice no longer replaces the exception the exchange has already failed with. Instead, it is added as a suppressed exception to the existing exception. +=== @PropertyInject - an invalid property value is an error + +When a property injected with `@PropertyInject` has a value that cannot be converted to the type of the field or +parameter, the injection now fails. Prior to Camel 4.23 the `defaultValue` was silently used instead, so a mistake in +the configured value (such as `port=80a` for an `int`) went unnoticed. The `defaultValue` is still used when the +property does not exist. + === Circuit Breaker EIP The exchange property `CamelCircuitBreakerResponseRejected` is now also set inside the `onFallback`,
