You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/05/13 21:29:55 UTC
(juneau) 03/21: refactor: decouple swap-aware get/set from BeanPropertyMeta via pluggable transforms (TODO-5 Step 3)
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
commit cc1a22236ffa0eb6d7c6daf0aa71a72d59fadf90
Author: James Bognar <ja...@jamesbog-ltmjqf2.internal.salesforce.com>
AuthorDate: Tue May 12 11:52:33 2026 -0400
refactor: decouple swap-aware get/set from BeanPropertyMeta via pluggable transforms (TODO-5 Step 3)
Co-authored-by: Cursor <cu...@cursor.com>
---
.../src/main/java/org/apache/juneau/BeanMeta.java | 74 +++++++++++
.../java/org/apache/juneau/BeanPropertyMeta.java | 143 +++++++++++----------
todo/TODO-5-bean-runtime-types-to-commons.md | 8 +-
3 files changed, 152 insertions(+), 73 deletions(-)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index f464590b26..29a8de9d20 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -39,6 +39,9 @@ import org.apache.juneau.commons.reflect.Visibility;
import org.apache.juneau.commons.utils.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
/**
* Encapsulates all access to the properties of a bean class (like a souped-up {@link java.beans.BeanInfo}).
@@ -624,6 +627,8 @@ public class BeanMeta<T> {
if (p.validate(marshallingContext, beanRegistry.get(), typeVarImpls, readOnlyProps, writeOnlyProps)) {
+ installSwapAwareTransforms(p);
+
if (nn(p.getter))
getterProps.put(p.getter.inner(), p.name);
@@ -638,6 +643,75 @@ public class BeanMeta<T> {
}
}
+ /**
+ * Installs swap-aware read/write transforms on a {@link BeanPropertyMeta.Builder} after validation.
+ *
+ * <p>
+ * After {@link BeanPropertyMeta.Builder#validate validate()} succeeds, the builder's {@code swap} and
+ * {@code rawTypeMeta} fields describe whether the property has a configured {@link ObjectSwap} (via
+ * {@link org.apache.juneau.annotation.MarshalledProp @MarshalledProp(format=...)} or
+ * {@link org.apache.juneau.annotation.Swap @Swap}) and whether the property's raw type has child swaps registered
+ * on it. This method packages those concerns into install-time closures so the marshalling-side swap behavior is
+ * established as data on the {@link BeanPropertyMeta} rather than executed by the bean-modeling
+ * {@link BeanPropertyMeta#get get}/{@link BeanPropertyMeta#set set} methods themselves.
+ *
+ * <p>
+ * If neither a property-level swap nor a child swap on the raw type meta is present, no transforms are installed
+ * and the property's {@code get}/{@code set} fall through to identity (raw access).
+ *
+ * @param p The builder to attach swap-aware transforms to.
+ */
+ @SuppressWarnings({
+ "rawtypes", // ObjectSwap used raw to mirror BeanPropertyMeta's field declaration.
+ "unchecked" // Wildcard ObjectSwap captured by raw alias to allow runtime polymorphic dispatch.
+ })
+ private static void installSwapAwareTransforms(BeanPropertyMeta.Builder p) {
+ ObjectSwap sw = p.swap;
+ ClassMeta<?> rtm = p.rawTypeMeta;
+ if (sw == null && (rtm == null || ! rtm.hasChildSwaps()))
+ return;
+ if (p.readTransform == null) {
+ p.readTransform = (session, o) -> {
+ try {
+ if (nn(sw))
+ return sw.swap(session, o);
+ if (o == null)
+ return null;
+ if (rtm.hasChildSwaps()) {
+ ObjectSwap f = rtm.getChildObjectSwapForSwap(o.getClass());
+ if (nn(f))
+ return f.swap(session, o);
+ }
+ return o;
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SerializeException(e);
+ }
+ };
+ }
+ if (p.writeTransform == null) {
+ p.writeTransform = (session, o) -> {
+ try {
+ if (nn(sw))
+ return sw.unswap(session, o, rtm);
+ if (o == null)
+ return null;
+ if (rtm.hasChildSwaps()) {
+ ObjectSwap f = rtm.getChildObjectSwapForUnswap(o.getClass());
+ if (nn(f))
+ return f.unswap(session, o, rtm);
+ }
+ return o;
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ParseException(e);
+ }
+ };
+ }
+ }
+
@Override /* Overridden from Object */
public boolean equals(Object o) {
return (o instanceof BeanMeta<?> o2) && eq(this, o2, (x, y) -> eq(x.classInfo, y.classInfo));
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
index 1d749a4bc4..8bec99efa8 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
@@ -92,14 +92,16 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
MethodInfo getter; // Package-private for BeanMeta access
MethodInfo setter; // Package-private for BeanMeta access
MethodInfo extraKeys; // Package-private for BeanMeta access
+ ClassMeta<?> rawTypeMeta; // Package-private for BeanMeta access (used to install swap-aware transforms)
+ ObjectSwap swap; // Package-private for BeanMeta access (used to install swap-aware transforms)
+ BiFunction<MarshallingSession,Object,Object> readTransform; // Package-private; defaults to identity if null.
+ BiFunction<MarshallingSession,Object,Object> writeTransform; // Package-private; defaults to identity if null.
private boolean isConstructorArg;
private boolean isUri;
private boolean isDyna;
private boolean isDynaGetterMap;
- private ClassMeta<?> rawTypeMeta;
private ClassMeta<?> typeMeta;
private List<String> properties;
- private ObjectSwap swap;
private BeanRegistry beanRegistry;
private Object overrideValue;
private BeanPropertyMeta delegateFor;
@@ -143,6 +145,46 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
return this;
}
+ /**
+ * Installs the read-side value transform for this property.
+ *
+ * <p>
+ * Applied to the raw getter result by {@link BeanPropertyMeta#get(BeanMap,String)} before it is returned to the
+ * caller. Defaults to identity (raw value passes through unchanged).
+ *
+ * <p>
+ * Used by the marshalling layer to install {@link ObjectSwap}-aware behavior at bean-meta construction time;
+ * the bean-modeling layer itself only invokes the function and does not directly reference
+ * {@link ObjectSwap}.
+ *
+ * @param value The transform function. Must not be <jk>null</jk>.
+ * @return This object.
+ */
+ public Builder readTransform(BiFunction<MarshallingSession,Object,Object> value) {
+ readTransform = assertArgNotNull(ARG_value, value);
+ return this;
+ }
+
+ /**
+ * Installs the write-side value transform for this property.
+ *
+ * <p>
+ * Applied to the incoming value by {@link BeanPropertyMeta#set(BeanMap,String,Object)} before the raw setter is
+ * invoked. Defaults to identity (raw value passes through unchanged).
+ *
+ * <p>
+ * Used by the marshalling layer to install {@link ObjectSwap}-aware behavior at bean-meta construction time;
+ * the bean-modeling layer itself only invokes the function and does not directly reference
+ * {@link ObjectSwap}.
+ *
+ * @param value The transform function. Must not be <jk>null</jk>.
+ * @return This object.
+ */
+ public Builder writeTransform(BiFunction<MarshallingSession,Object,Object> value) {
+ writeTransform = assertArgNotNull(ARG_value, value);
+ return this;
+ }
+
/**
* Sets the overridden value of this bean property.
*
@@ -520,10 +562,12 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
private final Object overrideValue; // The bean property value (if it's an overridden delegate).
private final List<String> properties; // The value of the @MarshalledProp(properties) annotation (unmodifiable).
private final ClassMeta<?> rawTypeMeta; // The real class type of the bean property.
+ private final BiFunction<MarshallingSession,Object,Object> readTransform; // Applied to raw getter result; identity by default.
private final boolean readOnly; // True if this property is read-only.
private final MethodInfo setter; // The bean property setter.
private final ObjectSwap swap; // ObjectSwap defined only via @MarshalledProp(format=...) annotation.
private final ClassMeta<?> typeMeta; // The transformed class type of the bean property.
+ private final BiFunction<MarshallingSession,Object,Object> writeTransform; // Applied to incoming value before raw setter; identity by default.
private final boolean writeOnly; // True if this property is write-only.
/**
@@ -555,6 +599,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
swap = b.swap;
typeMeta = b.typeMeta;
writeOnly = b.writeOnly;
+ readTransform = b.readTransform != null ? b.readTransform : (session, o) -> o;
+ writeTransform = b.writeTransform != null ? b.writeTransform : (session, o) -> o;
ap = bc.getAnnotationProvider();
hashCode = h(beanMeta, name);
@@ -1042,8 +1088,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
var session = m.getMarshallingSession();
- // Convert to raw form.
- value1 = unswap(session, value1);
+ // Apply the install-time write transform (identity by default; swap-aware in the marshalling layer).
+ value1 = writeTransform.apply(session, value1);
if (m.bean == null) {
@@ -1206,7 +1252,10 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
} else {
if (nn(swap) && value1 != null && swap.getSwapClass().isAssignableFrom(value1.getClass())) {
- value1 = swap.unswap(session, value1, rawTypeMeta);
+ // Defensive double-unswap path: value1 is still in swapped form (the outer writeTransform
+ // did not normalize it for some reason). Route through the install-time write transform so
+ // BeanPropertyMeta itself does not invoke ObjectSwap directly.
+ value1 = writeTransform.apply(session, value1);
} else {
// Pass bean as outer for non-static inner class instantiation (e.g. J2 with string constructor)
value1 = session.convertToMemberType(bean, value1, rawTypeMeta);
@@ -1298,32 +1347,28 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
}
private Object swapAndFilterProperty(MarshallingSession session, Object o) {
- try {
- o = swap(session, o);
- if (o == null)
- return null;
- if (nn(properties)) {
- if (rawTypeMeta.isArray()) {
- var a = (Object[])o;
- var l1 = new DelegateList(rawTypeMeta);
- var childType1 = rawTypeMeta.getElementType();
- for (var c1 : a)
- l1.add(applyChildPropertiesFilter(session, childType1, c1));
- return l1;
- } else if (rawTypeMeta.isCollection()) {
- var c = (Collection)o;
- var l = listOfSize(c.size());
- var childType = rawTypeMeta.getElementType();
- c.forEach(x -> l.add(applyChildPropertiesFilter(session, childType, x)));
- return l;
- } else {
- return applyChildPropertiesFilter(session, rawTypeMeta, o);
- }
+ o = readTransform.apply(session, o);
+ if (o == null)
+ return null;
+ if (nn(properties)) {
+ if (rawTypeMeta.isArray()) {
+ var a = (Object[])o;
+ var l1 = new DelegateList(rawTypeMeta);
+ var childType1 = rawTypeMeta.getElementType();
+ for (var c1 : a)
+ l1.add(applyChildPropertiesFilter(session, childType1, c1));
+ return l1;
+ } else if (rawTypeMeta.isCollection()) {
+ var c = (Collection)o;
+ var l = listOfSize(c.size());
+ var childType = rawTypeMeta.getElementType();
+ c.forEach(x -> l.add(applyChildPropertiesFilter(session, childType, x)));
+ return l;
+ } else {
+ return applyChildPropertiesFilter(session, rawTypeMeta, o);
}
- return o;
- } catch (SerializeException e) {
- throw bex(e);
}
+ return o;
}
private Object invokeGetter(Object bean, String pName) throws IllegalArgumentException {
@@ -1370,46 +1415,6 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> {
getClassMeta().getName(), cn(val));
}
- private Object swap(MarshallingSession session, Object o) throws SerializeException {
- try {
- // First use swap defined via @MarshalledProp.
- if (nn(swap))
- return swap.swap(session, o);
- if (o == null)
- return null;
- // Otherwise, look it up via bean context.
- if (rawTypeMeta.hasChildSwaps()) {
- ObjectSwap f = rawTypeMeta.getChildObjectSwapForSwap(o.getClass());
- if (nn(f))
- return f.swap(session, o);
- }
- return o;
- } catch (SerializeException e) {
- throw e;
- } catch (Exception e) {
- throw new SerializeException(e);
- }
- }
-
- private Object unswap(MarshallingSession session, Object o) throws ParseException {
- try {
- if (nn(swap))
- return swap.unswap(session, o, rawTypeMeta);
- if (o == null)
- return null;
- if (rawTypeMeta.hasChildSwaps()) {
- ObjectSwap f = rawTypeMeta.getChildObjectSwapForUnswap(o.getClass());
- if (nn(f))
- return f.unswap(session, o, rawTypeMeta);
- }
- return o;
- } catch (ParseException e) {
- throw e;
- } catch (Exception e) {
- throw new ParseException(e);
- }
- }
-
/**
* Returns <jk>true</jk> if this property is write-only.
*
diff --git a/todo/TODO-5-bean-runtime-types-to-commons.md b/todo/TODO-5-bean-runtime-types-to-commons.md
index 1fc5b3b08a..6cf99c7b34 100644
--- a/todo/TODO-5-bean-runtime-types-to-commons.md
+++ b/todo/TODO-5-bean-runtime-types-to-commons.md
@@ -4,13 +4,13 @@ This is the remaining work from **Phase 5 of the bean-layer split**. Phase 5a (t
---
-## Status (as of Phase 5b checkpoint)
+## Status (as of Phase 5c checkpoint)
-**Step 1 complete.** A `BeanConfigContext` POJO + builder now lives in `commons.bean`; `MarshallingContext.getBeanConfigContext()` returns a memoized snapshot view. The eight runtime types still live in `juneau-marshall` and still use `MarshallingContext` directly — Step 1 is purely additive infrastructure that future steps can lean on.
+**Step 3 complete.** `BeanPropertyMeta.get`/`set` no longer call `ObjectSwap.swap`/`unswap` directly. The class now carries two install-time `BiFunction<MarshallingSession,Object,Object>` callbacks (`readTransform`/`writeTransform`) that default to identity. `BeanMeta` installs swap-aware closures via a new private helper `installSwapAwareTransforms(BeanPropertyMeta.Builder)` immediately after `Builder.validate()` succeeds (only when `Builder.swap != null` or `Builder.rawTypeMeta.hasChil [...]
- [x] **Step 1** — `BeanConfigContext` POJO + builder in `commons.bean`. Carries: visibility settings, all `beans*Require*` toggles, `findFluentSetters`, `unsortedProperties`, `useInterfaceProxies`, `useJavaBeanIntrospector`, `ignoreMissingSetters`, `ignoreTransientFields`, `ignoreUnknownBeanProperties`, `propertyNamer`, `beanTypePropertyName`, `notBeanPackageNames` / `notBeanPackagePrefixes` / `notBeanClasses`, `BeanStore`, `AnnotationProvider`, optional `Predicate<ClassInfo>` override [...]
- [x] **Step 2** — Replaced `ClassMeta` with `ClassInfo` for pure-reflection access inside `BeanMeta`. Added a `classInfo` field (a re-typed view of the same instance as `classMeta`, since `ClassMeta extends ClassInfoTyped extends ClassInfo`) and routed all reflection calls (`inner()`, `isMemberClass()`, `isNotStatic()`, `isAnonymousClass()`, `isRecord()`, `isInterface()`, `getRecordComponents()`, `getName()`, `getParentsAndInterfaces()`, `getPublicConstructors()`, `getDeclaredConstructo [...]
-- [ ] **Step 3** — Remove swap-aware `get/set` from `BeanPropertyMeta`. Add identity-default `BiFunction<Object,Object,Object>` callbacks (or a small `BeanPropertyTransform` SPI) so the marshalling layer installs swap-aware behavior at session construction.
+- [x] **Step 3** — Removed swap-aware `get`/`set` from `BeanPropertyMeta`. Picked option (a) (pluggable callbacks). Added `BiFunction<MarshallingSession,Object,Object>` `readTransform` / `writeTransform` fields with identity defaults; exposed corresponding `Builder.readTransform(...)` / `Builder.writeTransform(...)` setters. The bean-modeling `get`/`set` paths inside `BeanPropertyMeta` no longer call `ObjectSwap.swap` / `ObjectSwap.unswap` directly — instead they invoke the installed tra [...]
- [ ] **Step 4** — Remove `MarshallingSession` back-pointer from `BeanMap`. After Step 3, `BeanMap.get/put` are raw property reads/writes; `MarshallingSession.toBeanMap` wraps a `BeanMap` for serialization and applies swaps externally.
- [ ] **Step 5** — Remove `BeanRegistry` field from `BeanPropertyMeta`. Lift dictionary metadata into a marshalling-side companion (`MarshalledPropertyMeta` or a side-map keyed by `BeanPropertyMeta`).
- [ ] **Step 6** — `BeanMeta` becomes constructible by both `ClassMeta` and direct `commons.bean` callers via `BeanMeta.of(MyClass.class, BeanConfigContext.DEFAULT)`. `ClassMeta` becomes a *consumer* of `BeanMeta` rather than its creator.
@@ -19,7 +19,7 @@ This is the remaining work from **Phase 5 of the bean-layer split**. Phase 5a (t
- [ ] **Step 9** — Reference sweep: 80–120 unique files (mostly inside `juneau-marshall`). Update imports, Javadoc `{@link …}` references, package-info docs.
- [ ] **Step 10** — Update `juneau-docs` release notes / migration guide (`docs/pages/release-notes/9.5.0.md`, `## Package Moves` section) with the bean-runtime relocations.
-The "incomplete-but-documented over broken-build" rule from Phase 5a still applies. When picking up the next slice of this work, Step 3 is the recommended next checkpoint — Step 2 removed the general `ClassMeta` reflection coupling, so the next blocker is the swap-aware `get`/`set` paths in `BeanPropertyMeta` (and the associated `rawTypeMeta` / `typeMeta` / `ObjectSwap` / `BeanRegistry` fields that Step 2 intentionally left in place).
+The "incomplete-but-documented over broken-build" rule from Phase 5a still applies. When picking up the next slice of this work, **Step 4 is the recommended next checkpoint** — Step 3 made `BeanPropertyMeta.get`/`set` raw (modulo installed callback), so the next blocker is the `MarshallingSession` back-pointer carried by `BeanMap` and the parallel swap-application sites currently inside `BeanMap` (none today on the read path — `BeanMap.get` just delegates to `BeanPropertyMeta.get` — but [...]
---