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  [...]
 
 ---