This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 8cd2c2e595 Fix BeanMap abstract collection default typing and archive 
TODO updates
8cd2c2e595 is described below

commit 8cd2c2e595ae6a1e725dc2065932e77707a34f7a
Author: James Bognar <[email protected]>
AuthorDate: Fri May 22 12:55:19 2026 -0400

    Fix BeanMap abstract collection default typing and archive TODO updates
---
 .../juneau/commons/bean/BeanPropertyMeta.java      |  30 ++-
 .../test/java/org/apache/juneau/BeanMap_Test.java  | 175 +++++++++++++++++
 ...NISHED-58-beanmap-typed-set-element-coercion.md | 115 +++++++++++
 ...-59-beanmap-abstract-collection-default-type.md |  48 +++++
 todo/TODO-58-beanmap-typed-set-element-coercion.md | 211 ---------------------
 todo/TODO.md                                       |   2 -
 6 files changed, 361 insertions(+), 220 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java
index 2b574cdd2b..74eea2c4aa 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java
@@ -1229,9 +1229,14 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                                        invokeSetter(bean, 
pName, valueList);
                                                        return r;
                                                }
-                                               throw 
bex(beanMeta.getBeanInfo(),
-                                                       "Cannot set property 
''{0}'' of type ''{1}'' to object of type ''{2}'' because the assigned map 
cannot be converted to the specified type because the property type is 
abstract, and the property value is currently null",
-                                                       name, 
propertyClass.getName(), cn(value1));
+
+                                               propList = 
createDefaultCollectionForAbstractType(propertyClass);
+                                               if (propList == null) {
+                                                       throw 
bex(beanMeta.getBeanInfo(),
+                                                               "Cannot set 
property ''{0}'' of type ''{1}'' to object of type ''{2}'' because the assigned 
map cannot be converted to the specified type because the property type is 
abstract, and the property value is currently null",
+                                                               name, 
propertyClass.getName(), cn(value1));
+                                               }
+                                               invokeSetter(bean, pName, 
propList);
                                        }
                                        propList.clear();
                                } else {
@@ -1244,12 +1249,11 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                }
 
                                // Set the values.
-                               var propList2 = propList;
-                               valueList.forEach(x -> {
+                               for (var x : valueList) {
                                        if (! elementType.isObject())
                                                x = session.convertToType(x, 
elementType);
-                                       propList2.add(x);
-                               });
+                                       propList.add(x);
+                               }
 
                        } else {
                                value1 = session.convertToMemberType(bean, 
value1, rawTypeMeta);
@@ -1366,6 +1370,18 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                return beanMeta.getClassInfo().getName();
        }
 
