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 a34c40a1a3 TODO-291: Fix @ParentProperty injection through
collections/maps
a34c40a1a3 is described below
commit a34c40a1a3832d62a455cfaf8fab1daae994c201
Author: James Bognar <[email protected]>
AuthorDate: Fri Jul 24 09:08:15 2026 -0400
TODO-291: Fix @ParentProperty injection through collections/maps
A collection/map element's @ParentProperty now receives the nearest
enclosing
bean (skipping intermediate containers at any depth; null at root) instead
of
the containing List/Map. Separates the parser's dual-purpose 'outer' param
via a
new ParserSession.parentBean() tracker, applied across all 9 streaming
parsers
plus HOCON/HJSON reconciliation. Makes the canonical
AddressBook/List<Person>
example round-trip and unifies behavior across all formats.
Behavior change for 10.0.0 (see release notes). New
ParentPropertyContainer_Test
(72 methods); full reactor gate green (122,889 tests, 0 failures).
Co-authored-by: Cursor <[email protected]>
---
.../org/apache/juneau/marshall/ParentProperty.java | 24 ++-
.../juneau/marshall/bson/BsonParserSession.java | 51 ++---
.../juneau/marshall/cbor/CborParserSession.java | 43 ++--
.../juneau/marshall/hjson/HjsonParserSession.java | 50 ++++-
.../juneau/marshall/hocon/HoconParserSession.java | 50 ++++-
.../juneau/marshall/html/HtmlParserSession.java | 108 +++++-----
.../juneau/marshall/json/JsonParserSession.java | 6 +-
.../marshall/msgpack/MsgPackParserSession.java | 43 ++--
.../juneau/marshall/parser/ParserSession.java | 33 +++
.../juneau/marshall/uon/UonParserSession.java | 6 +-
.../urlencoding/UrlEncodingParserSession.java | 6 +-
.../juneau/marshall/xml/XmlParserSession.java | 22 +-
.../juneau/marshall/yaml/YamlParserSession.java | 13 +-
.../juneau/ParentPropertyContainer_Test.java | 225 +++++++++++++++++++++
14 files changed, 544 insertions(+), 136 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
index 67a620f775..db253d8591 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
@@ -52,7 +52,7 @@ import java.lang.annotation.*;
*
* <jk>public class</jk> Person {
* <ja>@ParentProperty</ja>
- * <jk>public</jk> AddressBook addressBook; <jc>// Automatically
set to containing AddressBook</jc>
+ * <jk>public</jk> AddressBook addressBook; <jc>// Automatically
set to the enclosing AddressBook (the intervening List is skipped).</jc>
*
* <jk>public</jk> String name;
* <jk>public</jk> <jk>char</jk> sex;
@@ -79,6 +79,17 @@ import java.lang.annotation.*;
* <li>This allows child objects to navigate back to their parent if needed
* </ul>
*
+ * <h5 class='section'>Parent resolution through collections and maps:</h5>
+ * <ul class='spaced-list'>
+ * <li>A collection/map element's <ja>@ParentProperty</ja> is set to the
nearest enclosing bean, skipping any
+ * intervening collections, maps, or arrays. In the example
above, each <c>Person</c> lives inside a
+ * <c>List<Person></c>, yet its <c>addressBook</c>
back-reference receives the enclosing <c>AddressBook</c>
+ * bean, not the <c>List</c>. The same holds through arbitrarily
deep nesting
+ * (e.g. <c>List<List<Person>></c>,
<c>Map<String,List<Person>></c>).
+ * <li>A bean at the document root (or directly inside a top-level
collection) has no enclosing bean, so its
+ * <ja>@ParentProperty</ja> is left <jk>null</jk>.
+ * </ul>
+ *
* <h5 class='section'>Cyclic graphs and serialization:</h5>
* <ul class='spaced-list'>
* <li>When a <ja>@ParentProperty</ja> back-reference is also a
normally-visible bean property (e.g. a <jk>public</jk>
@@ -98,6 +109,17 @@ import java.lang.annotation.*;
* <jk>null</jk>, and parsing re-injects the parent via this
annotation.
* </ul>
*
+ * <p>
+ * Putting it together, the <c>AddressBook</c>/<c>List<Person></c> graph
above round-trips cleanly when the
+ * back-reference is omitted on serialize and re-injected on parse:
+ * <p class='bjava'>
+ * Serializer <jv>serializer</jv> =
JsonSerializer.<jsm>create</jsm>().ignoreRecursions().build();
+ * String <jv>json</jv> =
<jv>serializer</jv>.serialize(<jv>addressBook</jv>);
+ * AddressBook <jv>parsed</jv> =
JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>json</jv>, AddressBook.<jk>class</jk>);
+ * Person <jv>person</jv> = <jv>parsed</jv>.people.get(0);
+ * <jsm>assertTrue</jsm>(<jv>person</jv>.addressBook == <jv>parsed</jv>);
<jc>// Parent re-injected through the List.</jc>
+ * </p>
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/ParentPropertyAnnotation">@ParentProperty
Annotation</a>
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
index 74d3dfb668..0f0c589424 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
@@ -125,8 +125,8 @@ public class BsonParserSession extends
InputStreamParserSession implements Recor
yield null;
}
};
- if (nn(outer) && nn(o))
- setParent(targetType, o, outer);
+ if (nn(parentBean()) && nn(o))
+ setParent(targetType, o, parentBean());
return o;
}
@@ -179,29 +179,34 @@ public class BsonParserSession extends
InputStreamParserSession implements Recor
result = raw;
} else if (!eType.isOptional() && (nn(builder) ||
sType.canCreateNewBean(outer))) {
var beanMap = builder == null ? newBeanMap(outer,
sType.inner()) : toBeanMap(builder.create(this, eType));
- while (!is.isDocumentEnd()) {
- var et = is.readElementType();
- var name = is.readElementName();
- var key = trimKey(name);
- var bpm = beanMap.getPropertyMeta(key);
- Object value;
- if
(name.equals(getBeanTypePropertyName(eType))) {
- value = readTypedValue(is, et,
string(), null, null);
- if (nn(value))
- beanMap =
applyTypeProperty(beanMap, value.toString(), eType);
- } else if (bpm != null) {
- var bcm = (ClassMeta<?>)
bpm.getBeanInfo();
- value = readTypedValue(is, et, bcm,
beanMap.getBean(false), bpm);
- setName(bcm, value, key);
- try {
- bpm.set(beanMap, key, value);
- } catch (BeanRuntimeException e) {
- onBeanSetterException(nn(pMeta)
? pMeta : bpm, e);
- throw e;
+ var pb = swapParentBean(beanMap.getBean(false));
+ try {
+ while (!is.isDocumentEnd()) {
+ var et = is.readElementType();
+ var name = is.readElementName();
+ var key = trimKey(name);
+ var bpm = beanMap.getPropertyMeta(key);
+ Object value;
+ if
(name.equals(getBeanTypePropertyName(eType))) {
+ value = readTypedValue(is, et,
string(), null, null);
+ if (nn(value))
+ beanMap =
applyTypeProperty(beanMap, value.toString(), eType);
+ } else if (bpm != null) {
+ var bcm = (ClassMeta<?>)
bpm.getBeanInfo();
+ value = readTypedValue(is, et,
bcm, beanMap.getBean(false), bpm);
+ setName(bcm, value, key);
+ try {
+ bpm.set(beanMap, key,
value);
+ } catch (BeanRuntimeException
e) {
+
onBeanSetterException(nn(pMeta) ? pMeta : bpm, e);
+ throw e;
+ }
+ } else {
+ onUnknownProperty(key, beanMap,
readTypedValue(is, et, object(), null, null));
}
- } else {
- onUnknownProperty(key, beanMap,
readTypedValue(is, et, object(), null, null));
}
+ } finally {
+ swapParentBean(pb);
}
is.readDocumentTerminator();
result = builder == null ? beanMap.getBean() :
builder.build(this, beanMap.getBean(), eType);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
index 251e26a07a..182687d618 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
@@ -221,25 +221,30 @@ public class CborParserSession extends
InputStreamParserSession implements Token
} else if (nn(builder) ||
sType.canCreateNewBean(outer)) {
if (dt == MAP) {
BeanMap m = builder == null ?
newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType));
- for (var i = 0;
shouldContinueContainer(is, len, i); i++) {
- String pName =
readAnything(string(), is, m.getBean(false), null);
- var bpm =
m.getPropertyMeta(pName);
- if (bpm == null) {
- if
(pName.equals(getBeanTypePropertyName(eType)))
-
readAnything(string(), is, null, null);
- else
-
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
- } else {
- var cm = (ClassMeta<?>)
bpm.getBeanInfo();
- Object value =
readAnything(cm, is, m.getBean(false), bpm);
- setName(cm, value,
pName);
- try {
- bpm.set(m,
pName, value);
- } catch
(BeanRuntimeException e) {
-
onBeanSetterException(pMeta, e);
- throw e;
+ var pb =
swapParentBean(m.getBean(false));
+ try {
+ for (var i = 0;
shouldContinueContainer(is, len, i); i++) {
+ String pName =
readAnything(string(), is, m.getBean(false), null);
+ var bpm =
m.getPropertyMeta(pName);
+ if (bpm == null) {
+ if
(pName.equals(getBeanTypePropertyName(eType)))
+
readAnything(string(), is, null, null);
+ else
+
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
+ } else {
+ var cm =
(ClassMeta<?>) bpm.getBeanInfo();
+ Object value =
readAnything(cm, is, m.getBean(false), bpm);
+ setName(cm,
value, pName);
+ try {
+
bpm.set(m, pName, value);
+ } catch
(BeanRuntimeException e) {
+
onBeanSetterException(pMeta, e);
+ throw e;
+ }
}
}
+ } finally {
+ swapParentBean(pb);
}
o = builder == null ? m.getBean() :
builder.build(this, m.getBean(), eType);
} else {
@@ -324,8 +329,8 @@ public class CborParserSession extends
InputStreamParserSession implements Token
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
index 7c2188a7d0..a4ebb89eef 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
@@ -327,16 +327,54 @@ public class HjsonParserSession extends
ReaderParserSession implements RecordRea
var cm = (ClassMeta<?>) pm.getBeanInfo();
if (cm.getNameProperty() != null)
setName(cm, val, key);
- if (cm.getParentProperty() != null)
- setParent(cm, val, bean);
- if (cm.isMap() && val instanceof Map<?,?> val2 &&
!cm.getValueType().isObject() && cm.getValueType().getNameProperty() != null) {
- var valueType = cm.getValueType();
- for (Map.Entry<?,?> e :
((Map<?,?>)val2).entrySet())
- setName(valueType, e.getValue(),
e.getKey());
+ injectParentAnnotations(cm, val, entry.getValue(),
bean);
+ }
+ }
+
+ /*
+ * Recursively injects @ParentProperty (and @NameProperty on map values
/ nested beans) into val.
+ * Intermediate collections and maps are transparent: a collection/map
element's parent is
+ * parentBean (the nearest enclosing bean), skipping all intermediate
containers. This keeps HJSON
+ * consistent with the streaming parsers under TODO-291 (Option A).
+ */
+ private void injectParentAnnotations(ClassMeta<?> cm, Object val,
Object node, Object parentBean) throws ExecutableException {
+ if (val == null || cm == null)
+ return;
+ if (cm.getParentProperty() != null)
+ setParent(cm, val, parentBean);
+ if (cm.isCollectionOrArray()) {
+ var et = cm.getElementType();
+ if (et == null || et.isObject())
+ return;
+ var nodeList = node instanceof List<?> nl ? nl : null;
+ var i = 0;
+ for (var element : toIterable(val)) {
+ injectParentAnnotations(et, element, nodeList
!= null && i < nodeList.size() ? nodeList.get(i) : null, parentBean);
+ i++;
}
+ } else if (cm.isMap() && val instanceof Map<?,?> valMap) {
+ var vt = cm.getValueType();
+ if (vt == null || vt.isObject())
+ return;
+ var nodeMap = node instanceof Map<?,?> nm ? nm : null;
+ for (Map.Entry<?,?> e : valMap.entrySet()) {
+ if (vt.getNameProperty() != null)
+ setName(vt, e.getValue(), e.getKey());
+ injectParentAnnotations(vt, e.getValue(),
nodeMap != null ? nodeMap.get(e.getKey()) : null, parentBean);
+ }
+ } else if (cm.isBean() && !(val instanceof Map) && node
instanceof MarshalledMap nodeMap) {
+ injectAnnotations(nodeMap, val);
}
}
+ private static Iterable<?> toIterable(Object val) {
+ if (val instanceof Collection<?> c)
+ return c;
+ if (val instanceof Object[] a)
+ return Arrays.asList(a);
+ return List.of();
+ }
+
private static Object getBeanValueSafely(BeanMap<?> bm, String key) {
try {
return bm.get(key);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
index 5263d2d23c..6df44d9bff 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
@@ -512,16 +512,54 @@ public class HoconParserSession extends
ReaderParserSession implements RecordRea
var cm = (ClassMeta<?>) pm.getBeanInfo();
if (cm.getNameProperty() != null)
setName(cm, val, key);
- if (cm.getParentProperty() != null)
- setParent(cm, val, bean);
- if (cm.isMap() && val instanceof Map<?,?> val2 &&
!cm.getValueType().isObject() && cm.getValueType().getNameProperty() != null) {
- var valueType = cm.getValueType();
- for (Map.Entry<?,?> e : ((Map<?,?>)
val2).entrySet())
- setName(valueType, e.getValue(),
e.getKey());
+ injectParentAnnotations(cm, val, entry.getValue(),
bean);
+ }
+ }
+
+ /*
+ * Recursively injects @ParentProperty (and @NameProperty on map values
/ nested beans) into val.
+ * Intermediate collections and maps are transparent: a collection/map
element's parent is
+ * parentBean (the nearest enclosing bean), skipping all intermediate
containers. This keeps HOCON
+ * consistent with the streaming parsers under TODO-291 (Option A).
+ */
+ private void injectParentAnnotations(ClassMeta<?> cm, Object val,
Object node, Object parentBean) throws ExecutableException {
+ if (val == null || cm == null)
+ return;
+ if (cm.getParentProperty() != null)
+ setParent(cm, val, parentBean);
+ if (cm.isCollectionOrArray()) {
+ var et = cm.getElementType();
+ if (et == null || et.isObject())
+ return;
+ var nodeList = node instanceof List<?> nl ? nl : null;
+ var i = 0;
+ for (var element : toIterable(val)) {
+ injectParentAnnotations(et, element, nodeList
!= null && i < nodeList.size() ? nodeList.get(i) : null, parentBean);
+ i++;
}
+ } else if (cm.isMap() && val instanceof Map<?,?> valMap) {
+ var vt = cm.getValueType();
+ if (vt == null || vt.isObject())
+ return;
+ var nodeMap = node instanceof Map<?,?> nm ? nm : null;
+ for (Map.Entry<?,?> e : valMap.entrySet()) {
+ if (vt.getNameProperty() != null)
+ setName(vt, e.getValue(), e.getKey());
+ injectParentAnnotations(vt, e.getValue(),
nodeMap != null ? nodeMap.get(e.getKey()) : null, parentBean);
+ }
+ } else if (cm.isBean() && !(val instanceof Map) && node
instanceof MarshalledMap nodeMap) {
+ injectAnnotations(nodeMap, val);
}
}
+ private static Iterable<?> toIterable(Object val) {
+ if (val instanceof Collection<?> c)
+ return c;
+ if (val instanceof Object[] a)
+ return Arrays.asList(a);
+ return List.of();
+ }
+
private static Object getBeanValueSafely(BeanMap<?> bm, String key) {
try {
return bm.get(key);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
index 1e72774208..1201d82e69 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
@@ -394,8 +394,8 @@ public class HtmlParserSession extends XmlParserSession {
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
skipWs(r);
return (T)o;
@@ -410,39 +410,44 @@ public class HtmlParserSession extends XmlParserSession {
"java:S3776" // Cognitive complexity acceptable for HTML bean
parsing
})
private <T> BeanMap<T> readIntoBean(XmlReader r, BeanMap<T> m) throws
IOException, ParseException, ExecutableException, XMLStreamException {
- while (true) {
- HtmlTag tag = nextTag(r, TR, X_TABLE);
- if (tag == X_TABLE)
- break;
- tag = nextTag(r, TD, TH);
- // Skip over the column headers.
- if (tag == TH) {
- skipTag(r);
- r.nextTag();
- skipTag(r);
- } else {
- String key = getElementText(r);
- nextTag(r, TD);
- var pMeta = m.getPropertyMeta(key);
- if (pMeta == null) {
- onUnknownProperty(key, m,
readAnything(object(), r, null, false, null));
+ var pb = swapParentBean(m.getBean(false));
+ try {
+ while (true) {
+ HtmlTag tag = nextTag(r, TR, X_TABLE);
+ if (tag == X_TABLE)
+ break;
+ tag = nextTag(r, TD, TH);
+ // Skip over the column headers.
+ if (tag == TH) {
+ skipTag(r);
+ r.nextTag();
+ skipTag(r);
} else {
- var cm = (ClassMeta<?>)
pMeta.getBeanInfo();
- Object value = readAnything(cm, r,
m.getBean(false), false, pMeta);
- setName(cm, value, key);
- try {
- pMeta.set(m, key, value);
- } catch (BeanRuntimeException e) {
- onBeanSetterException(pMeta, e);
- throw e;
+ String key = getElementText(r);
+ nextTag(r, TD);
+ var pMeta = m.getPropertyMeta(key);
+ if (pMeta == null) {
+ onUnknownProperty(key, m,
readAnything(object(), r, null, false, null));
+ } else {
+ var cm = (ClassMeta<?>)
pMeta.getBeanInfo();
+ Object value = readAnything(cm,
r, m.getBean(false), false, pMeta);
+ setName(cm, value, key);
+ try {
+ pMeta.set(m, key,
value);
+ } catch (BeanRuntimeException
e) {
+
onBeanSetterException(pMeta, e);
+ throw e;
+ }
}
}
+ HtmlTag t = nextTag(r, X_TD, X_TR);
+ if (t == X_TD)
+ nextTag(r, X_TR);
}
- HtmlTag t = nextTag(r, X_TD, X_TR);
- if (t == X_TD)
- nextTag(r, X_TR);
+ return m;
+ } finally {
+ swapParentBean(pb);
}
- return m;
}
/*
@@ -550,25 +555,30 @@ public class HtmlParserSession extends XmlParserSession {
: newBeanMap(l, elementType.inner())
;
// @formatter:on
- for (var key : keys) {
- tag = nextTag(r, X_TD, TD, NULL);
- if (tag == X_TD)
- tag = nextTag(r, TD, NULL);
- if (tag == NULL) {
- m = null;
- nextTag(r, X_NULL);
- break;
- }
- BeanMapEntry e = m.getProperty(key);
- if (e == null) {
- readAnything(object(), r, l,
false, null);
- } else {
- BeanPropertyMeta bpm =
e.getMeta();
- var cm = (ClassMeta<?>)
bpm.getBeanInfo();
- Object value = readAnything(cm,
r, m.getBean(false), false, bpm);
- setName(cm, value, key);
- bpm.set(m, key, value);
+ var pb = swapParentBean(m.getBean(false));
+ try {
+ for (var key : keys) {
+ tag = nextTag(r, X_TD, TD,
NULL);
+ if (tag == X_TD)
+ tag = nextTag(r, TD,
NULL);
+ if (tag == NULL) {
+ m = null;
+ nextTag(r, X_NULL);
+ break;
+ }
+ BeanMapEntry e =
m.getProperty(key);
+ if (e == null) {
+ readAnything(object(),
r, l, false, null);
+ } else {
+ BeanPropertyMeta bpm =
e.getMeta();
+ var cm = (ClassMeta<?>)
bpm.getBeanInfo();
+ Object value =
readAnything(cm, r, m.getBean(false), false, bpm);
+ setName(cm, value, key);
+ bpm.set(m, key, value);
+ }
}
+ } finally {
+ swapParentBean(pb);
}
E element;
if (m == null) {
@@ -578,6 +588,8 @@ public class HtmlParserSession extends XmlParserSession {
} else {
element = (E)m.getBean();
}
+ if (nn(parentBean()) && nn(element))
+ setParent(elementType, element,
parentBean());
l.add(element);
} else {
String c =
getAttributes(r).get(getBeanTypePropertyName(type.getElementType()));
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
index 948104ef38..480fb7c1ff 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
@@ -276,8 +276,8 @@ public class JsonParserSession extends ReaderParserSession
implements TokenReada
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
@@ -328,6 +328,7 @@ public class JsonParserSession extends ReaderParserSession
implements TokenReada
var state = S1;
var currAttr = "";
int c = 0;
+ var pb = swapParentBean(m.getBean(false));
mark();
try {
while (c != -1) {
@@ -402,6 +403,7 @@ public class JsonParserSession extends ReaderParserSession
implements TokenReada
throw new ParseException(this, "Could not find
'}' marking end of JSON object.");
} finally {
unmark();
+ swapParentBean(pb);
}
return null; // Unreachable.
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
index 5e3940c2f5..835a6506a8 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
@@ -253,25 +253,30 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
} else if (nn(builder) ||
sType.canCreateNewBean(outer)) {
if (dt == MAP) {
BeanMap m = builder == null ?
newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType));
- for (var i = 0; i < length; i++) {
- String pName =
readAnything(string(), is, m.getBean(false), null);
- var bpm =
m.getPropertyMeta(pName);
- if (bpm == null) {
- if
(pName.equals(getBeanTypePropertyName(eType)))
-
readAnything(string(), is, null, null);
- else
-
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
- } else {
- var cm = (ClassMeta<?>)
bpm.getBeanInfo();
- Object value =
readAnything(cm, is, m.getBean(false), bpm);
- setName(cm, value,
pName);
- try {
- bpm.set(m,
pName, value);
- } catch
(BeanRuntimeException e) {
-
onBeanSetterException(pMeta, e);
- throw e;
+ var pb =
swapParentBean(m.getBean(false));
+ try {
+ for (var i = 0; i < length;
i++) {
+ String pName =
readAnything(string(), is, m.getBean(false), null);
+ var bpm =
m.getPropertyMeta(pName);
+ if (bpm == null) {
+ if
(pName.equals(getBeanTypePropertyName(eType)))
+
readAnything(string(), is, null, null);
+ else
+
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
+ } else {
+ var cm =
(ClassMeta<?>) bpm.getBeanInfo();
+ Object value =
readAnything(cm, is, m.getBean(false), bpm);
+ setName(cm,
value, pName);
+ try {
+
bpm.set(m, pName, value);
+ } catch
(BeanRuntimeException e) {
+
onBeanSetterException(pMeta, e);
+ throw e;
+ }
}
}
+ } finally {
+ swapParentBean(pb);
}
o = builder == null ? m.getBean() :
builder.build(this, m.getBean(), eType);
} else {
@@ -353,8 +358,8 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
index b9808aca03..cde9b0ddf8 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
@@ -279,6 +279,37 @@ public class ParserSession extends MarshallingSession {
m.set(o, parent);
}
+ /**
+ * Returns the nearest enclosing bean used as the parent for {@link
ParentProperty @ParentProperty} injection.
+ *
+ * <p>
+ * Unlike {@link #getOuter()} (which also serves as the instantiation
context for non-static member/inner classes),
+ * this value tracks only the nearest enclosing <i>bean</i> as the
object graph is parsed, deliberately skipping any
+ * intermediate collections/maps/arrays. It is {@code null} at the
document root (no enclosing bean), which means a
+ * bean parsed at the root or directly inside a top-level collection
has no parent injected.
+ *
+ * @return The nearest enclosing bean, or {@code null} if there is none.
+ */
+ protected final Object parentBean() {
+ return parentBean;
+ }
+
+ /**
+ * Sets the nearest-enclosing-bean tracker used for {@link
ParentProperty @ParentProperty} injection.
+ *
+ * <p>
+ * Callers should invoke this when they begin populating a bean's
properties (passing that bean), then pass the
+ * returned previous value back to this method once done, restoring the
tracker stack-style.
+ *
+ * @param value The new nearest enclosing bean (typically the bean
currently being populated).
+ * @return The previous value, to be passed back to this method to
restore the tracker.
+ */
+ protected final Object swapParentBean(Object value) {
+ var old = parentBean;
+ parentBean = value;
+ return old;
+ }
+
private final HttpPartSchema schema;
private final Method javaMethod;
private final Object outer;
@@ -289,6 +320,7 @@ public class ParserSession extends MarshallingSession {
private final Deque<StringBuilder> sbStack;
private BeanPropertyMeta currentProperty;
private ClassMeta<?> currentClass;
+ private Object parentBean;
private Position mark = new Position(-1);
private ParserPipe pipe;
@@ -303,6 +335,7 @@ public class ParserSession extends MarshallingSession {
ctx = builder.ctx;
javaMethod = builder.javaMethod;
outer = builder.outer;
+ parentBean = builder.outer;
schema = builder.schema;
trimStrings = builder.trimStrings;
nulls = builder.nulls == null ? Nulls.NOT_SET : builder.nulls;
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
index 918b8e9678..40db9ea1d9 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
@@ -422,8 +422,8 @@ public class UonParserSession extends ReaderParserSession
implements HttpPartPar
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
@@ -465,6 +465,7 @@ public class UonParserSession extends ReaderParserSession
implements HttpPartPar
var state = S1;
var currAttr = "";
+ var pb = swapParentBean(m.getBean(false));
mark();
try {
while (true) {
@@ -548,6 +549,7 @@ public class UonParserSession extends ReaderParserSession
implements HttpPartPar
}
} finally {
unmark();
+ swapParentBean(pb);
}
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
index e3098161f9..88ac48c94e 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
@@ -224,8 +224,8 @@ public class UrlEncodingParserSession extends
UonParserSession {
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
@@ -254,6 +254,7 @@ public class UrlEncodingParserSession extends
UonParserSession {
var state = S1;
var currAttr = "";
+ var pb = swapParentBean(m.getBean(false));
mark();
try {
while (c != -1) {
@@ -365,6 +366,7 @@ public class UrlEncodingParserSession extends
UonParserSession {
throw new ParseException(this, "Could not find
end of object.");
} finally {
unmark();
+ swapParentBean(pb);
}
return null; // Unreachable.
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
index fd726d4d33..3f23256afb 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
@@ -969,13 +969,23 @@ public class XmlParserSession extends ReaderParserSession
implements RecordReada
var m = nn(builder) ?
toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.inner());
var bpm =
getXmlBeanMeta(m.getMeta()).getPropertyMeta(fieldName);
var cm = (ClassMeta<?>)
m.getMeta().getBeanInfo();
- Object value = readAnything(cm, currAttr, r,
m.getBean(false), false, null);
- setName(cm, value, currAttr);
- bpm.set(m, currAttr, value);
+ var pb = swapParentBean(m.getBean(false));
+ try {
+ Object value = readAnything(cm,
currAttr, r, m.getBean(false), false, null);
+ setName(cm, value, currAttr);
+ bpm.set(m, currAttr, value);
+ } finally {
+ swapParentBean(pb);
+ }
o = nn(builder) ? builder.build(this,
m.getBean(), eType) : m.getBean();
} else {
var m = nn(builder) ?
toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.inner());
- m = readIntoBean(r, m, isNil);
+ var pb = swapParentBean(m.getBean(false));
+ try {
+ m = readIntoBean(r, m, isNil);
+ } finally {
+ swapParentBean(pb);
+ }
o = nn(builder) ? builder.build(this,
m.getBean(), eType) : m.getBean();
}
} else if (sType.isMap()) {
@@ -1026,8 +1036,8 @@ public class XmlParserSession extends ReaderParserSession
implements RecordReada
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
index 047dd9e49a..330d188dd3 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
@@ -312,8 +312,8 @@ public class YamlParserSession extends ReaderParserSession
implements RecordRead
if (nn(swap) && nn(o))
o = unswap(swap, o, eType);
- if (nn(outer))
- setParent(eType, o, outer);
+ if (nn(parentBean()))
+ setParent(eType, o, parentBean());
return (T)o;
}
@@ -488,6 +488,8 @@ public class YamlParserSession extends ReaderParserSession
implements RecordRead
}
private <T> void readBeanProperty(ParserReader r, BeanMap<T> m, String
currAttr) throws IOException, ParseException, ExecutableException {
+ var pb = swapParentBean(m.getBean(false));
+ try {
var pm = m.getPropertyMeta(currAttr);
setCurrentProperty(pm);
if (pm == null) {
@@ -503,6 +505,9 @@ public class YamlParserSession extends ReaderParserSession
implements RecordRead
throw e;
}
}
+ } finally {
+ swapParentBean(pb);
+ }
setCurrentProperty(null);
}
@@ -868,6 +873,7 @@ public class YamlParserSession extends ReaderParserSession
implements RecordRead
var state = S1;
var currAttr = "";
int c = 0;
+ var pb = swapParentBean(m.getBean(false));
mark();
try {
while (c != -1) {
@@ -941,6 +947,7 @@ public class YamlParserSession extends ReaderParserSession
implements RecordRead
throw new ParseException(this, "Could not find
'}' marking end of YAML flow mapping.");
} finally {
unmark();
+ swapParentBean(pb);
}
return null; // Unreachable.
@@ -953,6 +960,7 @@ public class YamlParserSession extends ReaderParserSession
implements RecordRead
int blockIndent = -1;
+ var pb = swapParentBean(m.getBean(false));
mark();
try {
while (true) {
@@ -1018,6 +1026,7 @@ public class YamlParserSession extends
ReaderParserSession implements RecordRead
}
} finally {
unmark();
+ swapParentBean(pb);
}
return m;
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
new file mode 100644
index 0000000000..623c64be16
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.marshall.bson.*;
+import org.apache.juneau.marshall.cbor.*;
+import org.apache.juneau.marshall.hjson.*;
+import org.apache.juneau.marshall.hocon.*;
+import org.apache.juneau.marshall.html.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.msgpack.*;
+import org.apache.juneau.marshall.parser.*;
+import org.apache.juneau.marshall.serializer.*;
+import org.apache.juneau.marshall.uon.*;
+import org.apache.juneau.marshall.urlencoding.*;
+import org.apache.juneau.marshall.xml.*;
+import org.apache.juneau.marshall.yaml.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Regression tests for TODO-291 / FINISHED-291.
+ *
+ * <p>
+ * Verifies that a bean annotated with {@link ParentProperty} that is nested
inside a collection or
+ * map (or nested containers) receives its <b>nearest enclosing bean</b> as
its parent when parsed,
+ * skipping all intermediate containers. This is the canonical {@code
AddressBook}/{@code List<Person>}
+ * case that previously failed to round-trip because the parser injected the
containing {@code List}
+ * (not the grandparent bean) as the parent.
+ */
+class ParentPropertyContainer_Test extends TestBase {
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Test beans.
+
//------------------------------------------------------------------------------------------------------------------
+
+ public static class AddressBook {
+ public List<Person> people = new ArrayList<>();
+ public List<List<Person>> groups = new ArrayList<>();
+ public Map<String,Person> byName = new LinkedHashMap<>();
+ public Map<String,List<Person>> byCity = new LinkedHashMap<>();
+ public Set<Person> members = new LinkedHashSet<>();
+ }
+
+ public static class Person {
+ public String name;
+
+ @ParentProperty
+ public AddressBook addressBook;
+ }
+
+ static AddressBook newAddressBook() {
+ var ab = new AddressBook();
+ ab.people.add(person("p1"));
+ ab.people.add(person("p2"));
+ ab.groups.add(new ArrayList<>(List.of(person("g1"),
person("g2"))));
+ ab.byName.put("k1", person("m1"));
+ ab.byCity.put("NYC", new ArrayList<>(List.of(person("c1"))));
+ ab.members.add(person("s1"));
+ return ab;
+ }
+
+ static Person person(String name) {
+ var p = new Person();
+ p.name = name;
+ return p;
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Format matrix.
+
//------------------------------------------------------------------------------------------------------------------
+
+ record Fmt(String name, Serializer s, Parser p) {
+ @Override public String toString() { return name; }
+ }
+
+ static Stream<Fmt> formats() {
+ return Stream.of(
+ new Fmt("Json", JsonSerializer.DEFAULT,
JsonParser.DEFAULT),
+ new Fmt("Xml", XmlSerializer.DEFAULT,
XmlParser.DEFAULT),
+ new Fmt("Html", HtmlSerializer.DEFAULT,
HtmlParser.DEFAULT),
+ new Fmt("Uon", UonSerializer.DEFAULT,
UonParser.DEFAULT),
+ new Fmt("UrlEncoding", UrlEncodingSerializer.DEFAULT,
UrlEncodingParser.DEFAULT),
+ new Fmt("Yaml", YamlSerializer.DEFAULT,
YamlParser.DEFAULT),
+ new Fmt("MsgPack", MsgPackSerializer.DEFAULT,
MsgPackParser.DEFAULT),
+ new Fmt("Cbor", CborSerializer.DEFAULT,
CborParser.DEFAULT),
+ new Fmt("Bson", BsonSerializer.DEFAULT,
BsonParser.DEFAULT),
+ new Fmt("Hocon", HoconSerializer.DEFAULT,
HoconParser.DEFAULT),
+ new Fmt("Hjson", HjsonSerializer.DEFAULT,
HjsonParser.DEFAULT)
+ );
+ }
+
+ static Object write(Serializer s, Object o) throws Exception {
+ if (s instanceof WriterSerializer ws)
+ return ws.write(o);
+ return ((OutputStreamSerializer)s).write(o);
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Tests.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("formats")
+ void a01_listElementParentIsEnclosingBean(Fmt f) throws Exception {
+ var ab = newAddressBook();
+ var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+ assertEquals(2, ab2.people.size(), f.name);
+ for (var p : ab2.people)
+ assertSame(ab2, p.addressBook, () -> f.name + ":
List<Person> element parent should be the enclosing AddressBook");
+ }
+
+ @ParameterizedTest
+ @MethodSource("formats")
+ void a02_nestedListElementParentSkipsAllContainers(Fmt f) throws
Exception {
+ var ab = newAddressBook();
+ var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+ assertEquals(1, ab2.groups.size(), f.name);
+ var group = ab2.groups.get(0);
+ assertEquals(2, group.size(), f.name);
+ for (var p : group)
+ assertSame(ab2, p.addressBook, () -> f.name + ":
List<List<Person>> element parent should skip both lists");
+ }
+
+ @ParameterizedTest
+ @MethodSource("formats")
+ void a03_mapValueParentIsEnclosingBean(Fmt f) throws Exception {
+ var ab = newAddressBook();
+ var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+ assertFalse(ab2.byName.isEmpty(), f.name);
+ for (var p : ab2.byName.values())
+ assertSame(ab2, p.addressBook, () -> f.name + ":
Map<String,Person> value parent should be the enclosing AddressBook");
+ }
+
+ @ParameterizedTest
+ @MethodSource("formats")
+ void a04_mapOfListElementParentSkipsContainers(Fmt f) throws Exception {
+ var ab = newAddressBook();
+ var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+ assertFalse(ab2.byCity.isEmpty(), f.name);
+ for (var l : ab2.byCity.values())
+ for (var p : l)
+ assertSame(ab2, p.addressBook, () -> f.name +
": Map<String,List<Person>> element parent should skip map and list");
+ }
+
+ @ParameterizedTest
+ @MethodSource("formats")
+ void a05_setElementParentIsEnclosingBean(Fmt f) throws Exception {
+ var ab = newAddressBook();
+ var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+ assertFalse(ab2.members.isEmpty(), f.name);
+ for (var p : ab2.members)
+ assertSame(ab2, p.addressBook, () -> f.name + ":
Set<Person> element parent should be the enclosing AddressBook");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Top-level collection - parent should be null (no enclosing bean),
not throw.
+ //
+ // Note: HOCON is excluded from the root-level-array cases. HOCON is a
config-file format whose document root
+ // must be an object (braceless key/value pairs), so a top-level array
is a parser limitation unrelated to
+ // @ParentProperty. HOCON's element-parent and null-parent semantics
are still exercised by a01-a05.
+
//------------------------------------------------------------------------------------------------------------------
+
+ static Stream<Fmt> readerFormatsNoHocon() {
+ return formats().filter(f -> f.s instanceof WriterSerializer &&
f.p instanceof ReaderParser && ! "Hocon".equals(f.name));
+ }
+
+ static Stream<Fmt> formatsNoHocon() {
+ return formats().filter(f -> ! "Hocon".equals(f.name));
+ }
+
+ @ParameterizedTest
+ @MethodSource("readerFormatsNoHocon")
+ void b01_topLevelListElementHasNullParent(Fmt f) throws Exception {
+ var l = new ArrayList<Person>(List.of(person("x"),
person("y")));
+ var out = write(f.s, l);
+ @SuppressWarnings("unchecked")
+ List<Person> l2 = (List<Person>) f.p.read((String) out,
List.class, Person.class);
+
+ assertEquals(2, l2.size(), f.name);
+ for (var p : l2)
+ assertNull(p.addressBook, () -> f.name + ": top-level
list element should have a null parent (no enclosing bean)");
+ }
+
+ @ParameterizedTest
+ @MethodSource("formatsNoHocon")
+ void b02_topLevelListBinaryHasNullParent(Fmt f) throws Exception {
+ // Binary/stream formats: round-trip via byte[] / InputStream.
+ var l = new ArrayList<Person>(List.of(person("x")));
+ Object out = write(f.s, l);
+ @SuppressWarnings("unchecked")
+ List<Person> l2 = out instanceof byte[] b
+ ? (List<Person>) f.p.read(new ByteArrayInputStream(b),
List.class, Person.class)
+ : (List<Person>) f.p.read((String) out, List.class,
Person.class);
+
+ assertEquals(1, l2.size(), f.name);
+ assertNull(l2.get(0).addressBook, () -> f.name + ": top-level
list element should have a null parent");
+ }
+}