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)