shashank created CAMEL-25092:
--------------------------------

             Summary: Property binding: nested list elements are appended, not 
created at their index, so a list of 11 or more beans or a list with gaps 
silently loses elements; the documented list key last fails
                 Key: CAMEL-25092
                 URL: https://issues.apache.org/jira/browse/CAMEL-25092
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank


A normal, 0-based list of 11 or more nested beans configured with property 
binding silently loses elements. For example, a Camel Main bean with a list of 
servers:

{noformat}
camel.beans.cluster = #class:com.foo.Cluster
camel.beans.cluster.servers[0].host = h0
camel.beans.cluster.servers[0].port = 1000
...
camel.beans.cluster.servers[10].host = h10
camel.beans.cluster.servers[10].port = 1010
{noformat}

gives a list of 10 servers, {{h0:1000}} to {{h9:1009}}. The server {{h10:1010}} 
is gone, and there is no error or warning. With 12 servers, {{h10}} and {{h11}} 
are both lost. The same happens with the properties of a YAML or XML bean, and 
wherever a map of properties is bound with {{PropertyBindingSupport}}.

Two things combine:

* {{bindProperties}} sorts the keys with {{PropertyBindingKeyComparator}}: 
first by the number of dots, then references first, then by plain 
{{String.compareTo}}. So the keys of element 10 are bound before those of 
element 1, because the character 0 sorts before the closing bracket.
* When a key goes through a list element, 
{{getOrCreatePropertyOgnlPathViaReflection}} and 
{{getOrCreatePropertyOgnlPathViaConfigurer}} look the element up with 
{{list.size() > idx ? list.get(idx) : null}}. If there is none, they create it 
and call {{list.add(instance)}}, which appends it at the end of the list, 
whatever the index is.

With the keys above, element 0 is created at index 0. The host of element 10 is 
then appended at index 1 and its port, which still finds no element at index 
10, at index 2. The keys of element 1 then find the element at index 1 and 
overwrite the host {{h10}}, and those of element 2 fill in the port-only 
element at index 2. Element 10 is lost.

The same append also spreads the keys of one element over several elements 
whenever the index is not the next free position, that is when the elements are 
numbered from 1 or have a gap:

{noformat}
servers[1].host=a, servers[1].port=1
  -> [Server{host=a, port=0}, Server{host=null, port=1}]
     expected [null, Server{host=a, port=1}]

servers[0].host=a, servers[5].host=z, servers[5].port=9
  -> [Server{host=a, port=0}, Server{host=z, port=0}, Server{host=null, port=9}]
     expected [Server{host=a, port=0}, null, null, null, null, Server{host=z, 
port=9}]
{noformat}

The rest of the list handling already places a value at its index and pads with 
null:
* a single key such as {{names\[2\]=x}} uses {{ObjectHelper.addListByIndex}},
* an array property is enlarged to the index,
* a list whose elements are declared first ({{servers\[3\]=#class:...}}, the 
CAMEL-15396 example) is right, because the declaring key creates the element at 
its index before the nested keys are bound.

Only an element created for a nested key is appended. Nothing documents that, 
and no test covers more than 2 nested elements.

*The list key "last"*

The documentation (property-binding.adoc) and the class javadoc say: "To refer 
to the last element, then use last as key." Every list and array index is 
parsed with {{Integer.parseInt}}, so {{names\[last\]=z}} and 
{{servers\[last\].port=7}} fail with {{NumberFormatException: For input string: 
"last"}}. The sentence has been in the javadoc since 3.0.0 and was never 
implemented. (An empty key in a nested key, {{servers\[\].port}}, does refer to 
the last element, but that is not documented.) The wording probably comes from 
the Simple language, where {{last}} works as a list index.

h3. Reproduction

A {{Config}} bean with a list of {{Server}} (host, port) and a list of String, 
bound with {{PropertyBindingSupport.build().bind(context, config, map)}} on 
main: the results above. The first example also fails through Camel Main 
itself: 12 servers bound with {{camel.beans.cluster.servers\[i\].host/port}} 
give 10 servers. The same results come with the options Camel Main uses for 
{{camel.beans}} (mandatory, ignore case, remove parameters), and with a 
configurer that implements {{getCollectionValueType}}. A small formal model 
(Lean 4) of the element lookup shows that, for any list and any index beyond 
its end, two keys with the same index are bound on two different elements, and 
that the new element is never at the index of the key. The model does not 
include the key order, so it does not cover the case with 11 or more elements; 
that one was found by a test and then traced through the comparator.

h3. Affected versions

The append ({{list.add(instance)}} in both methods) and the string order of the 
keys both came in 3.5.0, with nested list binding (CAMEL-15396), and both are 
in every release since, up to main. {{last}} is documented from 3.0.0 on and 
was never implemented.

h3. Proposed fix

* In both getOrCreate methods, create a missing element at its index with 
{{ObjectHelper.addListByIndex(list, idx, instance)}}, as the single key already 
does, and keep {{list.add}} only for the empty key. {{addListByIndex}} sets the 
element when the index is inside the list, so a null slot left by padding is 
filled in place and nothing shifts.
* Resolve the list key {{last}} to the index of the last element (0 for an 
empty list) where a list index is parsed. Arrays keep numeric indexes. If 
{{last}} is not wanted, the alternative is to remove the sentence from the 
documentation.
* Documentation: say that an index beyond the end of the list pads it with null.

Sorting the digits inside the brackets as numbers would hide the case with 11 
or more elements, but not the gaps. With the fix above the order of the keys no 
longer matters, so the comparator can stay as it is.

h3. Compatibility

A configuration that numbers its elements from 1, or leaves a gap, with one key 
per element ({{servers\[1\].host=a}}, {{servers\[2\].host=b}}) works today by 
accident: the elements are appended and the list has no gap ({{\[a, b\]}}). 
With the fix the list is {{\[null, a, b\]}}, as for a single key or an array, 
and code that iterates the list can hit the null. The pull request adds a note 
to the 4.23 upgrade guide.

Duplicate check (2026-09-28): JIRA text "PropertyBindingSupport" (42 issues), 
"list index", "nested list", "list binding", "property binding" with index, and 
"camel.beans" with list or array: only CAMEL-15396 (gaps, right for declared 
elements) and CAMEL-15394 (root object with lists) are related, neither is 
about the index of a nested element, the key order or "last". Open pull 
requests: #26983 (CAMEL-25083, camel-support ObjectHelper) and #26984 
(CAMEL-25084, camel-util ObjectHelper) do not change this code or 
{{addListByIndex}}. CAMEL-25009 (In Progress) changed the "#class:" parameters 
in the same file; no overlap.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to