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 8f291189e595490c3b8d2c8e6bdd1b48b131599b Author: James Bognar <[email protected]> AuthorDate: Tue May 12 12:46:00 2026 -0400 refactor: make BeanMeta/BeanPropertyMeta constructible from BeanConfigContext (TODO-5 Step 6) Co-authored-by: Cursor <[email protected]> --- .../src/main/java/org/apache/juneau/BeanMap.java | 25 ++- .../src/main/java/org/apache/juneau/BeanMeta.java | 215 ++++++++++++++++----- .../java/org/apache/juneau/BeanPropertyMeta.java | 200 +++++++++++-------- .../apache/juneau/commons/bean/BeanMeta_Test.java | 174 +++++++++++++++++ todo/TODO-5-bean-runtime-types-to-commons.md | 40 +++- 5 files changed, 517 insertions(+), 137 deletions(-) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java index f3efd25e1d..7da3909d2b 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java @@ -84,6 +84,23 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T return MarshallingContext.DEFAULT_SESSION.toBeanMap(bean); } + /** + * Convenience method for wrapping a bean inside a {@link BeanMap} using a pre-built {@link BeanMeta}. + * + * <p> + * This is the bean-modeling entry point — it builds a {@link BeanMap} without going through a + * {@link MarshallingSession}, paired with a {@link BeanMeta} typically produced by + * {@link BeanMeta#of(Class, BeanConfigContext)}. No marshalling session is attached. + * + * @param <T> The bean type. + * @param bean The bean being wrapped. + * @param meta The bean metadata. Must not be <jk>null</jk>. + * @return A new {@link BeanMap} instance wrapping the bean. + */ + public static <T> BeanMap<T> of(T bean, BeanMeta<T> meta) { + return new BeanMap<>(bean, meta); + } + /** The wrapped object. */ protected T bean; @@ -147,9 +164,9 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T public void add(String property, Object value) { var p = getPropertyMeta(property); if (p == null) { - if (meta.getMarshallingContext().isIgnoreUnknownBeanProperties()) + if (meta.getConfig().isIgnoreUnknownBeanProperties()) return; - throw bex(meta.getClassMeta(), "Bean property ''{0}'' not found.", property); + throw bex(meta.getClassInfo(), "Bean property ''{0}'' not found.", property); } p.add(this, property, value); } @@ -672,12 +689,12 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T public Object put(String property, Object value) { var p = getPropertyMeta(property); if (p == null) { - if (meta.getMarshallingContext().isIgnoreUnknownBeanProperties() || property.equals(typePropertyName)) + if (meta.getConfig().isIgnoreUnknownBeanProperties() || property.equals(typePropertyName)) return meta.onWriteProperty(bean, property, null); p = getPropertyMeta("*"); if (p == null) - throw bex(meta.getClassMeta(), "Bean property ''{0}'' not found.", property); + throw bex(meta.getClassInfo(), "Bean property ''{0}'' not found.", property); } return p.set(this, property, value); } 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 6a741cbe70..eb0cc118cd 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 @@ -19,6 +19,7 @@ package org.apache.juneau; import static org.apache.juneau.BeanMeta.MethodType.*; import static org.apache.juneau.commons.reflect.AnnotationTraversal.*; import static org.apache.juneau.commons.reflect.ReflectionUtils.*; +import static org.apache.juneau.commons.utils.AssertionUtils.*; import static org.apache.juneau.commons.utils.CollectionUtils.*; import static org.apache.juneau.commons.utils.StringUtils.*; import static org.apache.juneau.commons.utils.ThrowableUtils.*; @@ -358,13 +359,14 @@ public class BeanMeta<T> { } private BeanConstructor beanConstructor; // The constructor for this bean. - private final MarshallingContext marshallingContext; // The bean context that created this metadata object. + private final MarshallingContext marshallingContext; // The bean context that created this metadata object. Null when constructed via {@link #of(Class, BeanConfigContext)}. + private final BeanConfigContext config; // Bean-modeling settings facade — always non-null. Sources: marshallingContext.getBeanConfigContext() (marshalling-side) or the explicit BeanConfigContext (commons-side). private final MarshalledFilter beanFilter; // Optional bean filter associated with the target class. private final NullableSupplier<InvocationHandler> beanProxyInvocationHandler; // The invocation handler for this bean (if it's an interface). private final Supplier<BeanRegistry> beanRegistry; // The bean registry for this bean. private final Supplier<List<ClassInfo>> classHierarchy; // List of all classes traversed in the class hierarchy. - private final ClassMeta<T> classMeta; // The target class type that this meta object describes. - private final ClassInfo classInfo; // Pure-reflection view of the bean class (Step 2 of TODO-5 — decouples bean modeling from ClassMeta). + private final ClassMeta<T> classMeta; // The target class type that this meta object describes. Null when constructed via {@link #of(Class, BeanConfigContext)}. + private final ClassInfo classInfo; // Pure-reflection view of the bean class (Step 2 of TODO-5 — decouples bean modeling from ClassMeta). Always non-null. private final Supplier<String> dictionaryName; // The @Marshalled(typeName) annotation defined on this bean class. private final BeanPropertyMeta dynaProperty; // "extras" property. @SuppressWarnings("rawtypes") @@ -382,6 +384,43 @@ public class BeanMeta<T> { private final String typePropertyName; // "_type" property actual name. private final Map<BeanPropertyMeta,BeanRegistry> propertyBeanRegistries; // Per-property BeanRegistry side-map (Step 5 of TODO-5 — keeps BeanRegistry off BeanPropertyMeta itself). + /** + * Creates a {@link BeanMeta} for the specified class using the supplied {@link BeanConfigContext}. + * + * <p> + * This is the bean-modeling entry point — it constructs a {@link BeanMeta} purely from a {@link Class} + * and a {@link BeanConfigContext}, without touching any marshalling-side type infrastructure + * ({@link ClassMeta}, {@link MarshallingContext}, {@link BeanRegistry}, {@link org.apache.juneau.swap.ObjectSwap}). + * The returned {@link BeanMeta} carries enough information to do raw getter/setter invocation and property + * iteration; marshalling-aware reads ({@link #getClassMeta()}, type-resolution on each + * {@link BeanPropertyMeta#getClassMeta()}, the per-property {@link BeanRegistry}) remain <jk>null</jk>. + * + * <h5 class='section'>Example:</h5> + * <p class='bjava'> + * BeanMeta<Person> <jv>bm</jv> = BeanMeta.<jsm>of</jsm>(Person.<jk>class</jk>, BeanConfigContext.<jsf>DEFAULT</jsf>); + * BeanPropertyMeta <jv>name</jv> = <jv>bm</jv>.getProperties().get(<js>"name"</js>); + * </p> + * + * @param <T> The bean class type. + * @param beanClass The class to build a {@link BeanMeta} for. Must not be <jk>null</jk>. + * @param config The bean-modeling configuration to apply. Must not be <jk>null</jk>. + * @return A new {@link BeanMeta} for the specified class. + */ + public static <T> BeanMeta<T> of(Class<T> beanClass, BeanConfigContext config) { + return new BeanMeta<>(beanClass, config); + } + + /** + * Same as {@link #of(Class, BeanConfigContext)} using {@link BeanConfigContext#DEFAULT}. + * + * @param <T> The bean class type. + * @param beanClass The class to build a {@link BeanMeta} for. Must not be <jk>null</jk>. + * @return A new {@link BeanMeta} for the specified class. + */ + public static <T> BeanMeta<T> of(Class<T> beanClass) { + return of(beanClass, BeanConfigContext.DEFAULT); + } + /** * Constructor. * @@ -405,23 +444,45 @@ public class BeanMeta<T> { * @param pNames Explicit list of property names and order. If <jk>null</jk>, properties are determined automatically. * @param implClass Optional implementation class constructor to use if one cannot be found. Can be <jk>null</jk>. */ + protected BeanMeta(ClassMeta<T> cm, MarshalledFilter bf, String[] pNames, ClassInfo implClass) { + this(cm, cm, cm.getMarshallingContext().getBeanConfigContext(), cm.getMarshallingContext(), bf, pNames, implClass); + } + + /** + * Bean-modeling constructor — builds a {@link BeanMeta} without a {@link MarshallingContext}. + * + * <p> + * See {@link #of(Class, BeanConfigContext)} for the public entry point. The {@link #getClassMeta()}, + * {@link #getMarshallingContext() marshallingContext}, and per-property {@link BeanRegistry} fields are + * left <jk>null</jk>; the per-property {@link BeanPropertyMeta#getClassMeta() rawTypeMeta}/{@code typeMeta} + * fields are also <jk>null</jk> (no type-resolution is performed in this path). + * + * @param beanClass The bean class. Must not be <jk>null</jk>. + * @param config The bean-modeling configuration. Must not be <jk>null</jk>. + */ + protected BeanMeta(Class<T> beanClass, BeanConfigContext config) { + this(null, info(assertArgNotNull("beanClass", beanClass)), assertArgNotNull("config", config), null, null, null, null); + } + @SuppressWarnings({ - "java:S3776" // Cognitive complexity acceptable for bean metadata initialization + "java:S3776", // Cognitive complexity acceptable for bean metadata initialization + "java:S107" // 7 parameters needed to support both construction paths }) - protected BeanMeta(ClassMeta<T> cm, MarshalledFilter bf, String[] pNames, ClassInfo implClass) { + private BeanMeta(ClassMeta<T> cm, ClassInfo ci0, BeanConfigContext config, MarshallingContext mc, MarshalledFilter bf, String[] pNames, ClassInfo implClass) { classMeta = cm; - classInfo = cm; - marshallingContext = cm.getMarshallingContext(); + classInfo = ci0; + this.config = config; + marshallingContext = mc; beanFilter = bf; implClassConstructor = opt(implClass).map(x -> x.getPublicConstructor(x2 -> x2.hasNumParameters(0)).orElse(null)).orElse(null); - fluentSetters = marshallingContext.isFindFluentSetters() || (nn(bf) && bf.isFluentSetters()); + fluentSetters = config.isFindFluentSetters() || (nn(bf) && bf.isFluentSetters()); stopClass = opt(bf).map(x -> x.getStopClass()).orElse(info(Object.class)); beanRegistry = memoize(this::findBeanRegistry); classHierarchy = memoize(this::findClassHierarchy); beanConstructor = findBeanConstructor(); // Local variables for initialization - var ap = marshallingContext.getAnnotationProvider(); + var ap = config.getAnnotationProvider(); var c = classInfo.inner(); var ci = classInfo; String notABeanReasonTemp = null; @@ -432,14 +493,14 @@ public class BeanMeta<T> { var dynaPropertyValue = Value.<BeanPropertyMeta>empty(); var propertyBeanRegistriesTemp = CollectionUtils.<BeanPropertyMeta,BeanRegistry>map(); // Per-property BeanRegistry side-map (TODO-5 Step 5). var unsortedPropertiesTemp = false; - var ba = ap.find(Marshalled.class, cm); - var btList = ap.find(org.apache.juneau.commons.bean.BeanType.class, cm); - var propertyNamer = opt(bf).map(x -> x.getPropertyNamer()).orElse(marshallingContext.getPropertyNamer()); + var ba = ap.find(Marshalled.class, classInfo); + var btList = ap.find(org.apache.juneau.commons.bean.BeanType.class, classInfo); + var propertyNamer = opt(bf).map(x -> x.getPropertyNamer()).orElse(config.getPropertyNamer()); - this.typePropertyName = ba.stream().map(x -> x.inner().typePropertyName()).filter(Utils::ne).findFirst().orElseGet(marshallingContext::getBeanTypePropertyName); + this.typePropertyName = ba.stream().map(x -> x.inner().typePropertyName()).filter(Utils::ne).findFirst().orElseGet(config::getBeanTypePropertyName); // Check if constructor is required but not found (records are exempt since they use canonical constructors) - if (! beanConstructor.constructor().isPresent() && bf == null && marshallingContext.isBeansRequireDefaultConstructor() && ! ci.isRecord()) + if (! beanConstructor.constructor().isPresent() && bf == null && config.isBeansRequireDefaultConstructor() && ! ci.isRecord()) notABeanReasonTemp = "Class does not have the required no-arg constructor"; var bfo = opt(bf); @@ -452,7 +513,7 @@ public class BeanMeta<T> { // ensure that ordering first. fixedBeanProps.forEach(x -> normalProps.put(x, BeanPropertyMeta.builder(this, x))); - if (marshallingContext.isUseJavaBeanIntrospector()) { + if (config.isUseJavaBeanIntrospector()) { var c2 = bfo.map(x -> x.getInterfaceClass()).filter(Objects::nonNull).orElse(classInfo); BeanInfo bi = null; if (! c2.isInterface()) @@ -553,10 +614,10 @@ public class BeanMeta<T> { } // Make sure at least one property was found (records with no components are exempt). - if (bf == null && marshallingContext.isBeansRequireSomeProperties() && normalProps.isEmpty() && ! ci.isRecord()) + if (bf == null && config.isBeansRequireSomeProperties() && normalProps.isEmpty() && ! ci.isRecord()) notABeanReasonTemp = "No properties detected on bean class"; - unsortedPropertiesTemp = marshallingContext.isUnsortedProperties() || bfo.map(x -> x.isUnsortedProperties()).orElse(false) || !fixedBeanProps.isEmpty(); + unsortedPropertiesTemp = config.isUnsortedProperties() || bfo.map(x -> x.isUnsortedProperties()).orElse(false) || !fixedBeanProps.isEmpty(); propertiesValue.set(unsortedPropertiesTemp ? map() : sortedMap()); @@ -567,8 +628,10 @@ public class BeanMeta<T> { propertiesValue.get().put(k, pMeta); // Build the property-level BeanRegistry side-map entry from the builder's accumulated // dictionary classes. Parents to the bean-level registry so @MarshalledProp(dictionary={}) - // entries chain on top of bean and global dictionaries. - propertyBeanRegistriesTemp.put(pMeta, new BeanRegistry(marshallingContext, beanRegistry.get(), v.dictionaryClasses)); + // entries chain on top of bean and global dictionaries. Skipped on the commons-side path + // (no marshallingContext means no BeanRegistry construction). + if (nn(marshallingContext) && nn(v.dictionaryClasses)) + propertyBeanRegistriesTemp.put(pMeta, new BeanRegistry(marshallingContext, beanRegistry.get(), v.dictionaryClasses)); }); // If a beanFilter is defined, look for inclusion and exclusion lists. @@ -619,11 +682,12 @@ public class BeanMeta<T> { typeProperty = BeanPropertyMeta.builder(this, typePropertyName).canRead().canWrite().rawMetaType(String.class).build(); // Map the synthetic "_type" property to the bean-level BeanRegistry so consumers calling // typeProperty.getBeanRegistry() (currently none in-tree, but a public API path) get the same - // registry the property previously carried as a field. - propertyBeanRegistriesTemp.put(typeProperty, beanRegistry.get()); + // registry the property previously carried as a field. Skipped on the commons-side path. + if (nn(marshallingContext)) + propertyBeanRegistriesTemp.put(typeProperty, beanRegistry.get()); propertyBeanRegistries = u(propertyBeanRegistriesTemp); dictionaryName = memoize(this::findDictionaryName); - beanProxyInvocationHandler = memoize(() -> marshallingContext.isUseInterfaceProxies() && classInfo.isInterface() ? new BeanProxyInvocationHandler<>(this) : null); + beanProxyInvocationHandler = memoize(() -> config.isUseInterfaceProxies() && classInfo.isInterface() ? new BeanProxyInvocationHandler<>(this) : null); var factoryClassTemp = btList.stream().map(x -> x.inner().factory()).filter(x -> x != org.apache.juneau.commons.function.BeanFactory.Void.class).findFirst().orElse(null); factoryClass = factoryClassTemp; } @@ -636,6 +700,8 @@ public class BeanMeta<T> { if (p.field == null) findInnerBeanField(p.name).ifPresent(p::setInnerField); + // When constructed via the commons-side path, marshallingContext is null and validate() runs in + // raw-reflection mode (no ClassMeta/ObjectSwap/BeanRegistry resolution). if (p.validate(marshallingContext, typeVarImpls, readOnlyProps, writeOnlyProps)) { installSwapAwareTransforms(p); @@ -795,10 +861,39 @@ public class BeanMeta<T> { /** * Returns the {@link ClassMeta} of this bean. * - * @return The {@link ClassMeta} of this bean. + * <p> + * Returns <jk>null</jk> when this {@link BeanMeta} was constructed via the commons-side path + * ({@link #of(Class, BeanConfigContext)}) — in that case {@link #getClassInfo()} carries the + * pure-reflection view of the bean class. + * + * @return The {@link ClassMeta} of this bean, or <jk>null</jk> for bean-modeling-only construction. */ public ClassMeta<T> getClassMeta() { return classMeta; } + /** + * Returns the {@link ClassInfo} of this bean. + * + * <p> + * Pure-reflection view of the bean class. Always non-null, regardless of construction path. Use this in + * preference to {@link #getClassMeta()} for any bean-modeling read that does not depend on marshalling-aware + * type metadata; the commons-side construction path leaves {@link #getClassMeta()} <jk>null</jk>. + * + * @return The class info for this bean. Never <jk>null</jk>. + */ + public ClassInfo getClassInfo() { return classInfo; } + + /** + * Returns the bean-modeling configuration snapshot used to build this {@link BeanMeta}. + * + * <p> + * For marshalling-side construction, this is sourced from {@link MarshallingContext#getBeanConfigContext()}. + * For commons-side construction (via {@link #of(Class, BeanConfigContext)}), this is the {@link BeanConfigContext} + * passed to the factory. Always non-null. + * + * @return The bean-modeling configuration. Never <jk>null</jk>. + */ + public BeanConfigContext getConfig() { return config; } + /** * Returns the dictionary name for this bean as defined through the {@link Bean#typeName() @Marshalled(typeName)} annotation. * @@ -922,7 +1017,12 @@ public class BeanMeta<T> { /** * Returns the bean context that created this metadata object. * - * @return The bean context. + * <p> + * Returns <jk>null</jk> when this {@link BeanMeta} was constructed via the commons-side path + * ({@link #of(Class, BeanConfigContext)}). Use {@link #getConfig()} for the always-non-null bean-modeling + * configuration facade. + * + * @return The bean context, or <jk>null</jk> for bean-modeling-only construction. */ protected MarshallingContext getMarshallingContext() { return marshallingContext; } @@ -1086,7 +1186,7 @@ public class BeanMeta<T> { "unchecked" // Unchecked casts required for factory class and BeanStore result }) private org.apache.juneau.commons.function.BeanFactory resolveFactory(Class<? extends org.apache.juneau.commons.function.BeanFactory> fc) { - var bs = marshallingContext.getBeanStore(); + var bs = config.getBeanStore(); if (bs != null) { var opt = bs.getBean(fc); if (opt.isPresent()) @@ -1135,8 +1235,8 @@ public class BeanMeta<T> { "java:S3776" // Cognitive complexity acceptable for constructor finding logic }) private BeanConstructor findBeanConstructor() { - var ap = marshallingContext.getAnnotationProvider(); - var vis = marshallingContext.getBeanConstructorVisibility(); + var ap = config.getAnnotationProvider(); + var vis = config.getBeanConstructorVisibility(); var ci = classInfo; var l = ci.getPublicConstructors().stream().filter(x -> ap.has(BeanCtor.class, x)).toList(); @@ -1219,9 +1319,9 @@ public class BeanMeta<T> { * @return A collection of all bean fields found in the class hierarchy. */ private Collection<FieldInfo> findBeanFields() { - var v = marshallingContext.getBeanFieldVisibility(); - var noIgnoreTransients = ! marshallingContext.isIgnoreTransientFields(); - var ap = marshallingContext.getAnnotationProvider(); + var v = config.getBeanFieldVisibility(); + var noIgnoreTransients = ! config.isIgnoreTransientFields(); + var ap = config.getAnnotationProvider(); var isRecord = classInfo.isRecord(); var recordComponentNames = isRecord ? classInfo.getRecordComponents().stream().map(java.lang.reflect.RecordComponent::getName).collect(java.util.stream.Collectors.toSet()) @@ -1288,10 +1388,10 @@ public class BeanMeta<T> { }) private List<BeanMethod> findBeanMethods() { var l = new LinkedList<BeanMethod>(); - var ap = marshallingContext.getAnnotationProvider(); + var ap = config.getAnnotationProvider(); var ci = classInfo; - var v = marshallingContext.getBeanMethodVisibility(); - var pn = opt(beanFilter).map(x -> x.getPropertyNamer()).orElse(marshallingContext.getPropertyNamer()); + var v = config.getBeanMethodVisibility(); + var pn = opt(beanFilter).map(x -> x.getPropertyNamer()).orElse(config.getPropertyNamer()); var suppressedFromBeanIgnoredFields = findSuppressedPropertyNamesFromIgnoredFields(pn); classHierarchy.get().stream().forEach(c2 -> { @@ -1422,11 +1522,17 @@ public class BeanMeta<T> { * no dictionary classes are found. */ private BeanRegistry findBeanRegistry() { + // BeanRegistry is a marshalling-side concern (polymorphic dispatch via @Marshalled(typeName)/dictionary). + // On the commons-side construction path there is no MarshallingContext to drive it — return null and let + // callers route through getPropertyBeanRegistry(...) (which is also empty on this path). + if (marshallingContext == null) + return null; + // Bean dictionary on bean filter. var beanDictionaryClasses = opt(beanFilter).map(x -> new ArrayList<>(x.getBeanDictionary())).orElse(new ArrayList<>()); // Bean dictionary from @Marshalled(typeName) annotation. - var ba = marshallingContext.getAnnotationProvider().find(Marshalled.class, classInfo); + var ba = config.getAnnotationProvider().find(Marshalled.class, classInfo); ba.stream().map(x -> x.inner().typeName()).filter(Utils::ne).findFirst().ifPresent(x -> beanDictionaryClasses.add(classInfo)); return new BeanRegistry(marshallingContext, null, beanDictionaryClasses); @@ -1539,22 +1645,27 @@ public class BeanMeta<T> { return s; } - var n = classInfo - .getParentsAndInterfaces() - .stream() - .skip(1) - .map(marshallingContext::getClassMeta) - .map(ClassMeta::getBeanRegistry) - .filter(Objects::nonNull) - .map(x -> x.getTypeName(rawClass)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - - if (n != null) - return n; + // Parent-class BeanRegistry lookup is a marshalling-side concern: it walks parents and interfaces to + // see if any of THEIR ClassMeta-backed BeanRegistries declares a typeName for our raw class. On the + // commons-side path (no marshallingContext) we skip this entirely. + if (nn(marshallingContext)) { + var n = classInfo + .getParentsAndInterfaces() + .stream() + .skip(1) + .map(marshallingContext::getClassMeta) + .map(ClassMeta::getBeanRegistry) + .filter(Objects::nonNull) + .map(x -> x.getTypeName(rawClass)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (n != null) + return n; + } - return marshallingContext.getAnnotationProvider().find(Marshalled.class, classInfo) + return config.getAnnotationProvider().find(Marshalled.class, classInfo) .stream() .map(AnnotationInfo::inner) .filter(x -> ! x.typeName().isEmpty()) @@ -1600,7 +1711,7 @@ public class BeanMeta<T> { }) private Set<String> findSuppressedPropertyNamesFromIgnoredFields(PropertyNamer propertyNamer) { var s = new HashSet<String>(); - var ap = marshallingContext.getAnnotationProvider(); + var ap = config.getAnnotationProvider(); for (var c2 : classHierarchy.get()) { for (var x : c2.getDeclaredFields()) { if (! x.isNotStatic() || ! ap.has(BeanIgnore.class, x)) @@ -1656,8 +1767,8 @@ public class BeanMeta<T> { * in the class hierarchy. */ private Optional<FieldInfo> findInnerBeanField(String name) { - var noIgnoreTransients = ! marshallingContext.isIgnoreTransientFields(); - var ap = marshallingContext.getAnnotationProvider(); + var noIgnoreTransients = ! config.isIgnoreTransientFields(); + var ap = config.getAnnotationProvider(); // @formatter:off return classHierarchy.get().stream() 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 357993e386..22cf098fd6 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 @@ -85,14 +85,15 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { */ public static class Builder { BeanMeta<?> beanMeta; // Package-private for BeanMeta access - MarshallingContext bc; // Package-private for BeanMeta access + MarshallingContext bc; // Package-private for BeanMeta access. Null when the owning BeanMeta was built via the commons-side path. + BeanConfigContext config; // Package-private for BeanMeta access. Always non-null — sourced from the owning BeanMeta. String name; // Package-private for BeanMeta access FieldInfo field; // Package-private for BeanMeta access FieldInfo innerField; // Package-private for BeanMeta access 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) + ClassMeta<?> rawTypeMeta; // Package-private for BeanMeta access (used to install swap-aware transforms). Null on commons-side path (no type resolution). 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. @@ -113,6 +114,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { Builder(BeanMeta<?> beanMeta, String name) { this.beanMeta = beanMeta; this.bc = beanMeta.getMarshallingContext(); + this.config = beanMeta.getConfig(); this.name = name; } @@ -205,11 +207,19 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { * {@link MarshallingContext}, allowing callers from the bean-modeling layer to seed the type without * holding a {@link ClassMeta} reference. * + * <p> + * When the owning {@link BeanMeta} was built via the commons-side path + * ({@link BeanMeta#of(Class, BeanConfigContext)}), no marshalling context is available, so + * {@code rawTypeMeta}/{@code typeMeta} are left <jk>null</jk> and the property runs in raw-reflection mode. + * * @param value The raw metadata type for this bean property. * @return This object. */ public Builder rawMetaType(Class<?> value) { - return rawMetaType(bc.getClassMeta(assertArgNotNull(ARG_value, value))); + assertArgNotNull(ARG_value, value); + if (bc == null) + return this; + return rawMetaType(bc.getClassMeta(value)); } private static ObjectSwap marshalledPropSwap(AnnotationInfo<MarshalledProp> ai) { @@ -342,8 +352,15 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { * side-map keyed by the built {@link BeanPropertyMeta}. The bean-modeling layer itself no longer carries a * {@link BeanRegistry} reference. * - * @param bc The bean context. - * @param typeVarImpls Type variable implementations. + * <p> + * When {@code bc} is <jk>null</jk> (commons-side path), this method runs in raw-reflection mode: + * annotation reads are routed through {@link BeanConfigContext#getAnnotationProvider()} but no + * {@link ClassMeta} resolution or {@link ObjectSwap} discovery is performed; the property's + * {@code rawTypeMeta}/{@code typeMeta} stay <jk>null</jk> and the resulting {@link BeanPropertyMeta} + * exposes raw getter/setter invocation only. + * + * @param bc The bean context. May be <jk>null</jk> for the commons-side path. + * @param typeVarImpls Type variable implementations. Ignored when {@code bc} is <jk>null</jk>. * @param bpro Bean properties read-only set. * @param bpwo Bean properties write-only set. * @return <jk>true</jk> if this property is valid, <jk>false</jk> otherwise. @@ -357,12 +374,13 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { public boolean validate(MarshallingContext bc, TypeVariables typeVarImpls, Set<String> bpro, Set<String> bpwo) throws Exception { var bdClasses = list(); - var ap = bc.getAnnotationProvider(); + var ap = nn(bc) ? bc.getAnnotationProvider() : config.getAnnotationProvider(); if (field == null && getter == null && setter == null) return false; - if (field == null && setter == null && bc.isBeansRequireSettersForGetters() && ! isConstructorArg) + // Settings reads route through BeanConfigContext so the commons-side path works without a MarshallingContext. + if (field == null && setter == null && config.isBeansRequireSettersForGetters() && ! isConstructorArg) return false; canRead |= (nn(field) || nn(getter)); @@ -375,7 +393,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (nn(innerField)) { var lbp = ap.find(BeanProp.class, ifi); var lmp = ap.find(MarshalledProp.class, ifi); - if (nn(field) || ne(lbp)) { + if (nn(bc) && (nn(field) || ne(lbp))) { // Only use field type if it's a bean property or has @BeanProp annotation. // Otherwise, we want to infer the type from the getter or setter. rawTypeMeta = bc.resolveClassMeta(opt(last(lbp)).orElse(null), innerField.getFieldType(), typeVarImpls); @@ -403,9 +421,11 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (nn(getter)) { var lbp = ap.find(BeanProp.class, gi); var lmp = ap.find(MarshalledProp.class, gi); - if (rawTypeMeta == null) + if (nn(bc) && rawTypeMeta == null) rawTypeMeta = bc.resolveClassMeta(opt(last(lbp)).orElse(null), getter.getReturnType(), typeVarImpls); - isUri |= (rawTypeMeta.isUri() || ap.has(Uri.class, gi)); + if (nn(rawTypeMeta)) + isUri |= rawTypeMeta.isUri(); + isUri |= ap.has(Uri.class, gi); lbp.forEach(x -> { var beanp = x.inner(); if (ne(beanp.ro())) @@ -427,9 +447,11 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (nn(setter)) { var lbp = ap.find(BeanProp.class, si); var lmp = ap.find(MarshalledProp.class, si); - if (rawTypeMeta == null) + if (nn(bc) && rawTypeMeta == null) rawTypeMeta = bc.resolveClassMeta(opt(last(lbp)).orElse(null), setter.getParameterTypes().get(0), typeVarImpls); - isUri |= (rawTypeMeta.isUri() || ap.has(Uri.class, si)); + if (nn(rawTypeMeta)) + isUri |= rawTypeMeta.isUri(); + isUri |= ap.has(Uri.class, si); lbp.forEach(x -> { var beanp = x.inner(); if (ne(beanp.ro())) @@ -448,72 +470,75 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { ap.find(Swap.class, si).stream().forEach(x -> swap = swapSwap(x)); } - if (rawTypeMeta == null) + // On the commons-side path (bc == null), rawTypeMeta stays null and validate() accepts the property + // in raw-reflection mode. The marshalling-side path still requires a resolvable type. + if (nn(bc) && rawTypeMeta == null) return false; dictionaryClasses = bdClasses.stream().map(ReflectionUtils::info).toList(); isDyna = "*".equals(name); - // Do some annotation validation. - var ci = rawTypeMeta; - if (nn(getter)) { - var pt = getter.getParameterTypes(); - if (isDyna) { - if (ci.isAssignableTo(Map.class) && e(pt)) { - isDynaGetterMap = true; - } else if (pt.size() == 1 && pt.get(0).is(String.class)) { - // OK. + // Do some annotation validation. Type-aware validation requires rawTypeMeta — on the commons-side + // path we skip it entirely. + if (nn(rawTypeMeta)) { + var ci = rawTypeMeta; + if (nn(getter)) { + var pt = getter.getParameterTypes(); + if (isDyna) { + if (ci.isAssignableTo(Map.class) && e(pt)) { + isDynaGetterMap = true; + } else if (pt.size() == 1 && pt.get(0).is(String.class)) { + // OK. + } else { + return false; + } } else { - return false; + if (! ci.isAssignableTo(getter.getReturnType())) + return false; } - } else { - if (! ci.isAssignableTo(getter.getReturnType())) - return false; } - } - if (nn(setter)) { - var pt = setter.getParameterTypes(); - if (isDyna) { - if (pt.size() == 2 && pt.get(0).is(String.class)) { - // OK. + if (nn(setter)) { + var pt = setter.getParameterTypes(); + if (isDyna) { + if (pt.size() == 2 && pt.get(0).is(String.class)) { + // OK. + } else { + return false; + } } else { - return false; + if (pt.size() != 1 || ! ci.isAssignableTo(pt.get(0).inner())) + return false; } - } else { - if (pt.size() != 1 || ! ci.isAssignableTo(pt.get(0).inner())) - return false; } - } - if (nn(field)) { - if (isDyna) { - if (! field.getFieldType().isAssignableTo(Map.class)) - return false; - } else { - if (! ci.isAssignableTo(field.getFieldType())) - return false; + if (nn(field)) { + if (isDyna) { + if (! field.getFieldType().isAssignableTo(Map.class)) + return false; + } else { + if (! ci.isAssignableTo(field.getFieldType())) + return false; + } } - } - if (isDyna) { - rawTypeMeta = rawTypeMeta.getValueType(); - if (rawTypeMeta == null) - rawTypeMeta = bc.object(); - } - if (rawTypeMeta == null) - return false; + if (isDyna) { + rawTypeMeta = rawTypeMeta.getValueType(); + if (rawTypeMeta == null && nn(bc)) + rawTypeMeta = bc.object(); + } - if (typeMeta == null) { - if (nn(swap)) { - typeMeta = bc.getClassMeta(swap.getSwapClass()); - } else if (rawTypeMeta == null) { - typeMeta = bc.object(); - } else { - typeMeta = rawTypeMeta; + if (typeMeta == null) { + if (nn(swap) && nn(bc)) { + typeMeta = bc.getClassMeta(swap.getSwapClass()); + } else if (rawTypeMeta == null && nn(bc)) { + typeMeta = bc.object(); + } else { + typeMeta = rawTypeMeta; + } } + if (typeMeta == null) + typeMeta = rawTypeMeta; } - if (typeMeta == null) - typeMeta = rawTypeMeta; if (bpro.contains(name) || bpro.contains("*")) readOnly = true; @@ -536,9 +561,10 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { return new Builder(beanMeta, name); } - private final AnnotationProvider ap; // Annotation provider for finding annotations on this property. + private final AnnotationProvider ap; // Annotation provider for finding annotations on this property. Sourced from bc (marshalling-side) or beanMeta.getConfig() (commons-side). private final Supplier<List<AnnotationInfo<?>>> annotations; // Memoized list of all annotations on this property. - private final MarshallingContext bc; // The context that created this meta. + private final MarshallingContext bc; // The context that created this meta. Null when the owning BeanMeta was built via the commons-side path. + private final BeanConfigContext config; // Bean-modeling settings facade — always non-null. Mirrors the BeanMeta's config. private final BeanMeta<?> beanMeta; // The bean that this property belongs to. private final boolean canRead; // True if this property can be read. private final boolean canWrite; // True if this property can be written. @@ -571,6 +597,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { protected BeanPropertyMeta(Builder b) { annotations = memoize(this::findAnnotations); bc = b.bc; + config = b.config; beanMeta = b.beanMeta; canRead = b.canRead; canWrite = b.canWrite; @@ -594,7 +621,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { readTransform = b.readTransform != null ? b.readTransform : (session, o) -> o; writeTransform = b.writeTransform != null ? b.writeTransform : (session, o) -> o; - ap = bc.getAnnotationProvider(); + ap = nn(bc) ? bc.getAnnotationProvider() : b.config.getAnnotationProvider(); hashCode = h(beanMeta, name); } @@ -1005,12 +1032,12 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { return invokeGetter(bean, pName); } catch (Exception e) { - if (bc.isIgnoreInvocationExceptionsOnGetters()) { - if (rawTypeMeta.isPrimitive()) + if (nn(bc) && bc.isIgnoreInvocationExceptionsOnGetters()) { + if (nn(rawTypeMeta) && rawTypeMeta.isPrimitive()) return rawTypeMeta.getPrimitiveDefault(); return null; } - throw bex(e, beanMeta.getClassMeta(), "Exception occurred while getting property ''{0}''", name); + throw bex(e, beanMeta.getClassInfo(), "Exception occurred while getting property ''{0}''", name); } } @@ -1098,13 +1125,23 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { throw bex("Non-existent bean instance on bean."); } + // Raw-reflection path: when the owning BeanMeta was built via the commons-side path, rawTypeMeta and + // bc are null and there is no marshalling-aware type conversion to perform. Just invoke the setter + // (or write the field) directly. + if (rawTypeMeta == null) { + var bean = m.getBean(true); + var old = (nn(getter) || nn(field)) ? get(m, pName) : null; + invokeSetter(bean, pName, value1); + return old; + } + var isMap = rawTypeMeta.isMap(); var isCollection = rawTypeMeta.isCollection(); if ((! isDyna) && field == null && setter == null && ! (isMap || isCollection)) { - if ((value1 == null && bc.isIgnoreUnknownNullBeanProperties()) || bc.isIgnoreMissingSetters()) + if ((value1 == null && nn(bc) && bc.isIgnoreUnknownNullBeanProperties()) || config.isIgnoreMissingSetters()) return null; - throw bex(beanMeta.getClassMeta(), "Setter or public field not defined on property ''{0}''", name); + throw bex(beanMeta.getClassInfo(), "Setter or public field not defined on property ''{0}''", name); } var bean = m.getBean(true); // Don't use getBean() because it triggers array creation! @@ -1344,12 +1381,12 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { return swapAndFilterProperty(session, o); } catch (Exception e) { - if (bc.isIgnoreInvocationExceptionsOnGetters()) { - if (rawTypeMeta.isPrimitive()) + if (nn(bc) && bc.isIgnoreInvocationExceptionsOnGetters()) { + if (nn(rawTypeMeta) && rawTypeMeta.isPrimitive()) return rawTypeMeta.getPrimitiveDefault(); return null; } - throw bex(e, beanMeta.getClassMeta(), "Exception occurred while getting property ''{0}''", name); + throw bex(e, beanMeta.getClassInfo(), "Exception occurred while getting property ''{0}''", name); } } @@ -1388,14 +1425,14 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { } else if (nn(field)) m = (Map)field.get(bean); else - throw bex(beanMeta.getClassMeta(), MSG_getterOrFieldNotDefined, name); + throw bex(beanMeta.getClassInfo(), MSG_getterOrFieldNotDefined, name); return (m == null ? null : m.get(pName)); } if (nn(getter)) return getter.invoke(bean); if (nn(field)) return field.get(bean); - throw bex(beanMeta.getClassMeta(), MSG_getterOrFieldNotDefined, name); + throw bex(beanMeta.getClassInfo(), MSG_getterOrFieldNotDefined, name); } private Object invokeSetter(Object bean, String pName, Object val) throws IllegalArgumentException { @@ -1408,8 +1445,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { else if (nn(getter)) m = (Map<String,Object>)getter.invoke(bean); else - throw bex(beanMeta.getClassMeta(), "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is defined on this property, and the existing property value is null", - name, getClassMeta().getName(), cn(val)); + throw bex(beanMeta.getClassInfo(), "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is defined on this property, and the existing property value is null", + name, classNameForError(), cn(val)); return (m == null ? null : m.put(pName, val)); } if (nn(setter)) @@ -1418,8 +1455,15 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { field.set(bean, val); return null; } - throw bex(beanMeta.getClassMeta(), "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is defined on this property, and the existing property value is null", name, - getClassMeta().getName(), cn(val)); + throw bex(beanMeta.getClassInfo(), "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is defined on this property, and the existing property value is null", name, + classNameForError(), cn(val)); + } + + private String classNameForError() { + var cm = getClassMeta(); + if (nn(cm)) + return cm.getName(); + return beanMeta.getClassInfo().getName(); } /** diff --git a/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java b/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java new file mode 100644 index 0000000000..f20e24ecd1 --- /dev/null +++ b/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java @@ -0,0 +1,174 @@ +/* + * 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.commons.bean; + +import static org.junit.jupiter.api.Assertions.*; + +import org.apache.juneau.*; +import org.junit.jupiter.api.*; + +/** + * Tests the commons-side construction path of {@link BeanMeta} via + * {@link BeanMeta#of(Class, BeanConfigContext)}. + * + * <p> + * Verifies that property discovery and raw getter/setter invocation work without a + * {@code MarshallingContext}. All paths that touch the marshalling-aware fields + * ({@code classMeta}, {@code marshallingContext}, per-property {@code rawTypeMeta}) must + * gracefully fall through to bean-modeling-only behavior. + */ +class BeanMeta_Test extends TestBase { + + //==================================================================================================== + // Test POJO + //==================================================================================================== + + public static class A_Pojo { + private String x; + private int y; + public String getX() { return x; } + public void setX(String value) { x = value; } + public int getY() { return y; } + public void setY(int value) { y = value; } + } + + //==================================================================================================== + // Construction + //==================================================================================================== + + @Test + void a01_of_class_buildsBeanMeta() { + var bm = BeanMeta.of(A_Pojo.class); + assertNotNull(bm); + assertNotNull(bm.getClassInfo()); + assertNotNull(bm.getConfig()); + } + + @Test + void a02_of_classWithExplicitConfig_buildsBeanMeta() { + var bm = BeanMeta.of(A_Pojo.class, BeanConfigContext.DEFAULT); + assertNotNull(bm); + assertSame(BeanConfigContext.DEFAULT, bm.getConfig()); + } + + @Test + void a03_commonsConstructed_marshallingContextIsNull() { + var bm = BeanMeta.of(A_Pojo.class); + // The public protected accessor returns null on the commons-side path. + assertNull(bm.getClassMeta()); + } + + //==================================================================================================== + // Property discovery + //==================================================================================================== + + @Test + void b01_properties_discovered() { + var bm = BeanMeta.of(A_Pojo.class); + var props = bm.getProperties(); + assertNotNull(props); + assertTrue(props.containsKey("x")); + assertTrue(props.containsKey("y")); + assertEquals(2, props.size()); + } + + @Test + void b02_propertyMeta_hasGetterAndSetter() { + var bm = BeanMeta.of(A_Pojo.class); + var px = bm.getPropertyMeta("x"); + assertNotNull(px); + assertNotNull(px.getGetter()); + assertNotNull(px.getSetter()); + // rawTypeMeta is left null on the commons-side path — type resolution is a marshalling concern. + assertNull(px.getClassMeta()); + } + + //==================================================================================================== + // Raw getter/setter invocation via BeanPropertyMeta / BeanMap + //==================================================================================================== + + @Test + void c01_rawGet_returnsPropertyValue() { + var bm = BeanMeta.of(A_Pojo.class); + var p = new A_Pojo(); + p.setX("hello"); + p.setY(42); + + var map = BeanMap.of(p, bm); + var px = bm.getPropertyMeta("x"); + var py = bm.getPropertyMeta("y"); + + assertEquals("hello", px.get(map, "x")); + assertEquals(42, py.get(map, "y")); + } + + @Test + void c02_rawSet_updatesPropertyValue() { + var bm = BeanMeta.of(A_Pojo.class); + var p = new A_Pojo(); + var map = BeanMap.of(p, bm); + + bm.getPropertyMeta("x").set(map, "x", "world"); + bm.getPropertyMeta("y").set(map, "y", 99); + + assertEquals("world", p.getX()); + assertEquals(99, p.getY()); + } + + @Test + void c03_beanMap_get_put_roundTrip() { + var bm = BeanMeta.of(A_Pojo.class); + var p = new A_Pojo(); + var map = BeanMap.of(p, bm); + + map.put("x", "round-trip"); + map.put("y", 7); + + assertEquals("round-trip", map.get("x")); + assertEquals(7, map.get("y")); + assertEquals("round-trip", p.getX()); + assertEquals(7, p.getY()); + } + + @Test + void c04_getRaw_returnsRawPropertyValue() { + var bm = BeanMeta.of(A_Pojo.class); + var p = new A_Pojo(); + p.setX("raw"); + + var map = BeanMap.of(p, bm); + var px = bm.getPropertyMeta("x"); + assertEquals("raw", px.getRaw(map, "x")); + } + + //==================================================================================================== + // Custom BeanConfigContext (e.g. fluent setters, visibility tweaks) + //==================================================================================================== + + public static class D_FluentPojo { + private String name; + public String name() { return name; } + public D_FluentPojo name(String value) { name = value; return this; } + } + + @Test + void d01_findFluentSetters_discoversFluentProperties() { + var cfg = BeanConfigContext.create().findFluentSetters(true).build(); + var bm = BeanMeta.of(D_FluentPojo.class, cfg); + assertTrue(bm.getProperties().containsKey("name")); + } +} diff --git a/todo/TODO-5-bean-runtime-types-to-commons.md b/todo/TODO-5-bean-runtime-types-to-commons.md index f6cdd47a87..51f0589d83 100644 --- a/todo/TODO-5-bean-runtime-types-to-commons.md +++ b/todo/TODO-5-bean-runtime-types-to-commons.md @@ -4,7 +4,41 @@ This is the remaining work from **Phase 5 of the bean-layer split**. Phase 5a (t --- -## Status (as of Phase 5e checkpoint) +## Status (as of Phase 5f checkpoint) + +**Step 6 complete.** `BeanMeta` and `BeanPropertyMeta` can now be constructed without a `MarshallingContext`. Public surface added: +- `BeanMeta.of(Class<T>, BeanConfigContext)` and `BeanMeta.of(Class<T>)` static factories. +- `protected BeanMeta(Class<T>, BeanConfigContext)` constructor. +- `BeanMeta.getConfig()` and `BeanMeta.getClassInfo()` — always non-null accessors. +- `BeanMap.of(T, BeanMeta<T>)` static factory for pairing a bean with a commons-built `BeanMeta` (no session). + +Internal restructuring: +- Existing `BeanMeta(ClassMeta<T>, MarshalledFilter, String[], ClassInfo)` constructor preserved (still used by `ClassMeta.findBeanMeta()`/`BeanMetaFiltered`). Both constructors now delegate to a private all-args constructor that builds property metadata once. +- New `BeanMeta.config` field (non-null `BeanConfigContext`) is the source of truth for ALL settings reads — visibility, namers, ignores, `findFluentSetters`, `isUnsortedProperties`, `isBeansRequireDefaultConstructor`/`SomeProperties`, `getBeanTypePropertyName`, `getAnnotationProvider`, `getBeanStore`, `isUseJavaBeanIntrospector`, `isUseInterfaceProxies`, `isIgnoreTransientFields`. The marshalling-side path sources it from `marshallingContext.getBeanConfigContext()` (wired in Step 1); th [...] +- `BeanMeta.classMeta` and `BeanMeta.marshallingContext` are now documented-nullable. They stay non-null only on the marshalling-side construction path; commons-side construction leaves them null. `classInfo` and `config` are always non-null. +- `BeanMeta.findBeanRegistry()` returns `null` when `marshallingContext == null` (BeanRegistry is a marshalling-side concern). `BeanMeta.findDictionaryName()`'s parents/interfaces `marshallingContext::getClassMeta` stream is gated behind a non-null check. The synthetic `_type` property is built unconditionally (rawMetaType is a no-op when bc is null) but the side-map entry pairing it with the bean-level registry is skipped on the commons-side path. + +`BeanPropertyMeta` changes: +- `BeanPropertyMeta.Builder` carries both `MarshallingContext bc` (nullable on commons-side) and `BeanConfigContext config` (always non-null, sourced from the owning `BeanMeta`). +- `BeanPropertyMeta.Builder.rawMetaType(Class<?>)` no-ops when `bc == null` — leaves `rawTypeMeta` null and the property runs in raw-reflection mode. +- `BeanPropertyMeta.Builder.validate(...)`: annotation reads route through `bc.getAnnotationProvider()` when bc is non-null, falling back to `config.getAnnotationProvider()`. All `bc.resolveClassMeta(...)` / `bc.getClassMeta(...)` / `bc.object()` calls are guarded by `nn(bc)`. Type-aware validation (isAssignableTo checks for getter/setter/field type compatibility, isDyna value-type resolution) is wrapped in `if (nn(rawTypeMeta))`. The `if (rawTypeMeta == null) return false` bailout was r [...] +- `BeanPropertyMeta` instance carries the same `bc` (nullable) plus a non-null `config` field mirrored from the builder. The `ap` field is sourced from `bc.getAnnotationProvider()` when bc is non-null, else `config.getAnnotationProvider()`. +- `BeanPropertyMeta.getRaw(...)`: catch block guards `bc.isIgnoreInvocationExceptionsOnGetters()` and `rawTypeMeta.isPrimitive()` with null checks. +- `BeanPropertyMeta.getInner(...)`: same catch-block guards. Error message now uses `beanMeta.getClassInfo()` instead of `beanMeta.getClassMeta()` so it works on both construction paths. +- `BeanPropertyMeta.set(...)`: new early-return raw-reflection path when `rawTypeMeta == null` — calls `invokeSetter(bean, pName, value1)` directly after capturing the old value via `get(...)`. Skips all the `isMap/isCollection/setPropertyValue` machinery that depends on type metadata. +- `BeanPropertyMeta.set(...)` settings reads route through `config.isIgnoreMissingSetters()` and guard `bc.isIgnoreUnknownNullBeanProperties()` with non-null check. +- `BeanPropertyMeta.invokeGetter`/`invokeSetter` error messages: `beanMeta.getClassMeta()` → `beanMeta.getClassInfo()`. New private `classNameForError()` helper handles the type-name format (uses `getClassMeta().getName()` when non-null, else falls back to `classInfo.getName()`). + +`BeanMap` changes: +- `BeanMap.add(...)` and `BeanMap.put(...)`: `meta.getMarshallingContext().isIgnoreUnknownBeanProperties()` → `meta.getConfig().isIgnoreUnknownBeanProperties()`. Error-message argument switched from `meta.getClassMeta()` to `meta.getClassInfo()` for compatibility with commons-built `BeanMeta`. +- New `BeanMap.of(T bean, BeanMeta<T> meta)` static factory — the commons-side counterpart to `BeanMap.of(T bean)` (which still routes through `MarshallingContext.DEFAULT_SESSION.toBeanMap(bean)`). + +Test coverage: new `BeanMeta_Test` at `juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java` covers 10 scenarios — `BeanMeta.of(Class)` / `BeanMeta.of(Class, BeanConfigContext.DEFAULT)` factories, property discovery, raw getter/setter invocation via `BeanPropertyMeta.get/set/getRaw`, `BeanMap.get/put` round-trips, and a custom `BeanConfigContext` with `findFluentSetters` enabled. Verifies `getClassMeta() == null` on commons-built `BeanMeta` and `getClassInfo() != [...] + +Known limitations of the commons-side path (acceptable for Step 6, scoped for later cleanup): +- `BeanPropertyMeta.set` short-circuits to raw setter invocation when `rawTypeMeta == null` — no type conversion, no collection/map setter-fallback, no `swap.unswap` path. Bean-modeling-only callers should pass values already of the property type. +- `BeanPropertyMeta.add(BeanMap, String, Object)` / `add(BeanMap, String, String, Object)` (Collection/array/Map helpers) still read `rawTypeMeta` unconditionally — calling them on a commons-built `BeanPropertyMeta` will NPE. Same applies to `applyChildPropertiesFilter`, `setArray`, and the second half of `setPropertyValue`. None of these paths are reached by the test, and none are needed for the bean-modeling minimum. +- `MarshalledProp` swap detection in `validate(...)` still runs even when bc is null (it doesn't require a `MarshallingContext`), but no `installSwapAwareTransforms` is invoked since the marshalling-side install helper short-circuits when `rawTypeMeta == null`. **Step 5 complete.** `BeanPropertyMeta` no longer carries a `BeanRegistry` field. Picked **Option B** (side-map keyed by `BeanPropertyMeta`) — there are only 3 marshalling-side call sites that read the per-property registry (`ParserSession.getClassMeta(...)`, `SerializerSession.getBeanTypeName(...)`, `XmlParserSession.parseIntoMap-mixed-content`), and they all use the existing `pMeta.getBeanRegistry()` public accessor. The accessor stays as a backwards-compat delegate that routes to the [...] @@ -17,13 +51,13 @@ This is the remaining work from **Phase 5 of the bean-layer split**. Phase 5a (t - [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 [...] - [x] **Step 4** — Removed `MarshallingSession` from the `BeanMap` constructor signature (Option c — transitional setter). `BeanMap` now exposes `setMarshallingSession(MarshallingSession)` that the marshalling layer wires in immediately after construction. The `session` field defaults to null on direct construction; only `BeanMap.getBean()` (for read-only beans with constructor args), `BeanPropertyMeta.add`/`set`, and child-properties-filter operations need it set, and they all go throug [...] - [x] **Step 5** — Removed `BeanRegistry` field from `BeanPropertyMeta`. Picked **Option B** — side-map keyed by `BeanPropertyMeta` lives on `BeanMeta` (`Map<BeanPropertyMeta,BeanRegistry> propertyBeanRegistries`). Per-property registries are constructed by `BeanMeta` after `v.build()` from the builder's package-private `dictionaryClasses` field (populated during `Builder.validate(...)`). `Builder.beanRegistry(...)` public setter, the `beanRegistry` builder field, and the `parentBeanRegi [...] -- [ ] **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. +- [x] **Step 6** — `BeanMeta.of(Class<T>, BeanConfigContext)` factory + `protected BeanMeta(Class<T>, BeanConfigContext)` constructor wired up. `BeanMeta` now carries a non-null `BeanConfigContext config` facade for all settings reads; the `marshallingContext` and `classMeta` fields are documented-nullable and stay null on the commons-side path. `BeanPropertyMeta.Builder.bc` and `BeanPropertyMeta.bc` similarly nullable; new mirrored `config` field on both. `Builder.validate(...)` accepts [...] - [ ] **Step 7** — Re-check whether `ExtendedBeanMeta` and per-format extensions (`XmlBeanMeta`, `RdfBeanMeta`, `HtmlBeanMeta`) need to follow `BeanMeta` to `commons.bean`. Default expectation: they stay in `juneau-marshall`. - [ ] **Step 8** — `git mv` the eight runtime types into `juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/`. Verify `juneau-commons` still compiles standalone (`cd juneau-core/juneau-commons && mvn clean compile`). - [ ] **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 6 is the recommended next checkpoint** — Step 5 dropped the `BeanRegistry` field from `BeanPropertyMeta` (now on a `BeanMeta` side-map) and lifted two of the surviving `this.classMeta` references in `BeanMeta.findDictionaryName(...)` to `classInfo.inner()`. The remaining marshalling coupling on `BeanMeta` itself: (a) the `marshallingContext` field is rea [...] +The "incomplete-but-documented over broken-build" rule from Phase 5a still applies. When picking up the next slice of this work, **Step 7 is the recommended next checkpoint** — Step 6 wired the commons-side construction path through `BeanMeta.of(Class<T>, BeanConfigContext)`, made `marshallingContext`/`classMeta` nullable on `BeanMeta`, and made `bc`/`rawTypeMeta` nullable on `BeanPropertyMeta`. The marshalling-side construction path remains behaviorally identical: `BeanMetaFiltered` and [...] ---