+       private Collection<?> createDefaultCollectionForAbstractType(Class<?> 
propertyClass) {
+               if (propertyClass == SortedSet.class || propertyClass == 
NavigableSet.class)
+                       return new TreeSet<>();
+               if (propertyClass == Set.class)
+                       return new LinkedHashSet<>();
+               if (propertyClass == Deque.class || propertyClass == 
Queue.class)
+                       return new ArrayDeque<>();
+               if (propertyClass == List.class || propertyClass == 
Collection.class)
+                       return new ArrayList<>();
+               return null;
+       }
+
        /**
         * Returns <jk>true</jk> if this property is write-only.
         *
diff --git a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
index f17395b660..09c0abdaf3 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
@@ -1832,6 +1832,181 @@ class BeanMap_Test extends TestBase {
                }
        }
 
+       
//====================================================================================================
+       // Typed collection elements in BeanMap.put should coerce to the 
property element type.
+       //
+       // Sibling of a41/a42 (typed-map key coercion).  Exercises the 
analogous gap on the typed-collection
+       // branch of BeanPropertyMeta.setPropertyValue: feeding a List/Set of 
Strings into a Set/List of
+       // EnumType should coerce each element via session.convertToType(...) 
rather than dropping it (or
+       // silently storing the raw String against the typed property).
+       
//====================================================================================================
+       @Test void a43_typedSetField_coercesStringElementsToEnum() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AD());
+               a.put("s", list("ONE","TWO"));
+
+               var b = a.getBean();
+               assertEquals(2, b.s.size());
+               assertTrue(b.s.stream().allMatch(x -> x instanceof HEnum));
+               assertTrue(b.s.contains(HEnum.ONE));
+               assertTrue(b.s.contains(HEnum.TWO));
+       }
+
+       public static class AD {
+               @BeanProp(type=TreeSet.class, params=HEnum.class)
+               public Set<HEnum> s;
+       }
+
+       @Test void a44_typedSetSetter_coercesStringElementsToEnum() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AE());
+               a.put("s", list("ONE","TWO"));
+
+               var b = a.getBean();
+               assertEquals(2, b.getS().size());
+               assertTrue(b.getS().stream().allMatch(x -> x instanceof HEnum));
+               assertTrue(b.getS().contains(HEnum.ONE));
+               assertTrue(b.getS().contains(HEnum.TWO));
+       }
+
+       public static class AE {
+               private final TreeSet<HEnum> s = new TreeSet<>();
+               public TreeSet<HEnum> getS() { return s; }
+               public void setS(TreeSet<HEnum> v) {
+                       s.clear();
+                       s.addAll(v);
+               }
+       }
+
+       @Test void a45_typedListField_coercesStringElementsToEnum() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AF());
+               a.put("l", list("ONE","TWO"));
+
+               var b = a.getBean();
+               assertList(b.l, "ONE", "TWO");
+               assertTrue(b.l.stream().allMatch(x -> x instanceof HEnum));
+       }
+
+       public static class AF {
+               public List<HEnum> l;
+       }
+
+       @Test void a46_typedSetFromSetSource_coercesStringElementsToEnum() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AD());
+               a.put("s", set("ONE","TWO"));
+
+               var b = a.getBean();
+               assertEquals(2, b.s.size());
+               assertTrue(b.s.stream().allMatch(x -> x instanceof HEnum));
+               assertTrue(b.s.contains(HEnum.ONE));
+               assertTrue(b.s.contains(HEnum.TWO));
+       }
+
+       @Test void a47_typedSetField_stringElementsKeepWorking() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AG());
+               a.put("s", list("x","y"));
+
+               var b = a.getBean();
+               assertTrue(b.s.contains("x"));
+               assertTrue(b.s.contains("y"));
+       }
+
+       public static class AG {
+               @BeanProp(type=TreeSet.class, params=String.class)
+               public Set<String> s;
+       }
+
+       @Test void 
a48_typedSetProtectedField_IRSStyle_coercesStringElementsToEnum() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AH());
+               a.put("s", List.of("ONE","TWO"));
+
+               var b = a.getBean();
+               assertEquals(2, b.s.size());
+               assertTrue(b.s.stream().allMatch(x -> x instanceof HEnum));
+               assertTrue(b.s.contains(HEnum.ONE));
+               assertTrue(b.s.contains(HEnum.TWO));
+       }
+
+       public static class AH {
+               @BeanProp(type=TreeSet.class, params=HEnum.class)
+               protected Set<HEnum> s;
+       }
+
+       @Test void 
a49_abstractSetField_noHint_usesLinkedHashSetAndCoercesElements() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AI());
+               a.put("s", List.of("ONE","TWO"));
+
+               var b = a.getBean();
+               assertTrue(b.s instanceof LinkedHashSet);
+               assertEquals(2, b.s.size());
+               assertTrue(b.s.stream().allMatch(x -> x instanceof HEnum));
+               assertEquals(list(HEnum.ONE, HEnum.TWO), new ArrayList<>(b.s));
+       }
+
+       public static class AI {
+               public Set<HEnum> s;
+       }
+
+       @Test void 
a50_abstractSortedSetField_noHint_usesTreeSetAndCoercesElements() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AJ());
+               a.put("s", List.of("TWO","ONE"));
+
+               var b = a.getBean();
+               assertTrue(b.s instanceof TreeSet);
+               assertEquals(2, b.s.size());
+               assertTrue(b.s.stream().allMatch(x -> x instanceof HEnum));
+               assertEquals(list(HEnum.ONE, HEnum.TWO), new ArrayList<>(b.s));
+       }
+
+       public static class AJ {
+               public SortedSet<HEnum> s;
+       }
+
+       @Test void 
a51_abstractQueueField_noHint_usesArrayDequeAndCoercesElements() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AK());
+               a.put("q", List.of("ONE","TWO"));
+
+               var b = a.getBean();
+               assertTrue(b.q instanceof ArrayDeque);
+               assertEquals(2, b.q.size());
+               assertTrue(b.q.stream().allMatch(x -> x instanceof HEnum));
+               assertEquals(list(HEnum.ONE, HEnum.TWO), new ArrayList<>(b.q));
+       }
+
+       public static class AK {
+               public Queue<HEnum> q;
+       }
+
+       @Test void 
a52_abstractDequeField_noHint_usesArrayDequeAndCoercesElements() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AL());
+               a.put("d", List.of("ONE","TWO"));
+
+               var b = a.getBean();
+               assertTrue(b.d instanceof ArrayDeque);
+               assertEquals(2, b.d.size());
+               assertTrue(b.d.stream().allMatch(x -> x instanceof HEnum));
+               assertEquals(list(HEnum.ONE, HEnum.TWO), new ArrayList<>(b.d));
+       }
+
+       public static class AL {
+               public Deque<HEnum> d;
+       }
+
+       @Test void 
a53_abstractSetSetter_noHint_usesLinkedHashSetAndCoercesElements() {
+               var a = MarshallingContext.DEFAULT.toBeanMap(new AM());
+               a.put("s", List.of("ONE","TWO"));
+
+               var b = a.getBean();
+               assertTrue(b.getS() instanceof LinkedHashSet);
+               assertEquals(2, b.getS().size());
+               assertTrue(b.getS().stream().allMatch(x -> x instanceof HEnum));
+               assertEquals(list(HEnum.ONE, HEnum.TWO), new 
ArrayList<>(b.getS()));
+       }
+
+       public static class AM {
+               private Set<HEnum> s;
+               public Set<HEnum> getS() { return s; }
+               public void setS(Set<HEnum> v) { s = v; }
+       }
+
        
//====================================================================================================
        // containsKey with plain beans vs @MarshalledProp(name="*") 
dyna/extras map
        
//====================================================================================================
diff --git a/todo/FINISHED-58-beanmap-typed-set-element-coercion.md 
b/todo/FINISHED-58-beanmap-typed-set-element-coercion.md
new file mode 100644
index 0000000000..f88961815f
--- /dev/null
+++ b/todo/FINISHED-58-beanmap-typed-set-element-coercion.md
@@ -0,0 +1,115 @@
+# FINISHED-58 — Harden `BeanPropertyMeta` typed-collection element write path
+
+Completed: 2026-05-22
+
+## Outcome
+
+TODO-58 is complete. The typed-collection write path in 
`BeanPropertyMeta.setPropertyValue(...)` now uses an explicit-iteration pattern 
that mirrors the just-merged typed-map fix (TODO-14, commit `affabe50f3`). New 
focused regression tests in `BeanMap_Test` cover the typed-`Set<EnumType>` / 
typed-`List<EnumType>` write shapes — including the exact 
`@BeanProp(type=TreeSet.class, params=EnumType.class) protected Set<EnumType>` 
shape that the IRS `Suspension` bean uses in `central-routing/i [...]
+
+## Important finding — the original symptom did NOT reproduce
+
+The plan was filed on the hypothesis that `BeanMap.put` was *silently 
dropping* elements when feeding `List<String>` into `Set<EnumType>` against an 
`@BeanProp(type=TreeSet.class, params=EnumType.class)` field. We could not 
reproduce that symptom on the current `master` (post `affabe50f3`).
+
+Direct instrumentation of the IRS-style shape (`protected Set<HEnum> s` 
annotated `@BeanProp(type=TreeSet.class, params=HEnum.class)`) showed:
+
+- `rawTypeMeta` resolved to `TreeSet<HEnum>` (concrete, 
`canCreateNewInstance() == true`).
+- `rawTypeMeta.getElementType()` resolved to `HEnum` (NOT `Object`).
+- `BeanMap.put("s", List.of("ONE","TWO"))` produced a `TreeSet` of size 2 
containing the **converted** `HEnum.ONE` and `HEnum.TWO` enum values — not 
strings.
+
+So whatever the IRS team observed on their build, the silent-element-drop is 
not present in the current commons-side write path. Possible explanations:
+
+1. Their Juneau snapshot predates a fix already shipped on `master`.
+2. Their `Suspension` bean has a shape detail that didn't carry over into the 
reproducer (we tried both `public` and `protected` fields and the 
`List.of`/`list(...)` source flavor).
+3. Their workaround was speculative and the real failure was elsewhere in the 
change-ledger pipeline (e.g. `ChangeableDaoBean.setProperty`'s 
exception-swallowing wrapper).
+
+Regardless, the regression tests added under this work item lock in the 
correct behavior so it can't silently regress later.
+
+## Implemented changes
+
+### 1) Commons-side production change
+
+File:
+- 
`juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java`
+
+Change (concrete-collection write branch of `setPropertyValue(...)`):
+- Replaced the `valueList.forEach(x -> { ... propList2.add(x); })` lambda 
write path with an explicit `for (var x : valueList) { ... propList.add(x); }` 
loop.
+- Removed the now-unnecessary `propList2` capture local (the explicit loop can 
mutate `propList` directly without needing an effectively-final local for the 
lambda).
+- Element conversion semantics are **unchanged** (still 
`session.convertToType(x, elementType)` when `! elementType.isObject()`).
+
+Why:
+- Symmetry with the TODO-14 fix on the typed-map branch (commit `affabe50f3`), 
which also moved from `valueMap.forEach((k,v) -> ...)` to an explicit `for (var 
e : valueMap.entrySet()) { ... }` loop for the same dispatch site.
+- Easier to extend in future (e.g. to add per-element `nn(x) && ! 
elementType.isInstance(x)` short-circuit guards, or to add side-effects between 
the convert and the `add`) without re-juggling lambda captures.
+- More readable: lambda parameter reassignment for the convert-then-add 
pattern is subtle.
+
+The abstract-collection branch (lines around 1218-1230) was already using 
explicit `listIterator()` iteration with reassignment, so no change was needed 
there.
+
+### 2) Regression / coverage test additions
+
+File:
+- `juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java`
+
+Added tests:
+- `a43_typedSetField_coercesStringElementsToEnum` — 
`@BeanProp(type=TreeSet.class, params=HEnum.class) public Set<HEnum>` field; 
`BeanMap.put("s", list("ONE","TWO"))`; asserts size, `instanceof HEnum`, and 
enum contains.
+- `a44_typedSetSetter_coercesStringElementsToEnum` — getter/setter-backed 
`TreeSet<HEnum>` property; same input shape and assertions via `getS()`.
+- `a45_typedListField_coercesStringElementsToEnum` — `public List<HEnum>` 
field (no `@BeanProp`); exercises the abstract-collection branch with element 
conversion.
+- `a46_typedSetFromSetSource_coercesStringElementsToEnum` — same as `a43` but 
with a `Set<String>` source instead of `List<String>`, to confirm the 
concrete-collection branch handles non-`List` source collections.
+- `a47_typedSetField_stringElementsKeepWorking` — 
`@BeanProp(type=TreeSet.class, params=String.class)` field with `List<String>` 
input; confirms the no-conversion-needed case still works.
+- `a48_typedSetProtectedField_IRSStyle_coercesStringElementsToEnum` — 
`@BeanProp(type=TreeSet.class, params=HEnum.class) protected Set<HEnum>` field 
(exact IRS `Suspension.types` shape) with `List.of(...)` source.
+
+All six tests pass on `master` *before* this change, and continue to pass 
*after* this change — they are pure regression coverage.
+
+## Verification run
+
+### Focused area
+- `mvn -pl juneau-utest -am -Dtest=BeanMap_Test 
-Dsurefire.failIfNoSpecifiedTests=false test`
+  - `org.apache.juneau.BeanMap_Test`: **52 run, 0 failures, 0 errors, 0 
skipped**
+  - `org.apache.juneau.transforms.BeanMap_Test`: **2 run, 0 failures, 0 
errors, 0 skipped**
+
+### Parser regression suite (sibling-shape parser families)
+- `mvn -pl juneau-utest -am 
-Dtest='Hjson*Test,Hocon*Test,Proto*Test,Bson*Test' 
-Dsurefire.failIfNoSpecifiedTests=false test`
+  - **341 run, 0 failures, 0 errors, 2 skipped** (pre-existing skips).
+
+### Enum matrix acceptance check
+- `mvn -pl juneau-utest -am -Dtest=EnumFormat_RoundTrip_Test 
-Dsurefire.failIfNoSpecifiedTests=false test`
+  - `EnumFormat_RoundTrip_Test`: **2268 run, 0 failures, 0 errors, 0 skipped**
+
+### Broader suite
+- `./scripts/test.py`
+  - Build phase: success
+  - Test phase: success
+
+## Open questions — answered
+
+1. **What does `rawTypeMeta.getElementType()` actually return for 
`@BeanProp(type=TreeSet.class, params=HEnum.class) Set<HEnum> s`?**
+   - Returns `ClassMeta<HEnum>` (`isObject() == false`). `rawTypeMeta` itself 
resolves to `TreeSet<HEnum>` with `inner() == TreeSet.class` and 
`canCreateNewInstance() == true`. So the conversion plumbing has everything it 
needs.
+2. **Does the bug reproduce without the `@BeanProp` annotation?**
+   - The "silent drop" symptom does not reproduce **at all** on current 
`master` — neither with `@BeanProp` nor without. However: a plain `public 
Set<HEnum> s;` field (abstract `Set`, no setter, no `@BeanProp`) **throws** 
`BeanRuntimeException("Cannot set property 's' of type 'java.util.Set' to 
object of type 'java.util.ArrayList' because the assigned map cannot be 
converted to the specified type because the property type is abstract, and the 
property value is currently null")` when fed  [...]
+3. **Does the symmetric `List<EnumType>` shape reproduce?**
+   - No — `public List<HEnum> l;` correctly coerces `List<String>` input via 
the abstract-collection branch's existing per-element `listIterator()` 
conversion path. Test `a45` covers this.
+4. **Is the silent-drop actually silent, or is there a warning being 
swallowed?**
+   - Moot — the silent-drop did not reproduce. No exception is being swallowed 
by `ignoreInvocationExceptionsOnSetters` on the tested shapes. (The IRS 
`ChangeableDaoBean.setProperty` wrapper does have its own `try/catch 
(RuntimeException)`; if their actual symptom *was* a throw, that wrapper would 
swallow it and present as a silent drop one layer up. Worth flagging back to 
the IRS team.)
+
+## Follow-up audit summary (sibling-shape pass)
+
+Reviewed:
+- `BeanPropertyMeta.setPropertyValue(...)` Collection branches (both abstract 
and concrete).
+- Element-type resolution for `@BeanProp(type=..., params=...)` field/setter 
combinations.
+
+Findings:
+- No additional `Collection<E>` element-side coercion gaps in the typed write 
path itself.
+- The concrete-collection branch's `forEach` write loop was correct but 
stylistically inconsistent with the just-fixed Map branch — harmonized in this 
work.
+- **Latent bug found, NOT fixed under this work item:** An abstract 
`Set<EnumType>` field with **no** setter and **no** `@BeanProp(type=...)` (e.g. 
plain `public Set<HEnum> s;`) cannot be populated from a `List<String>` source. 
The abstract-collection branch's guard `propertyClass.isInstance(valueList) || 
(nn(setter) && setter.getParameterTypes().get(0).is(Collection.class))` 
short-circuits to a `throw` because `Set.isInstance(ArrayList) == false` and 
there is no setter. This isn't the s [...]
+
+Disposition:
+- No new TODO was filed from this audit pass for TODO-58 scope.
+- The latent abstract-Set-no-setter-no-`@BeanProp` issue is documented above 
for whoever runs into it next.
+
+## Phase 4 — downstream cleanup notification (REMAINING)
+
+This phase is left for the parent agent / human to action because it requires 
reaching out to the IRS team and verifying their snapshot of Juneau:
+
+1. **Verify** that the IRS `Suspension.java#parse(...)` override (lines 
330-349 of PR #1806) was actually addressing a real Juneau bug at the snapshot 
they were testing against. The symptom does not reproduce on current `master`, 
so either:
+   - Their Juneau snapshot was older than `affabe50f3`'s base, or
+   - Their real failure was the exception-swallowing wrapper in 
`ChangeableDaoBean.setProperty`, not the `BeanMap.put` conversion path.
+2. **If verified redundant**, notify `#central-routing-irs` (or the IRS team's 
preferred channel) that the per-bean `parse()` override can be removed when 
they next pick up Juneau.
+3. **Otherwise**, ask the IRS team to share a minimal reproducer against the 
exact Juneau version where they saw the failure, so we can decide whether to 
backport or chase the actual cause.
diff --git a/todo/FINISHED-59-beanmap-abstract-collection-default-type.md 
b/todo/FINISHED-59-beanmap-abstract-collection-default-type.md
new file mode 100644
index 0000000000..fe4af0055a
--- /dev/null
+++ b/todo/FINISHED-59-beanmap-abstract-collection-default-type.md
@@ -0,0 +1,48 @@
+# FINISHED-59 — Default concrete types for abstract collection `BeanMap.put` 
writes
+
+Completed: 2026-05-22
+
+## Outcome
+
+TODO-59 is complete. `BeanPropertyMeta.setPropertyValue(...)` now assigns 
sensible default concrete collections when writing to abstract, field-only or 
setter-backed collection properties with null current value and no 
`@BeanProp(type=...)` hint, instead of throwing.
+
+## Implemented changes
+
+### 1) Commons-side production fix
+
+File:
+- 
`juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java`
+
+Change:
+- In the abstract-collection branch of `setPropertyValue(...)`, when `propList 
== null` and direct assignment cannot be used, the code now creates a default 
collection and assigns it instead of immediately throwing.
+- Added `createDefaultCollectionForAbstractType(Class<?>)` with explicit 
mappings:
+  - `Set` -> `LinkedHashSet`
+  - `SortedSet` / `NavigableSet` -> `TreeSet`
+  - `Queue` / `Deque` -> `ArrayDeque`
+  - `List` / `Collection` -> `ArrayList`
+- Existing behavior remains unchanged for already-working direct-assignment 
paths (e.g. `List`/`Collection` values that are already assignable).
+
+### 2) Regression test coverage
+
+File:
+- `juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java`
+
+Added tests:
+- `a49_abstractSetField_noHint_usesLinkedHashSetAndCoercesElements`
+- `a50_abstractSortedSetField_noHint_usesTreeSetAndCoercesElements`
+- `a51_abstractQueueField_noHint_usesArrayDequeAndCoercesElements`
+- `a52_abstractDequeField_noHint_usesArrayDequeAndCoercesElements`
+- `a53_abstractSetSetter_noHint_usesLinkedHashSetAndCoercesElements`
+
+These tests verify element coercion to `HEnum` and concrete type selection for 
field-only and setter-backed abstract collection shapes.
+
+## Verification run
+
+- `mvn -pl juneau-utest -am -Dtest=BeanMap_Test 
-Dsurefire.failIfNoSpecifiedTests=false test` -> **PASS**
+- `mvn -pl juneau-utest -am 
-Dtest='Hjson*Test,Hocon*Test,Proto*Test,Bson*Test' 
-Dsurefire.failIfNoSpecifiedTests=false test` -> **PASS**
+- `mvn -pl juneau-utest -am -Dtest=EnumFormat_RoundTrip_Test 
-Dsurefire.failIfNoSpecifiedTests=false test` -> **PASS**
+- `./scripts/test.py` -> **PASS** (build + test)
+
+## Scope notes
+
+- Abstract-map default-concrete selection was intentionally left out-of-scope 
per TODO-59 boundaries; no map behavior changes were made here.
diff --git a/todo/TODO-58-beanmap-typed-set-element-coercion.md 
b/todo/TODO-58-beanmap-typed-set-element-coercion.md
deleted file mode 100644
index 19ad96e11c..0000000000
--- a/todo/TODO-58-beanmap-typed-set-element-coercion.md
+++ /dev/null
@@ -1,211 +0,0 @@
-# TODO-58 — Fix silent element-drop when `BeanMap.put` converts `List<String>` 
→ `Set<EnumType>`
-
-Source: filed 2026-05-22 after the IRS team (`central-routing/irs` PR 
[#1806](https://git.soma.salesforce.com/central-routing/irs/pull/1806)) was 
forced to add a per-bean `parse()` override in `Suspension.java` to work around 
a silent element-drop in Juneau's `BeanMap.put` conversion path. The override 
exists purely to bypass Juneau and parse the CDL string into a typed 
`TreeSet<SuspensionType>` up front. Without it, the bean's 
`Set<SuspensionType>` property ends up empty even though the [...]
-
----
-
-## 1. Background / context
-
-### The symptom (verbatim from the downstream workaround)
-
-The IRS `ChangeableDaoBean` framework feeds property updates from a "Change" 
ledger into Juneau beans via:
-
-```154:167:/Users/james.bognar/git/central-routing/irs/irs-server/src/main/java/com/sfdc/irs/dao/ChangeableDaoBean.java
-       protected void setProperty(String property, String value) {
-               try {
-                       var bm = beanMap(this);
-                       var type = 
ofNullable(bm.getPropertyMeta(property)).orElseThrow(()->new 
RuntimeException("Property "+property+" not defined on class 
"+getClass().getSimpleName())).getClassMeta();
-                       bm.put(property, parse(type, property, value));
-               } catch (RuntimeException e) {
-```
-
-The base `parse(ClassMeta, String, String)` returns a generic `List<String>` 
for any Collection-typed property:
-
-```232:235:/Users/james.bognar/git/central-routing/irs/irs-server/src/main/java/com/sfdc/irs/dao/ChangeableDaoBean.java
-       protected <T> Object parse(ClassMeta<T> c, String property, String val) 
{  // NOSONAR
-               if (c.isAssignableFrom(Collection.class)) { return 
cdlToList(val); }
-               return val;
-       }
-```
-
-PR #1806 introduced a new `Suspension` field:
-
-```java
-@Beanp(type=TreeSet.class, params=SuspensionType.class)
-protected Set<SuspensionType> types;
-```
-
-…and discovered that `bm.put("types", List.of("SBX_DEV","SBX_DEVPRO"))` 
produced an **empty** `TreeSet<SuspensionType>` rather than `{SBX_DEV, 
SBX_DEVPRO}`. The wire-form `String` tokens were silently discarded instead of 
being converted to `SuspensionType` enum instances. The override they added 
(the `parse` method at lines 330-349 of `Suspension.java`) sidesteps Juneau 
entirely for this one property by producing the already-typed 
`TreeSet<SuspensionType>` so `BeanMap.put` has no elemen [...]
-
-Comment from the workaround:
-
-> Juneau's generic `List<String>` → `Set<SuspensionType>` conversion silently 
drops elements for this property, so we parse the CDL string into a typed 
`TreeSet<SuspensionType>` up front. Other properties fall through to the base 
implementation.
-
-### Why the same shape already works for other IRS properties
-
-`Suspension` has several other collection-typed properties (`instances`, 
`allowList`) that round-trip through the same base `parse()` → `cdlToList()` → 
`BeanMap.put()` path without dropping elements. Those properties are 
`SortedSet<String>` / `InstanceNameSet`-style — i.e. the element type is 
`String` (or a `String`-coercible wrapper). The element drop only manifests 
when the target element type is an **enum** (or, conjecturally, any type that 
requires `String → T` conversion via `conver [...]
-
-### Presumed fix site (not yet verified)
-
-The most likely fix site is the `Collection` branch of 
`BeanPropertyMeta.setPropertyValue` in 
`juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java`.
 The branch looks correct on the surface:
-
-```1190:1246:juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java
-                       } else if (isCollection && (setter == null || ! 
pcInfo.isAssignableFrom(vc))) {
-
-                               if (! (value1 instanceof Collection)) {
-                                       if (value1 instanceof CharSequence 
value2)
-                                               value1 = 
session.parseToList(value2);
-                                       else
-                                               throw 
bex(beanMeta.getBeanInfo(), "Cannot set property ''{0}'' of type ''{1}'' to 
object of type ''{2}''", name, propertyClass.getName(), cn(value1));
-                               }
-
-                               var valueList = (Collection)value1;
-                               var propList = (Collection)r;
-                               var elementType = rawTypeMeta.getElementType();
-
-                               // If the property type is abstract, then we 
either need to reuse the existing
-                               // collection (if it's not null), or try to 
assign the value directly.
-                               if (! rawTypeMeta.canCreateNewInstance()) {
-                                       if (propList == null) {
-                                               if (setter == null && field == 
null)
-                                                       throw 
bex(beanMeta.getBeanInfo(),
-                                                               "Cannot set 
property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter or 
public field is defined, and the current value is null", name,
-                                                               
propertyClass.getName(), cn(value1));
-
-                                               if 
(propertyClass.isInstance(valueList) || (nn(setter) && 
setter.getParameterTypes().get(0).is(Collection.class))) {
-                                                       if (! 
elementType.isObject()) {
-                                                               var l = new 
ArrayList<>(valueList);
-                                                               for (var i = 
l.listIterator(); i.hasNext();) {
-                                                                       var v = 
i.next();
-                                                                       var 
needsConversion = v == null ? elementType.isOptional() : ! 
elementType.isInstance(v);
-                                                                       if 
(needsConversion)
-                                                                               
i.set(session.convertToType(v, elementType));
-                                                               }
-                                                               valueList = l;
-                                                       }
-                                                       invokeSetter(bean, 
pName, valueList);
-                                                       return r;
-                                               }
-```
-
-`elementType` is supposed to be `ClassMeta<SuspensionType>` and 
`convertToType` should handle `String → Enum` via `Enum.valueOf`. Two leading 
hypotheses for why elements actually get dropped:
-
-1. **`elementType.isObject()` returns true** because 
`rawTypeMeta.getElementType()` doesn't honor `@Beanp(params=…)` for the 
abstract-`Set`-with-concrete-`type` shape. If `elementType` is `Object`, the 
entire per-element conversion block at lines 1213-1221 is skipped, and 
`List<String>` is handed straight to the setter as a literal `List<String>` — 
which then fails to be assigned into `Set<SuspensionType>` (or is silently 
dropped at a higher level).
-2. **`BeanMap.put` routes through a different branch** entirely for 
`@Beanp(type=TreeSet.class)` on an abstract `Set` field — e.g. the `setter == 
null` check at line 1190 fires differently when `@Beanp` declares a concrete 
`type` that overrides the field's declared type, and the value gets handed to 
`convertToMemberType` (line 1249) instead. `convertToMemberType` may then take 
a `List<String> → Set<Enum>` shortcut path that doesn't actually iterate 
elements.
-
-Phase 1's first job is to reproduce the gap in a standalone unit test and tag 
the precise branch.
-
----
-
-## 2. Scope
-
-### In scope
-
-- Reproduce the symptom in a standalone `juneau-commons` (or `juneau-utest`) 
unit test that has no IRS dependency: bean with `@Beanp(type=TreeSet.class, 
params=EnumType.class) Set<EnumType> prop;`, `BeanMap.put("prop", 
List.of("FOO","BAR"))`, assert the bean's getter returns `{FOO, BAR}` (not an 
empty set).
-- Identify the actual fix site in `BeanPropertyMeta` (most likely the 
`Collection` branch around lines 1190-1246) and patch it to convert elements 
via `session.convertToType(x, elementType)` when the source-list element type 
doesn't match the target-set element type.
-- Confirm the fix doesn't regress the `List<String>` → `Set<String>` shape 
(the common case that already works), the `List<String>` → `List<EnumType>` 
shape, or the JSON family's parser-level conversion path.
-- Verify the IRS workaround in `Suspension.java#parse(...)` (lines 330-349 of 
PR #1806) becomes unnecessary, then notify the IRS team so they can remove it 
once they pick up the next Juneau release.
-
-### Out of scope
-
-- The IRS-side workaround itself — leave it in place until the Juneau fix 
ships. Removing it is a downstream cleanup, not part of this plan.
-- The `Map<K, V>` key-coercion gap tracked under 
`todo/TODO-14-beanpropertymeta-map-key-coercion.md`. Same file, related shape, 
but a different branch and a different missing call.
-- Generic-arity changes to `@Beanp` — fix is limited to honoring the existing 
`type` / `params` declarations correctly during the `List<String>` → 
`Set<EnumType>` path.
-- Per-format parser workarounds — this gap surfaces in **direct 
`BeanMap.put`** usage (no parser involved), so the fix has to be at the 
commons-side bean-property assignment site, not at a parser dispatch site.
-
----
-
-## 3. Phases
-
-### Phase 1 — Reproduce the symptom in a standalone test
-
-Land a new unit test under `juneau-utest` that:
-
-1. Defines a small bean with a `Set<TestEnum>` property annotated 
`@Beanp(type=TreeSet.class, params=TestEnum.class)` — matching the exact 
`Suspension.java` shape.
-2. Builds a `BeanMap` for the bean from a default `BeanSession`.
-3. Calls `BeanMap.put("types", List.of("FOO","BAR"))` — i.e. hands a 
`List<String>` directly to the bean property, bypassing every parser.
-4. Asserts that the bean's `Set<TestEnum>` getter returns `{TestEnum.FOO, 
TestEnum.BAR}`.
-
-The test must **fail** on the current commons-side code (proving the gap is 
real and matches the IRS symptom) and **pass** after the Phase 2 fix. Use a 
`TestEnum` defined in the test's own scope to avoid coupling the unit test to 
any real domain enum.
-
-Add three companion assertion-only variants in the same test file to nail down 
the precise shape:
-
-- `List<String>` → `Set<String>` (no enum conversion needed) — must pass 
today, must keep passing.
-- `List<String>` → `List<TestEnum>` (target is a `List`, not a `Set`) — 
exercises the same Collection branch with a different concrete type.
-- `Set<String>` → `Set<TestEnum>` (source is already a Set) — confirms the bug 
isn't specific to `List` source.
-
-Use a debugger or targeted `System.err.println` (removed before commit) to 
capture which branch of `setPropertyValue` actually fires and what 
`elementType` / `rawTypeMeta.getElementType()` resolve to in the failing case. 
Append the answer to the Open Questions section.
-
-### Phase 2 — Implement the commons-side fix
-
-Once Phase 1 has pinned the branch and the failing predicate, the fix is one 
of:
-
-- **If `elementType.isObject()` is firing on a properly-annotated 
`@Beanp(params=…)` Set field**, the fix is in `ClassMeta` (or in whatever 
resolves `rawTypeMeta.getElementType()` for `@Beanp`-overridden types) to honor 
the `params[]` declaration when the declared field type is parameterized but 
the `@Beanp(type=…)` overrides to a concrete type.
-- **If the value-conversion branch is being skipped for a different reason** 
(e.g. the `(propertyClass.isInstance(valueList) || 
setter.getParameterTypes().get(0).is(Collection.class))` guard at line 1212 is 
short-circuiting and falling through to a `convertToMemberType` shortcut at 
line 1249), the fix is to extend that branch to also run the per-element 
coercion before invoking the setter.
-- **If `BeanMap.put` is routing through a completely different code path** for 
this shape, the fix is at that path, not in `setPropertyValue`. Phase 1's 
debugger pass tells us which.
-
-Either way: no new `ClassMeta` API, no `BeanSession` change, no signature 
change on `setPropertyValue`. The conversion plumbing (`session.convertToType`, 
`elementType.isInstance`, `elementType.isOptional`) is already wired through.
-
-### Phase 3 — Regression check across collection shapes
-
-Run targeted tests against the existing collection-property coverage to 
confirm the fix doesn't regress already-working shapes:
-
-```bash
-mvn -pl juneau-utest -am 
-Dtest='*BeanMap*Test,*BeanPropertyMeta*Test,*ClassMeta*Test' test
-```
-
-Then a full sweep:
-
-```bash
-./scripts/test.py
-```
-
-The full `EnumFormat_RoundTrip_Test` matrix tracked under TODO-57 must stay at 
its current pass rate (the matrix exercises `Set<Enum>` and `List<Enum>` shapes 
through every parser; a regression in the commons-side fix would show up there 
immediately).
-
-### Phase 4 — Downstream cleanup notification
-
-Once the fix ships in a Juneau release:
-
-1. Confirm the IRS `Suspension.java#parse(...)` override (lines 330-349 of PR 
#1806) becomes a no-op against the new Juneau version — i.e. the override's 
behavior matches what `BeanMap.put` now does on its own. Run the IRS test added 
for that override (`SuspensionServiceTest`, `SuspensionTest`, or whichever test 
covers the change-ledger round trip) against a snapshot Juneau build with this 
fix.
-2. Notify the IRS team (Slack `#central-routing-irs` or the equivalent) that 
the override can be removed.
-3. The IRS team owns the actual removal — it's not part of this plan.
-
----
-
-## 4. Open questions
-
-1. **What does `rawTypeMeta.getElementType()` actually return for 
`@Beanp(type=TreeSet.class, params=SuspensionType.class) Set<SuspensionType> 
types`?** Phase 1 answers this. If it returns `ClassMeta<Object>`, the bug is 
upstream of `setPropertyValue` (in `ClassMeta` / `BeanPropertyMeta` setup). If 
it returns `ClassMeta<SuspensionType>`, the bug is downstream (in the 
Collection branch's conversion call).
-2. **Does the bug reproduce without the `@Beanp` annotation?** i.e. plain 
`Set<SuspensionType> types;` with a setter `setTypes(Set<SuspensionType>)`. If 
yes, the issue is purely with generic type resolution on `Set<EnumType>` fields 
and has nothing to do with `@Beanp`. If no, the issue is specific to how 
`@Beanp(type=…, params=…)` is consumed during property assignment.
-3. **Does the symmetric `List<EnumType>` shape (List instead of Set) 
reproduce?** Phase 1 covers this. If `List<EnumType>` works but `Set<EnumType>` 
doesn't, the bug is in the abstract-collection branch (`! 
rawTypeMeta.canCreateNewInstance()` at line 1205) that handles `Set` 
differently from `List`.
-4. **Is the silent-drop actually silent, or is there a warning being 
swallowed?** `BeanPropertyMeta.setPropertyValue` has an 
`ignoreInvocationExceptionsOnSetters` flag (line 1258). If the conversion is 
actually throwing and being swallowed by that flag, the fix is partly to 
disable the flag for this path, or to log the swallowed exception at debug 
level.
-5. **Relationship to TODO-14.** TODO-14 is about `Map<K, V>` key coercion in 
the same file; TODO-57 surfaced the same underlying philosophy ("inspect entry 
values but not entry keys"). This TODO-58 is about `Set<E>` / `List<E>` element 
coercion where the source element type doesn't match the target element type. 
The three plans probably share a common root cause (generic-type-aware element 
inspection during `BeanMap.put`); worth checking after Phase 1 whether one 
unified fix closes all t [...]
-
----
-
-## 5. Acceptance criteria
-
-- New `juneau-utest` test (Phase 1) reproduces the symptom on pre-fix code, 
passes on post-fix code.
-- All four assertion-only variants from Phase 1 (`List<String>→Set<String>`, 
`List<String>→List<TestEnum>`, `Set<String>→Set<TestEnum>`, plus the headline 
`List<String>→Set<TestEnum>`) pass on post-fix code.
-- `./scripts/test.py` clean across the rest of the suite.
-- `EnumFormat_RoundTrip_Test` matrix (tracked under TODO-57) stays at its 
current pass rate — no parser-level regression.
-- Open Questions 1-4 above are answered in writing and appended to this plan.
-- IRS `Suspension.java#parse(...)` override (PR #1806 lines 330-349) verified 
redundant against the fixed Juneau build, and the IRS team notified.
-
----
-
-## 6. Out of scope
-
-- The IRS-side `Suspension.java#parse(...)` override removal — owned by the 
IRS team, downstream of the Juneau release.
-- The `Map<K, V>` key-coercion gap — see 
`todo/TODO-14-beanpropertymeta-map-key-coercion.md`.
-- Sibling shapes (`Iterable<E>`, generic-typed arrays `T[]`) — flagged under 
Open Question 5 in TODO-14, not part of this plan's initial fix.
-- Performance optimization of `convertToType` itself — orthogonal.
-- `@Beanp(params=…)` semantics for non-Collection / non-Map shapes — out of 
scope.
-
----
-
-## 7. Related plans / references
-
-- **IRS PR #1806** — 
[git.soma.salesforce.com/central-routing/irs/pull/1806](https://git.soma.salesforce.com/central-routing/irs/pull/1806).
 The `Suspension.java#parse(...)` override at lines 330-349 of the new file is 
the downstream workaround this plan exists to retire.
-- **`todo/TODO-14-beanpropertymeta-map-key-coercion.md`** — sibling plan for 
the `Map<K, V>` key-coercion gap in the same file. Same philosophy ("inspect 
one side, not the other"), different branch.
-- **`todo/TODO-57-format-round-trip-tests.md`** — the round-trip test matrix 
that surfaced the `Map<K, V>` gap (Bug #7b) and would surface this `Set<E>` / 
`List<E>` gap if it manifested through any parser. The fact that it doesn't 
surface there confirms this is a `BeanMap.put`-direct-usage bug, not a parser 
bug.
-- **The `BeanPropertyMeta.setPropertyValue` Collection branch** — 
`juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanPropertyMeta.java`
 lines 1190-1246. Most likely fix site, pending Phase 1 confirmation.
diff --git a/todo/TODO.md b/todo/TODO.md
index 1afe645949..2c3b4cad94 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -9,5 +9,3 @@
 
 - [TODO-30] Investigate moving `ClassMeta` and related non-marshalling type 
metadata from `juneau-marshall` into `juneau-commons` (analysis/feasibility 
pass). See `todo/TODO-30-classmeta-to-commons.md`.
 
-- [TODO-58] Fix silent element-drop when `BeanMap.put` converts `List<String>` 
→ `Set<EnumType>` against an `@Beanp(type=TreeSet.class, 
params=EnumType.class)`-annotated property. Surfaced downstream in 
`central-routing/irs` PR #1806, which had to add a per-bean `parse()` override 
to work around it. See `todo/TODO-58-beanmap-typed-set-element-coercion.md`.
-


Reply via email to