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 <[email protected]>
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 <[email protected]>
---
 .../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  [...]
 
 ---
 

Reply via email to