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