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`,

Reply via email to