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 13d63ec4c0e5 CAMEL-25009: camel-core - Bean binding: @PropertyInject
hides an invalid value, and overloaded constructors are not found (#26863)
13d63ec4c0e5 is described below
commit 13d63ec4c0e580d232c176a1b8091aec75ec783b
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 26 21:25:53 2026 +0200
CAMEL-25009: camel-core - Bean binding: @PropertyInject hides an invalid
value, and overloaded constructors are not found (#26863)
- @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 | 11 +++
5 files changed, 209 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 4503dbd53743..4870b4b1242d 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
@@ -103,6 +103,17 @@ 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 (Breaking change)
+
+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.
+
+This can break an application that has such an invalid value configured and
worked because the `defaultValue` was
+good enough, as it now fails when starting. To keep the previous result,
correct the configured value, or remove the
+property so the `defaultValue` is used.
+
=== Circuit Breaker EIP
The exchange property `CamelCircuitBreakerResponseRejected` is now also set
inside the `onFallback`,