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 6e6282c2e7b5 CAMEL-25092: camel-support - property binding creates a
nested list element at its index (#26993)
6e6282c2e7b5 is described below
commit 6e6282c2e7b5ecf1b071585eb4747440c5f6e654
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Sep 28 19:17:09 2026 +0530
CAMEL-25092: camel-support - property binding creates a nested list element
at its index (#26993)
When a property key goes through a list element that does not exist yet,
such as servers[1].host, PropertyBindingSupport created the element and
appended it with list.add, whatever the index was. The next key with the
same index found no element at that index either and created another one.
Because the keys are sorted as strings (servers[10] before servers[2]), a
dense, 0-based list of 11 or more nested beans lost elements silently, and
a list numbered from 1 or with gaps had its keys spread over several
elements.
The element is now created with ObjectHelper.addListByIndex, as a single
key such as names[3]=x already did, so the list is padded with null up to
the index. The empty key [] still appends.
The list key "last", documented but never implemented, now refers to the
last element of a list (index 0 for an empty list). Arrays still need a
number.
The property binding docs say that the list is padded with null, and the
4.23 upgrade guide notes that a list numbered from 1 or with gaps, with one
key per element, now has null elements.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../support/PropertyBindingSupportListTest.java | 236 +++++++++++++++++++++
.../java/org/apache/camel/main/MainBeansTest.java | 63 ++++++
.../camel/support/PropertyBindingSupport.java | 36 +++-
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 16 ++
.../modules/ROOT/pages/property-binding.adoc | 2 +-
5 files changed, 345 insertions(+), 8 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportListTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportListTest.java
index 0fe4c088d705..7940b15e6f54 100644
---
a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportListTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportListTest.java
@@ -24,10 +24,13 @@ import java.util.Properties;
import org.apache.camel.CamelContext;
import org.apache.camel.ContextTestSupport;
import org.apache.camel.PropertyBindingException;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.spi.PropertyConfigurerGetter;
import org.junit.jupiter.api.Test;
import static org.apache.camel.util.CollectionHelper.mapOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -171,6 +174,153 @@ public class PropertyBindingSupportListTest extends
ContextTestSupport {
assertEquals("I changed this", bar.getWorks().get(1).getName());
}
+ @Test
+ public void testPropertiesListNestedMoreThanTenElements() {
+ Foo foo = new Foo();
+
+ // the keys are sorted as strings, so works[10] and works[11] are
bound before works[2]
+ Map<String, Object> prop = new LinkedHashMap<>();
+ for (int i = 0; i < 12; i++) {
+ prop.put("bar.works[" + i + "].id", String.valueOf(100 + i));
+ prop.put("bar.works[" + i + "].name", "Company " + i);
+ }
+
+ PropertyBindingSupport.build().bind(context, foo, prop);
+
+ List<Company> works = foo.getBar().getWorks();
+ assertEquals(12, works.size());
+ for (int i = 0; i < 12; i++) {
+ assertEquals(100 + i, works.get(i).getId());
+ assertEquals("Company " + i, works.get(i).getName());
+ }
+ }
+
+ @Test
+ public void testPropertiesListNestedWithGapsNoDeclaration() {
+ Foo foo = new Foo();
+
+ Map<String, Object> prop = new LinkedHashMap<>();
+ prop.put("bar.works[1].id", "123");
+ prop.put("bar.works[1].name", "Acme");
+
+ PropertyBindingSupport.build().bind(context, foo, prop);
+
+ List<Company> works = foo.getBar().getWorks();
+ assertEquals(2, works.size());
+ assertNull(works.get(0));
+ assertEquals(123, works.get(1).getId());
+ assertEquals("Acme", works.get(1).getName());
+ }
+
+ @Test
+ public void testPropertiesListNestedSparse() {
+ Foo foo = new Foo();
+
+ Map<String, Object> prop = new LinkedHashMap<>();
+ prop.put("bar.works[0].name", "Zero");
+ prop.put("bar.works[5].id", "5");
+ prop.put("bar.works[5].name", "Five");
+
+ PropertyBindingSupport.build().bind(context, foo, prop);
+
+ List<Company> works = foo.getBar().getWorks();
+ assertEquals(6, works.size());
+ assertEquals("Zero", works.get(0).getName());
+ for (int i = 1; i < 5; i++) {
+ assertNull(works.get(i));
+ }
+ assertEquals(5, works.get(5).getId());
+ assertEquals("Five", works.get(5).getName());
+ }
+
+ @Test
+ public void testPropertiesListNestedFromOne() {
+ Foo foo = new Foo();
+
+ // numbered from 1: the element at index 0 is null
+ Map<String, Object> prop = new LinkedHashMap<>();
+ prop.put("bar.works[1].name", "One");
+ prop.put("bar.works[2].name", "Two");
+
+ PropertyBindingSupport.build().bind(context, foo, prop);
+
+ List<Company> works = foo.getBar().getWorks();
+ assertEquals(3, works.size());
+ assertNull(works.get(0));
+ assertEquals("One", works.get(1).getName());
+ assertEquals("Two", works.get(2).getName());
+ }
+
+ @Test
+ public void testPropertiesListNestedWithGapsViaConfigurer() {
+ context.getRegistry().bind(Company.class.getName(), new
CompanyConfigurer());
+
+ Cluster cluster = new Cluster();
+
+ Map<String, Object> prop = new LinkedHashMap<>();
+ prop.put("servers[1].id", "1");
+ prop.put("servers[1].name", "One");
+ prop.put("servers[3].id", "3");
+ prop.put("servers[3].name", "Three");
+
+ // no reflection so the configurers must do all the work
+ PropertyBindingSupport.build().withConfigurer(new
ClusterConfigurer()).withReflection(false)
+ .bind(context, cluster, prop);
+
+ List<Company> servers = cluster.getServers();
+ assertEquals(4, servers.size());
+ assertNull(servers.get(0));
+ assertEquals(1, servers.get(1).getId());
+ assertEquals("One", servers.get(1).getName());
+ assertNull(servers.get(2));
+ assertEquals(3, servers.get(3).getId());
+ assertEquals("Three", servers.get(3).getName());
+ }
+
+ @Test
+ public void testPropertiesListLast() {
+ Foo foo = new Foo();
+
+ PropertyBindingSupport.build().bind(context, foo, mapOf(
+ "bar.works[0]", "#bean:company1",
+ "bar.works[1]", "#class:" + Company.class.getName(),
+ "bar.names[0]", "a",
+ "bar.names[1]", "b"));
+ assertEquals(2, foo.getBar().getWorks().size());
+ assertEquals(2, foo.getBar().getNames().size());
+
+ // last refers to the last element of the list
+ PropertyBindingSupport.build().bind(context, foo, mapOf(
+ "bar.works[last].id", "789",
+ "bar.works[last].name", "Last",
+ "bar.names[last]", "z"));
+
+ List<Company> works = foo.getBar().getWorks();
+ assertEquals(2, works.size());
+ assertEquals(123, works.get(0).getId());
+ assertEquals("Acme", works.get(0).getName());
+ assertEquals(789, works.get(1).getId());
+ assertEquals("Last", works.get(1).getName());
+ assertEquals(List.of("a", "z"), foo.getBar().getNames());
+ }
+
+ @Test
+ public void testPropertiesListLastEmpty() {
+ Foo foo = new Foo();
+
+ // last on an empty list creates the first element
+ PropertyBindingSupport.build().bind(context, foo, mapOf(
+ "bar.works[last].id", "789",
+ "bar.works[last].name", "Last",
+ "bar.names[last]", "z"));
+
+ List<Company> works = foo.getBar().getWorks();
+ assertEquals(1, works.size());
+ assertEquals(789, works.get(0).getId());
+ assertEquals("Last", works.get(0).getName());
+ assertEquals(List.of("z"), foo.getBar().getNames());
+ }
+
@Test
public void testPropertiesNotList() {
Foo foo = new Foo();
@@ -216,6 +366,7 @@ public class PropertyBindingSupportListTest extends
ContextTestSupport {
private int age;
private boolean rider;
private List<Company> works; // should auto-create this via the setter
+ private List<String> names;
private boolean goldCustomer;
public int getAge() {
@@ -242,6 +393,14 @@ public class PropertyBindingSupportListTest extends
ContextTestSupport {
this.works = works;
}
+ public List<String> getNames() {
+ return names;
+ }
+
+ public void setNames(List<String> names) {
+ this.names = names;
+ }
+
public boolean isGoldCustomer() {
return goldCustomer;
}
@@ -251,4 +410,81 @@ public class PropertyBindingSupportListTest extends
ContextTestSupport {
}
}
+ public static class Cluster {
+ private List<Company> servers;
+
+ public List<Company> getServers() {
+ return servers;
+ }
+
+ public void setServers(List<Company> servers) {
+ this.servers = servers;
+ }
+ }
+
+ private static class ClusterConfigurer implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean configure(CamelContext camelContext, Object target,
String name, Object value, boolean ignoreCase) {
+ if ("servers".equals(name)) {
+ ((Cluster) target).setServers((List<Company>) value);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ return "servers".equals(name) ? List.class : null;
+ }
+
+ @Override
+ public Object getOptionValue(Object target, String name, boolean
ignoreCase) {
+ return "servers".equals(name) ? ((Cluster) target).getServers() :
null;
+ }
+
+ @Override
+ public Object getCollectionValueType(Object target, String name,
boolean ignoreCase) {
+ return "servers".equals(name) ? Company.class : null;
+ }
+ }
+
+ private static class CompanyConfigurer implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object target,
String name, Object value, boolean ignoreCase) {
+ Company company = (Company) target;
+ if ("id".equals(name)) {
+ company.setId(Integer.parseInt(value.toString()));
+ return true;
+ } else if ("name".equals(name)) {
+ company.setName(value.toString());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ if ("id".equals(name)) {
+ return int.class;
+ } else if ("name".equals(name)) {
+ return String.class;
+ }
+ return null;
+ }
+
+ @Override
+ public Object getOptionValue(Object target, String name, boolean
ignoreCase) {
+ Company company = (Company) target;
+ if ("id".equals(name)) {
+ return company.getId();
+ } else if ("name".equals(name)) {
+ return company.getName();
+ }
+ return null;
+ }
+ }
+
}
diff --git
a/core/camel-main/src/test/java/org/apache/camel/main/MainBeansTest.java
b/core/camel-main/src/test/java/org/apache/camel/main/MainBeansTest.java
index 175dea1379d9..ccbdc0d45ca5 100644
--- a/core/camel-main/src/test/java/org/apache/camel/main/MainBeansTest.java
+++ b/core/camel-main/src/test/java/org/apache/camel/main/MainBeansTest.java
@@ -172,6 +172,36 @@ public class MainBeansTest {
main.stop();
}
+ @Test
+ public void testBindBeansNestedListMoreThanTenElements() {
+ Main main = new Main();
+ main.configure().addRoutesBuilder(new MyRouteBuilder());
+
+ // the elements are created when binding their nested keys
(servers[10] is bound before servers[2])
+ main.addProperty("camel.beans.cluster", "#class:" +
MyCluster.class.getName());
+ for (int i = 0; i < 12; i++) {
+ main.addProperty("camel.beans.cluster.servers[" + i + "].host",
"host" + i);
+ main.addProperty("camel.beans.cluster.servers[" + i + "].port",
String.valueOf(1000 + i));
+ }
+
+ main.start();
+
+ CamelContext camelContext = main.getCamelContext();
+ assertNotNull(camelContext);
+
+ MyCluster cluster =
camelContext.getRegistry().lookupByNameAndType("cluster", MyCluster.class);
+ assertNotNull(cluster);
+
+ List<MyServer> servers = cluster.getServers();
+ assertEquals(12, servers.size());
+ for (int i = 0; i < 12; i++) {
+ assertEquals("host" + i, servers.get(i).getHost());
+ assertEquals(1000 + i, servers.get(i).getPort());
+ }
+
+ main.stop();
+ }
+
@Test
public void testBindBeansMapSquareDotKey() {
Main main = new Main();
@@ -272,6 +302,39 @@ public class MainBeansTest {
main.stop();
}
+ public static class MyCluster {
+ private List<MyServer> servers;
+
+ public List<MyServer> getServers() {
+ return servers;
+ }
+
+ public void setServers(List<MyServer> servers) {
+ this.servers = servers;
+ }
+ }
+
+ public static class MyServer {
+ private String host;
+ private int port;
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+ }
+
public static class MyRouteBuilder extends RouteBuilder {
@Override
public void configure() {
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 330aef236984..d962bc6058a5 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
@@ -63,7 +63,8 @@ import static
org.apache.camel.util.StringHelper.startsWithIgnoreCase;
* <li>map</li> - Properties can lookup in Map's using map syntax, eg foo[bar]
where foo is the name of the property
* that is a Map instance, and bar is the name of the key.</li>
* <li>list</li> - Properties can refer or add to in List's using list syntax,
eg foo[0] where foo is the name of the
- * property that is a List instance, and 0 is the index. To refer to the last
element, then use last as key.</li>
+ * property that is a List instance, and 0 is the index. To refer to the last
element, then use last as key. An index
+ * beyond the end of the list pads the list with null elements up to the
index.</li>
* <li>reference by property placeholder id - Values can refer to a property
placeholder key with #property:myKey</li>
* <li>reference by bean id - Values can refer to other beans in the registry
by prefixing with # or #bean: eg #myBean
* or #bean:myBean. It is recommended to favour using `#bean:` syntax to make
it obvious it's a bean reference.</li>
@@ -613,6 +614,17 @@ public final class PropertyBindingSupport {
return name.contains("[") && name.endsWith("]");
}
+ /**
+ * The index of a list key: a number, or {@code last} for the last element
of the list (index 0 if the list is
+ * empty).
+ */
+ private static int listIndex(String lookupKey, int size) {
+ if ("last".equals(lookupKey)) {
+ return Math.max(0, size - 1);
+ }
+ return Integer.parseInt(lookupKey);
+ }
+
private static boolean setPropertyCollectionViaReflection(
CamelContext context, Object target, String name, Object value,
boolean ignoreCase, boolean reference, boolean optional)
@@ -673,7 +685,7 @@ public final class PropertyBindingSupport {
return true;
} else if (obj instanceof List list) {
if (isNotEmpty(lookupKey)) {
- int idx = Integer.parseInt(lookupKey);
+ int idx = listIndex(lookupKey, list.size());
org.apache.camel.util.ObjectHelper.addListByIndex(list, idx,
value);
} else {
list.add(value);
@@ -757,7 +769,7 @@ public final class PropertyBindingSupport {
return true;
} else if (obj instanceof List list) {
if (isNotEmpty(lookupKey)) {
- int idx = Integer.parseInt(lookupKey);
+ int idx = listIndex(lookupKey, list.size());
if (idx < list.size()) {
list.set(idx, value);
} else if (idx == list.size()) {
@@ -992,7 +1004,7 @@ public final class PropertyBindingSupport {
}
} else if (answer instanceof List list) {
if (isNotEmpty(lookupKey)) {
- int idx = Integer.parseInt(lookupKey);
+ int idx = listIndex(lookupKey, list.size());
answer = list.size() > idx ? list.get(idx) : null;
} else {
if (list.isEmpty()) {
@@ -1009,7 +1021,12 @@ public final class PropertyBindingSupport {
if (parameterType != null
&&
org.apache.camel.util.ObjectHelper.hasDefaultPublicNoArgConstructor(parameterType))
{
Object instance =
context.getInjector().newInstance(parameterType);
- list.add(instance);
+ if (isNotEmpty(lookupKey)) {
+ // create the element at its index (the list is padded
with null if needed)
+
org.apache.camel.util.ObjectHelper.addListByIndex(list, listIndex(lookupKey,
list.size()), instance);
+ } else {
+ list.add(instance);
+ }
answer = instance;
}
}
@@ -1123,7 +1140,7 @@ public final class PropertyBindingSupport {
}
} else if (answer instanceof List list) {
if (isNotEmpty(lookupKey)) {
- int idx = Integer.parseInt(lookupKey);
+ int idx = listIndex(lookupKey, list.size());
answer = list.size() > idx ? list.get(idx) : null;
} else {
if (list.isEmpty()) {
@@ -1153,7 +1170,12 @@ public final class PropertyBindingSupport {
if (parameterType != null
&&
org.apache.camel.util.ObjectHelper.hasDefaultPublicNoArgConstructor(parameterType))
{
Object instance =
context.getInjector().newInstance(parameterType);
- list.add(instance);
+ if (isNotEmpty(lookupKey)) {
+ // create the element at its index (the list is padded
with null if needed)
+
org.apache.camel.util.ObjectHelper.addListByIndex(list, listIndex(lookupKey,
list.size()), instance);
+ } else {
+ list.add(instance);
+ }
answer = instance;
}
}
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 f5c3bf47b730..d5a6dcc6fb92 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
@@ -791,6 +791,22 @@ kept running at full CPU without any log. No configuration
that worked before is
To take a value from somewhere else and otherwise a default, use another key
for the override, such as
`timeout={{?timeout.override:5000}}`.
+=== camel-support - property binding creates a nested list element at its index
+
+When a property key goes through a list element that does not exist yet, such
as
+`camel.beans.cluster.servers[1].host`, the element is now created at its
index, and the list is padded with `null`
+up to that index, as a single key such as `names[3]=x` and an array property
already did. Before, the new element
+was appended to the end of the list whatever its index was. So the keys of one
element could end up on several
+elements, and a list of 11 or more nested elements lost some of them, because
the keys are sorted as strings
+(`servers[10].host` is bound before `servers[2].host`).
+
+A configuration that numbers its elements from 1, or leaves a gap, with one
key per element, such as
+`servers[1].host=a` and `servers[2].host=b`, used to give a list without the
gap (`[a, b]`). It now gives
+`[null, a, b]`. Number the elements from 0 without gaps, or make the code that
uses the list skip `null` elements.
+
+The list key `last`, which was documented but failed with a
`NumberFormatException`, now refers to the last element
+of a list (index 0 when the list is empty). Arrays still need a number.
+
=== camel-core - XmlConverter SAX parser factory
`XmlConverter.createSAXParserFactory()` now also disables external parameter
entities and external
diff --git a/docs/user-manual/modules/ROOT/pages/property-binding.adoc
b/docs/user-manual/modules/ROOT/pages/property-binding.adoc
index 3b4873953314..ec7729b0c173 100644
--- a/docs/user-manual/modules/ROOT/pages/property-binding.adoc
+++ b/docs/user-manual/modules/ROOT/pages/property-binding.adoc
@@ -16,7 +16,7 @@ The `PropertyBindingSupport` class supports binding String
valued properties to
- _property placeholders_ - Keys and values using Camels property placeholder
will be resolved.
- _nested_ - Properties can be nested using the dot syntax (OGNL and builder
pattern using with as prefix), eg `foo.bar=123`.
- _map_ - Properties can lookup in Map's using map syntax, eg `foo[bar]` where
foo is the name of the property that is a Map instance, and bar is the name of
the key.
-- _list_ - Properties can refer or add to in List's using list syntax, eg
`foo[0]` where foo is the name of the property that is a List instance, and 0
is the index. To refer to the last element, then use `last` as key.
+- _list_ - Properties can refer or add to in List's using list syntax, eg
`foo[0]` where foo is the name of the property that is a List instance, and 0
is the index. To refer to the last element, then use `last` as key (for a List,
not an array). An index beyond the end of the list pads the list with `null`
elements up to the index, also when the element is created for a nested key
such as `foo[3].name`, so `foo[1].name` and `foo[2].name` without `foo[0]` give
a list whose first element [...]
- _reference by property placeholder id_ - Values can refer to a property
placeholder key with `#property:myKey`
- _reference by bean id_ - Values can refer to other beans in the registry by
prefixing with `#` or `#bean:` eg `#myBean` or `#bean:myBean`. It is
recommended to favour using `#bean:` syntax to make it obvious it's a bean
reference.
- _reference by type_ - Values can refer to singleton beans by their type in
the registry by prefixing with `#type:` syntax, eg `#type:com.foo.MyClassType`.