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`.
-