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 0ae599d2b2caff74ac7c5eee1e2429c8b7122e74 Author: James Bognar <[email protected]> AuthorDate: Tue May 12 11:20:37 2026 -0400 refactor: introduce BeanConfigContext foundation for bean-runtime move (Phase 5b of bean-layer split) Adds BeanConfigContext, the runtime sibling of @BeanConfig, in org.apache.juneau.commons.bean. Carries the bean-modeling subset of runtime configuration (visibility thresholds, beans*Require* toggles, fluent-setter detection, property naming, bean-type property name, not-a-bean exclusion sets, BeanStore, AnnotationProvider, optional isNotABean predicate override) so the bean-modeling runtime can be consumed without referencing any marshalling-aware types. MarshallingContext now exposes a memoized getBeanConfigContext() returning a snapshot view, so future steps can migrate BeanMeta / BeanPropertyMeta off direct MarshallingContext access without the disruption of moving the eight runtime types in a single commit. This is Step 1 of TODO-5; the eight runtime types (BeanMap, BeanMeta, BeanPropertyMeta, BeanMapEntry, BeanMetaFiltered, BeanPropertyValue, BeanPropertyConsumer, BeanProxyInvocationHandler) still live in juneau-marshall and still couple to ClassMeta / ObjectSwap / BeanRegistry. TODO-5-bean-runtime-types-to-commons.md is updated to mark Step 1 done and describe the remaining steps. Tests: 100% instruction / 97% branch coverage on BeanConfigContext via juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanConfigContext_Test.java. Full juneau test suite passes; juneau-commons compiles standalone. Co-authored-by: Cursor <[email protected]> --- .../juneau/commons/bean/BeanConfigContext.java | 646 +++++++++++++++++++++ .../java/org/apache/juneau/MarshallingContext.java | 51 ++ .../commons/bean/BeanConfigContext_Test.java | 422 ++++++++++++++ todo/TODO-5-bean-runtime-types-to-commons.md | 19 + 4 files changed, 1138 insertions(+) diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanConfigContext.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanConfigContext.java new file mode 100644 index 0000000000..e9309fa9ee --- /dev/null +++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanConfigContext.java @@ -0,0 +1,646 @@ +/* + * 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.apache.juneau.commons.utils.AssertionUtils.*; +import static org.apache.juneau.commons.utils.CollectionUtils.*; +import static org.apache.juneau.commons.utils.Utils.*; + +import java.util.*; +import java.util.function.*; + +import org.apache.juneau.commons.inject.*; +import org.apache.juneau.commons.reflect.*; + +/** + * Immutable runtime configuration for the bean-modeling layer. + * + * <p> + * {@code BeanConfigContext} is the runtime sibling of the {@link BeanConfig @BeanConfig} annotation: + * it carries the resolved values that drive how Java types are introspected as beans, independent + * of any marshalling concern. Settings include visibility thresholds, required-property toggles, + * fluent-setter detection, property naming, not-bean exclusions, the active {@link BeanStore} and + * {@link AnnotationProvider}, and a few related hooks. + * + * <p> + * This type is the bean-modeling counterpart of the marshalling-layer context that lives in + * {@code juneau-marshall}. It exists so the bean-modeling runtime (in {@code commons.bean}) can + * be used independently of the marshalling stack, and so the marshalling layer can compose a + * {@code BeanConfigContext} snapshot to feed bean-modeling code without exposing marshalling-only + * APIs. + * + * <h5 class='section'>Construction:</h5> + * <p class='bjava'> + * <jc>// All defaults.</jc> + * BeanConfigContext <jv>defaults</jv> = BeanConfigContext.<jsf>DEFAULT</jsf>; + * + * <jc>// Customized.</jc> + * BeanConfigContext <jv>ctx</jv> = BeanConfigContext.<jsm>create</jsm>() + * .beanClassVisibility(Visibility.<jsf>PROTECTED</jsf>) + * .findFluentSetters(<jk>true</jk>) + * .propertyNamer(<jk>new</jk> PropertyNamerDLC()) + * .build(); + * + * <jc>// Copy and tweak.</jc> + * BeanConfigContext <jv>ctx2</jv> = <jv>ctx</jv>.copy() + * .unsortedProperties(<jk>true</jk>) + * .build(); + * </p> + * + * <h5 class='section'>See Also:</h5><ul> + * <li class='ja'>{@link BeanConfig @BeanConfig} — the annotation form. + * </ul> + */ +@SuppressWarnings({ + "java:S107" // Builder.build() invokes a multi-arg constructor; high cardinality is inherent to a configuration POJO. +}) +public final class BeanConfigContext { + + /** + * Default {@link BeanConfigContext} instance with all settings at their defaults. + * + * <p> + * Equivalent to {@code BeanConfigContext.create().build()}. + */ + public static final BeanConfigContext DEFAULT = create().build(); + + /** + * Creates a new builder for {@link BeanConfigContext}. + * + * @return A new builder. + */ + public static Builder create() { + return new Builder(); + } + + private final Visibility beanClassVisibility; + private final Visibility beanConstructorVisibility; + private final Visibility beanFieldVisibility; + private final Visibility beanMethodVisibility; + + private final boolean beansRequireDefaultConstructor; + private final boolean beansRequireSerializable; + private final boolean beansRequireSettersForGetters; + private final boolean beansRequireSomeProperties; + private final boolean findFluentSetters; + private final boolean ignoreMissingSetters; + private final boolean ignoreTransientFields; + private final boolean ignoreUnknownBeanProperties; + private final boolean unsortedProperties; + private final boolean useInterfaceProxies; + private final boolean useJavaBeanIntrospector; + + private final PropertyNamer propertyNamer; + private final String beanTypePropertyName; + + private final Set<String> notBeanPackageNames; + private final Set<String> notBeanPackagePrefixes; + private final Set<Class<?>> notBeanClasses; + + private final BeanStore beanStore; + private final AnnotationProvider annotationProvider; + private final Predicate<ClassInfo> notABeanPredicate; + + private BeanConfigContext(Builder b) { + beanClassVisibility = b.beanClassVisibility; + beanConstructorVisibility = b.beanConstructorVisibility; + beanFieldVisibility = b.beanFieldVisibility; + beanMethodVisibility = b.beanMethodVisibility; + beansRequireDefaultConstructor = b.beansRequireDefaultConstructor; + beansRequireSerializable = b.beansRequireSerializable; + beansRequireSettersForGetters = b.beansRequireSettersForGetters; + beansRequireSomeProperties = b.beansRequireSomeProperties; + findFluentSetters = b.findFluentSetters; + ignoreMissingSetters = b.ignoreMissingSetters; + ignoreTransientFields = b.ignoreTransientFields; + ignoreUnknownBeanProperties = b.ignoreUnknownBeanProperties; + unsortedProperties = b.unsortedProperties; + useInterfaceProxies = b.useInterfaceProxies; + useJavaBeanIntrospector = b.useJavaBeanIntrospector; + propertyNamer = b.propertyNamer; + beanTypePropertyName = b.beanTypePropertyName; + notBeanPackageNames = u(b.notBeanPackageNames); + notBeanPackagePrefixes = u(b.notBeanPackagePrefixes); + notBeanClasses = u(b.notBeanClasses); + beanStore = b.beanStore; + annotationProvider = b.annotationProvider; + notABeanPredicate = b.notABeanPredicate; + } + + /** + * Returns a builder pre-populated with the values from this context. + * + * @return A new builder. + */ + public Builder copy() { + return new Builder(this); + } + + /** + * Minimum bean class visibility. + * + * <p> + * Classes are not considered beans unless they meet this minimum visibility requirement. + * + * @return The minimum bean class visibility. Never <jk>null</jk>. + */ + public Visibility getBeanClassVisibility() { return beanClassVisibility; } + + /** + * Minimum bean constructor visibility. + * + * @return The minimum bean constructor visibility. Never <jk>null</jk>. + */ + public Visibility getBeanConstructorVisibility() { return beanConstructorVisibility; } + + /** + * Minimum bean field visibility. + * + * @return The minimum bean field visibility. Never <jk>null</jk>. + */ + public Visibility getBeanFieldVisibility() { return beanFieldVisibility; } + + /** + * Minimum bean method visibility. + * + * @return The minimum bean method visibility. Never <jk>null</jk>. + */ + public Visibility getBeanMethodVisibility() { return beanMethodVisibility; } + + /** + * Returns whether classes must have a no-arg constructor to be considered beans. + * + * @return <jk>true</jk> if a no-arg constructor is required. + */ + public boolean isBeansRequireDefaultConstructor() { return beansRequireDefaultConstructor; } + + /** + * Returns whether classes must implement {@link java.io.Serializable} to be considered beans. + * + * @return <jk>true</jk> if {@code Serializable} is required. + */ + public boolean isBeansRequireSerializable() { return beansRequireSerializable; } + + /** + * Returns whether bean properties must have a setter to be considered writable from the getter. + * + * @return <jk>true</jk> if setters are required for getters. + */ + public boolean isBeansRequireSettersForGetters() { return beansRequireSettersForGetters; } + + /** + * Returns whether classes must have at least one property to be considered beans. + * + * @return <jk>true</jk> if at least one property is required. + */ + public boolean isBeansRequireSomeProperties() { return beansRequireSomeProperties; } + + /** + * Returns whether fluent-style setters (returning <c>this</c>) should be detected. + * + * @return <jk>true</jk> if fluent setters are detected. + */ + public boolean isFindFluentSetters() { return findFluentSetters; } + + /** + * Returns whether bean properties without setters should be silently ignored during deserialization. + * + * @return <jk>true</jk> if missing setters are ignored. + */ + public boolean isIgnoreMissingSetters() { return ignoreMissingSetters; } + + /** + * Returns whether {@code transient} fields should be excluded from bean property detection. + * + * @return <jk>true</jk> if transient fields are ignored. + */ + public boolean isIgnoreTransientFields() { return ignoreTransientFields; } + + /** + * Returns whether unknown properties on incoming bean payloads should be ignored. + * + * @return <jk>true</jk> if unknown properties are ignored. + */ + public boolean isIgnoreUnknownBeanProperties() { return ignoreUnknownBeanProperties; } + + /** + * Returns whether properties should preserve their JVM-discovered (non-alphabetical) order. + * + * @return <jk>true</jk> if properties remain unsorted. + */ + public boolean isUnsortedProperties() { return unsortedProperties; } + + /** + * Returns whether interface proxies should be created for bean interfaces. + * + * @return <jk>true</jk> if interface proxies are enabled. + */ + public boolean isUseInterfaceProxies() { return useInterfaceProxies; } + + /** + * Returns whether {@link java.beans.Introspector} should be used to discover bean properties. + * + * @return <jk>true</jk> if the JavaBeans introspector is used. + */ + public boolean isUseJavaBeanIntrospector() { return useJavaBeanIntrospector; } + + /** + * Returns the {@link PropertyNamer} used to derive property names from getter/setter/field names. + * + * @return The active property namer. Never <jk>null</jk>. + */ + public PropertyNamer getPropertyNamer() { return propertyNamer; } + + /** + * Returns the property name used to embed the bean dictionary type name (default: <js>"_type"</js>). + * + * @return The bean type property name. Never <jk>null</jk>. + */ + public String getBeanTypePropertyName() { return beanTypePropertyName; } + + /** + * Returns the set of fully qualified package names whose classes are excluded from bean detection. + * + * @return The not-a-bean package name set. Never <jk>null</jk>. Unmodifiable. + */ + public Set<String> getNotBeanPackageNames() { return notBeanPackageNames; } + + /** + * Returns the set of fully qualified package prefixes whose classes are excluded from bean detection. + * + * @return The not-a-bean package prefix set. Never <jk>null</jk>. Unmodifiable. + */ + public Set<String> getNotBeanPackagePrefixes() { return notBeanPackagePrefixes; } + + /** + * Returns the set of classes (and supertypes) that are explicitly excluded from bean detection. + * + * @return The not-a-bean class set. Never <jk>null</jk>. Unmodifiable. + */ + public Set<Class<?>> getNotBeanClasses() { return notBeanClasses; } + + /** + * Returns the active {@link BeanStore} used for factory-based bean instantiation, or <jk>null</jk> if none configured. + * + * @return The bean store, or <jk>null</jk>. + */ + public BeanStore getBeanStore() { return beanStore; } + + /** + * Returns the {@link AnnotationProvider} used to discover annotations on classes/methods/fields/parameters. + * + * @return The annotation provider. Never <jk>null</jk>. + */ + public AnnotationProvider getAnnotationProvider() { return annotationProvider; } + + /** + * Returns <jk>true</jk> if the specified class is excluded from bean detection. + * + * <p> + * The default implementation checks {@link #getNotBeanClasses()}, {@link #getNotBeanPackageNames()}, and + * {@link #getNotBeanPackagePrefixes()}, and rejects arrays/primitives/enums/annotations. A custom + * predicate (set via {@link Builder#notABeanPredicate(Predicate)}) overrides this behavior entirely. + * + * @param ci The class info being tested. Must not be <jk>null</jk>. + * @return <jk>true</jk> if the class is excluded from bean detection. + */ + public boolean isNotABean(ClassInfo ci) { + assertArgNotNull("ci", ci); + if (notABeanPredicate != null) + return notABeanPredicate.test(ci); + if (ci.isArray() || ci.isPrimitive() || ci.isEnum() || ci.isAnnotation()) + return true; + var p = ci.getPackage(); + if (nn(p)) { + var pn = p.getName(); + for (var p2 : notBeanPackageNames) + if (pn.equals(p2)) + return true; + for (var p2 : notBeanPackagePrefixes) + if (pn.startsWith(p2)) + return true; + } + for (var exclude : notBeanClasses) + if (ci.isAssignableTo(exclude)) + return true; + return false; + } + + /** + * Builder for {@link BeanConfigContext}. + */ + public static final class Builder { + + private Visibility beanClassVisibility = Visibility.PUBLIC; + private Visibility beanConstructorVisibility = Visibility.PUBLIC; + private Visibility beanFieldVisibility = Visibility.PUBLIC; + private Visibility beanMethodVisibility = Visibility.PUBLIC; + + private boolean beansRequireDefaultConstructor; + private boolean beansRequireSerializable; + private boolean beansRequireSettersForGetters; + private boolean beansRequireSomeProperties = true; + private boolean findFluentSetters; + private boolean ignoreMissingSetters = true; + private boolean ignoreTransientFields = true; + private boolean ignoreUnknownBeanProperties; + private boolean unsortedProperties; + private boolean useInterfaceProxies = true; + private boolean useJavaBeanIntrospector; + + private PropertyNamer propertyNamer = new BasicPropertyNamer(); + private String beanTypePropertyName = "_type"; + + private Set<String> notBeanPackageNames = new LinkedHashSet<>(); + private Set<String> notBeanPackagePrefixes = new LinkedHashSet<>(); + private Set<Class<?>> notBeanClasses = new LinkedHashSet<>(); + + private BeanStore beanStore; + private AnnotationProvider annotationProvider = AnnotationProvider.INSTANCE; + private Predicate<ClassInfo> notABeanPredicate; + + private Builder() {} + + private Builder(BeanConfigContext src) { + beanClassVisibility = src.beanClassVisibility; + beanConstructorVisibility = src.beanConstructorVisibility; + beanFieldVisibility = src.beanFieldVisibility; + beanMethodVisibility = src.beanMethodVisibility; + beansRequireDefaultConstructor = src.beansRequireDefaultConstructor; + beansRequireSerializable = src.beansRequireSerializable; + beansRequireSettersForGetters = src.beansRequireSettersForGetters; + beansRequireSomeProperties = src.beansRequireSomeProperties; + findFluentSetters = src.findFluentSetters; + ignoreMissingSetters = src.ignoreMissingSetters; + ignoreTransientFields = src.ignoreTransientFields; + ignoreUnknownBeanProperties = src.ignoreUnknownBeanProperties; + unsortedProperties = src.unsortedProperties; + useInterfaceProxies = src.useInterfaceProxies; + useJavaBeanIntrospector = src.useJavaBeanIntrospector; + propertyNamer = src.propertyNamer; + beanTypePropertyName = src.beanTypePropertyName; + notBeanPackageNames = new LinkedHashSet<>(src.notBeanPackageNames); + notBeanPackagePrefixes = new LinkedHashSet<>(src.notBeanPackagePrefixes); + notBeanClasses = new LinkedHashSet<>(src.notBeanClasses); + beanStore = src.beanStore; + annotationProvider = src.annotationProvider; + notABeanPredicate = src.notABeanPredicate; + } + + /** + * Builds a new {@link BeanConfigContext} from this builder's state. + * + * @return A new immutable {@link BeanConfigContext}. + */ + public BeanConfigContext build() { + return new BeanConfigContext(this); + } + + /** + * Sets the minimum bean class visibility. + * + * @param value The visibility threshold. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder beanClassVisibility(Visibility value) { beanClassVisibility = assertArgNotNull("value", value); return this; } + + /** + * Sets the minimum bean constructor visibility. + * + * @param value The visibility threshold. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder beanConstructorVisibility(Visibility value) { beanConstructorVisibility = assertArgNotNull("value", value); return this; } + + /** + * Sets the minimum bean field visibility. + * + * @param value The visibility threshold. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder beanFieldVisibility(Visibility value) { beanFieldVisibility = assertArgNotNull("value", value); return this; } + + /** + * Sets the minimum bean method visibility. + * + * @param value The visibility threshold. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder beanMethodVisibility(Visibility value) { beanMethodVisibility = assertArgNotNull("value", value); return this; } + + /** + * Toggles the requirement that beans have a no-arg default constructor. + * + * @param value The new value. + * @return This object. + */ + public Builder beansRequireDefaultConstructor(boolean value) { beansRequireDefaultConstructor = value; return this; } + + /** + * Toggles the requirement that beans implement {@link java.io.Serializable}. + * + * @param value The new value. + * @return This object. + */ + public Builder beansRequireSerializable(boolean value) { beansRequireSerializable = value; return this; } + + /** + * Toggles the requirement that bean getters have matching setters. + * + * @param value The new value. + * @return This object. + */ + public Builder beansRequireSettersForGetters(boolean value) { beansRequireSettersForGetters = value; return this; } + + /** + * Toggles the requirement that beans expose at least one property. + * + * @param value The new value. + * @return This object. + */ + public Builder beansRequireSomeProperties(boolean value) { beansRequireSomeProperties = value; return this; } + + /** + * Toggles fluent-setter detection (setters that return <c>this</c>). + * + * @param value The new value. + * @return This object. + */ + public Builder findFluentSetters(boolean value) { findFluentSetters = value; return this; } + + /** + * Toggles silent ignoring of properties without setters during deserialization. + * + * @param value The new value. + * @return This object. + */ + public Builder ignoreMissingSetters(boolean value) { ignoreMissingSetters = value; return this; } + + /** + * Toggles exclusion of {@code transient} fields from bean property detection. + * + * @param value The new value. + * @return This object. + */ + public Builder ignoreTransientFields(boolean value) { ignoreTransientFields = value; return this; } + + /** + * Toggles silent ignoring of unknown properties on incoming bean payloads. + * + * @param value The new value. + * @return This object. + */ + public Builder ignoreUnknownBeanProperties(boolean value) { ignoreUnknownBeanProperties = value; return this; } + + /** + * Toggles whether properties remain in JVM-discovered (non-alphabetical) order. + * + * @param value The new value. + * @return This object. + */ + public Builder unsortedProperties(boolean value) { unsortedProperties = value; return this; } + + /** + * Toggles automatic creation of interface proxies for bean interfaces. + * + * @param value The new value. + * @return This object. + */ + public Builder useInterfaceProxies(boolean value) { useInterfaceProxies = value; return this; } + + /** + * Toggles use of {@link java.beans.Introspector} for property discovery. + * + * @param value The new value. + * @return This object. + */ + public Builder useJavaBeanIntrospector(boolean value) { useJavaBeanIntrospector = value; return this; } + + /** + * Sets the {@link PropertyNamer} used to derive property names. + * + * @param value The property namer. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder propertyNamer(PropertyNamer value) { propertyNamer = assertArgNotNull("value", value); return this; } + + /** + * Sets the property name used to embed the bean dictionary type (default: <js>"_type"</js>). + * + * @param value The property name. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder beanTypePropertyName(String value) { beanTypePropertyName = assertArgNotNull("value", value); return this; } + + /** + * Adds package names whose classes should be excluded from bean detection. + * + * @param values The package names. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder notBeanPackageNames(String...values) { + assertArgNotNull("values", values); + Collections.addAll(notBeanPackageNames, values); + return this; + } + + /** + * Adds package prefixes whose classes should be excluded from bean detection. + * + * @param values The package prefixes. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder notBeanPackagePrefixes(String...values) { + assertArgNotNull("values", values); + Collections.addAll(notBeanPackagePrefixes, values); + return this; + } + + /** + * Adds classes (and supertypes) that should be excluded from bean detection. + * + * @param values The classes. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder notBeanClasses(Class<?>...values) { + assertArgNotNull("values", values); + Collections.addAll(notBeanClasses, values); + return this; + } + + /** + * Replaces the not-a-bean package name set. + * + * @param values The package names. May be <jk>null</jk> for an empty set. + * @return This object. + */ + public Builder notBeanPackageNames(Collection<String> values) { + notBeanPackageNames = new LinkedHashSet<>(values == null ? sete() : values); + return this; + } + + /** + * Replaces the not-a-bean package prefix set. + * + * @param values The package prefixes. May be <jk>null</jk> for an empty set. + * @return This object. + */ + public Builder notBeanPackagePrefixes(Collection<String> values) { + notBeanPackagePrefixes = new LinkedHashSet<>(values == null ? sete() : values); + return this; + } + + /** + * Replaces the not-a-bean class set. + * + * @param values The classes. May be <jk>null</jk> for an empty set. + * @return This object. + */ + public Builder notBeanClasses(Collection<? extends Class<?>> values) { + notBeanClasses = new LinkedHashSet<>(values == null ? sete() : values); + return this; + } + + /** + * Sets the active {@link BeanStore} used for factory-based bean instantiation. + * + * @param value The bean store. May be <jk>null</jk>. + * @return This object. + */ + public Builder beanStore(BeanStore value) { beanStore = value; return this; } + + /** + * Sets the active {@link AnnotationProvider}. + * + * @param value The annotation provider. Must not be <jk>null</jk>. + * @return This object. + */ + public Builder annotationProvider(AnnotationProvider value) { annotationProvider = assertArgNotNull("value", value); return this; } + + /** + * Installs a custom predicate that fully overrides {@link BeanConfigContext#isNotABean(ClassInfo)}. + * + * <p> + * When set, the default not-bean computation (package/class exclusions, array/primitive/enum/annotation + * checks) is bypassed entirely. Pass <jk>null</jk> to revert to the built-in behavior. + * + * @param value The predicate. May be <jk>null</jk>. + * @return This object. + */ + public Builder notABeanPredicate(Predicate<ClassInfo> value) { notABeanPredicate = value; return this; } + } +} diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java index bfac213a84..cbf369aae2 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java @@ -3640,6 +3640,7 @@ public class MarshallingContext extends Context implements ConversionFinder { } private final NullableSupplier<WriterSerializer> beanToStringSerializer; + private final NullableSupplier<BeanConfigContext> beanConfigContext; private final BeanRegistry beanRegistry; private final MarshallingSession defaultSession; private final ConfigurableConverter converter; @@ -3757,6 +3758,40 @@ public class MarshallingContext extends Context implements ConversionFinder { beanRegistry = new BeanRegistry(this, null, list()); defaultSession = createSession().unmodifiable().build(); beanToStringSerializer = memoize(() -> Json5Serializer.create().marshallingContext(this).build()); + beanConfigContext = memoize(this::buildBeanConfigContext); + } + + /* + * Builds the {@link BeanConfigContext} snapshot exposed by {@link #getBeanConfigContext()}. + * + * Lifts the bean-modeling subset of this context's settings into a portable POJO that the bean-modeling + * runtime in {@code commons.bean} can consume without referencing any marshalling-aware types. + */ + private BeanConfigContext buildBeanConfigContext() { + return BeanConfigContext.create() + .beanClassVisibility(beanClassVisibility) + .beanConstructorVisibility(beanConstructorVisibility) + .beanFieldVisibility(beanFieldVisibility) + .beanMethodVisibility(beanMethodVisibility) + .beansRequireDefaultConstructor(beansRequireDefaultConstructor) + .beansRequireSerializable(beansRequireSerializable) + .beansRequireSettersForGetters(beansRequireSettersForGetters) + .beansRequireSomeProperties(beansRequireSomeProperties) + .findFluentSetters(findFluentSetters) + .ignoreMissingSetters(ignoreMissingSetters) + .ignoreTransientFields(ignoreTransientFields) + .ignoreUnknownBeanProperties(ignoreUnknownBeanProperties) + .unsortedProperties(unsortedProperties) + .useInterfaceProxies(useInterfaceProxies) + .useJavaBeanIntrospector(useJavaBeanIntrospector) + .propertyNamer(propertyNamerBean) + .beanTypePropertyName(typePropertyName) + .notBeanPackageNames(notBeanPackageNames) + .notBeanPackagePrefixes(notBeanPackagePrefixes) + .notBeanClasses(notBeanClasses.stream().map(ClassInfo::inner).toList()) + .beanStore(beanStore) + .annotationProvider(getAnnotationProvider()) + .build(); } /** @@ -3823,6 +3858,22 @@ public class MarshallingContext extends Context implements ConversionFinder { */ public final List<ClassInfo> getBeanDictionary() { return beanDictionary; } + /** + * Returns the {@link BeanConfigContext} snapshot for this marshalling context. + * + * <p> + * The snapshot lifts the bean-modeling subset of this context's settings into a portable POJO that the + * bean-modeling runtime in {@code commons.bean} can consume without referencing any marshalling-aware types. + * It is computed lazily on first access and cached for the lifetime of this context. + * + * <p> + * Mutating the returned snapshot's settings (via {@link BeanConfigContext#copy()} and a fresh build) does not + * affect this marshalling context. + * + * @return The bean-modeling configuration snapshot. Never <jk>null</jk>. + */ + public final BeanConfigContext getBeanConfigContext() { return beanConfigContext.get(); } + /** * Minimum bean field visibility. * diff --git a/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanConfigContext_Test.java b/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanConfigContext_Test.java new file mode 100644 index 0000000000..77e1e3a8bb --- /dev/null +++ b/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanConfigContext_Test.java @@ -0,0 +1,422 @@ +/* + * 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.apache.juneau.commons.reflect.ReflectionUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.commons.inject.*; +import org.apache.juneau.commons.reflect.*; +import org.junit.jupiter.api.*; + +class BeanConfigContext_Test extends TestBase { + + //==================================================================================================== + // Defaults / DEFAULT singleton + //==================================================================================================== + + @Test + void a01_default_visibilityIsPublic() { + var ctx = BeanConfigContext.DEFAULT; + assertEquals(Visibility.PUBLIC, ctx.getBeanClassVisibility()); + assertEquals(Visibility.PUBLIC, ctx.getBeanConstructorVisibility()); + assertEquals(Visibility.PUBLIC, ctx.getBeanFieldVisibility()); + assertEquals(Visibility.PUBLIC, ctx.getBeanMethodVisibility()); + } + + @Test + void a02_default_booleanToggles() { + var ctx = BeanConfigContext.DEFAULT; + assertFalse(ctx.isBeansRequireDefaultConstructor()); + assertFalse(ctx.isBeansRequireSerializable()); + assertFalse(ctx.isBeansRequireSettersForGetters()); + assertTrue(ctx.isBeansRequireSomeProperties()); + assertFalse(ctx.isFindFluentSetters()); + assertTrue(ctx.isIgnoreMissingSetters()); + assertTrue(ctx.isIgnoreTransientFields()); + assertFalse(ctx.isIgnoreUnknownBeanProperties()); + assertFalse(ctx.isUnsortedProperties()); + assertTrue(ctx.isUseInterfaceProxies()); + assertFalse(ctx.isUseJavaBeanIntrospector()); + } + + @Test + void a03_default_namingAndCollections() { + var ctx = BeanConfigContext.DEFAULT; + assertNotNull(ctx.getPropertyNamer()); + assertInstanceOf(BasicPropertyNamer.class, ctx.getPropertyNamer()); + assertEquals("_type", ctx.getBeanTypePropertyName()); + assertTrue(ctx.getNotBeanPackageNames().isEmpty()); + assertTrue(ctx.getNotBeanPackagePrefixes().isEmpty()); + assertTrue(ctx.getNotBeanClasses().isEmpty()); + } + + @Test + void a04_default_storeAndAnnotationProvider() { + var ctx = BeanConfigContext.DEFAULT; + assertNull(ctx.getBeanStore()); + assertNotNull(ctx.getAnnotationProvider()); + assertSame(AnnotationProvider.INSTANCE, ctx.getAnnotationProvider()); + } + + //==================================================================================================== + // Builder setters + //==================================================================================================== + + @Test + void b01_builder_visibilitySetters() { + var ctx = BeanConfigContext.create() + .beanClassVisibility(Visibility.PROTECTED) + .beanConstructorVisibility(Visibility.PRIVATE) + .beanFieldVisibility(Visibility.DEFAULT) + .beanMethodVisibility(Visibility.PROTECTED) + .build(); + assertEquals(Visibility.PROTECTED, ctx.getBeanClassVisibility()); + assertEquals(Visibility.PRIVATE, ctx.getBeanConstructorVisibility()); + assertEquals(Visibility.DEFAULT, ctx.getBeanFieldVisibility()); + assertEquals(Visibility.PROTECTED, ctx.getBeanMethodVisibility()); + } + + @Test + void b02_builder_booleanToggles() { + var ctx = BeanConfigContext.create() + .beansRequireDefaultConstructor(true) + .beansRequireSerializable(true) + .beansRequireSettersForGetters(true) + .beansRequireSomeProperties(false) + .findFluentSetters(true) + .ignoreMissingSetters(false) + .ignoreTransientFields(false) + .ignoreUnknownBeanProperties(true) + .unsortedProperties(true) + .useInterfaceProxies(false) + .useJavaBeanIntrospector(true) + .build(); + assertTrue(ctx.isBeansRequireDefaultConstructor()); + assertTrue(ctx.isBeansRequireSerializable()); + assertTrue(ctx.isBeansRequireSettersForGetters()); + assertFalse(ctx.isBeansRequireSomeProperties()); + assertTrue(ctx.isFindFluentSetters()); + assertFalse(ctx.isIgnoreMissingSetters()); + assertFalse(ctx.isIgnoreTransientFields()); + assertTrue(ctx.isIgnoreUnknownBeanProperties()); + assertTrue(ctx.isUnsortedProperties()); + assertFalse(ctx.isUseInterfaceProxies()); + assertTrue(ctx.isUseJavaBeanIntrospector()); + } + + @Test + void b03_builder_propertyNamerAndTypeName() { + var dlc = new PropertyNamerDLC(); + var ctx = BeanConfigContext.create() + .propertyNamer(dlc) + .beanTypePropertyName("kind") + .build(); + assertSame(dlc, ctx.getPropertyNamer()); + assertEquals("kind", ctx.getBeanTypePropertyName()); + } + + @Test + void b04_builder_notBeanVarargs() { + var ctx = BeanConfigContext.create() + .notBeanPackageNames("pkg.a", "pkg.b") + .notBeanPackagePrefixes("pkg.c.", "pkg.d.") + .notBeanClasses(String.class, Integer.class) + .build(); + assertEquals(Set.of("pkg.a", "pkg.b"), ctx.getNotBeanPackageNames()); + assertEquals(Set.of("pkg.c.", "pkg.d."), ctx.getNotBeanPackagePrefixes()); + assertEquals(Set.of(String.class, Integer.class), ctx.getNotBeanClasses()); + } + + @Test + void b05_builder_notBeanCollectionReplacers() { + var ctx = BeanConfigContext.create() + .notBeanPackageNames("seed.a") + .notBeanPackageNames(List.of("only.a", "only.b")) + .notBeanPackagePrefixes(List.of("pre.")) + .notBeanClasses(List.of(String.class, Long.class)) + .build(); + assertEquals(Set.of("only.a", "only.b"), ctx.getNotBeanPackageNames()); + assertEquals(Set.of("pre."), ctx.getNotBeanPackagePrefixes()); + assertEquals(Set.of(String.class, Long.class), ctx.getNotBeanClasses()); + } + + @Test + void b06_builder_notBeanCollectionReplacers_nullClearsAll() { + var ctx = BeanConfigContext.create() + .notBeanPackageNames("seed.a") + .notBeanPackagePrefixes("seed.b.") + .notBeanClasses(String.class) + .notBeanPackageNames((Collection<String>)null) + .notBeanPackagePrefixes((Collection<String>)null) + .notBeanClasses((Collection<? extends Class<?>>)null) + .build(); + assertTrue(ctx.getNotBeanPackageNames().isEmpty()); + assertTrue(ctx.getNotBeanPackagePrefixes().isEmpty()); + assertTrue(ctx.getNotBeanClasses().isEmpty()); + } + + @Test + void b07_builder_storeAndAnnotationProvider() { + var store = new BasicBeanStore(null); + var ap = AnnotationProvider.create().build(); + var ctx = BeanConfigContext.create() + .beanStore(store) + .annotationProvider(ap) + .build(); + assertSame(store, ctx.getBeanStore()); + assertSame(ap, ctx.getAnnotationProvider()); + } + + @Test + void b08_builder_beanStore_acceptsNull() { + var ctx = BeanConfigContext.create().beanStore(null).build(); + assertNull(ctx.getBeanStore()); + } + + //==================================================================================================== + // Setter null-arg validation + //==================================================================================================== + + @Test + void c01_setters_rejectNullVisibility() { + var b = BeanConfigContext.create(); + assertThrows(IllegalArgumentException.class, () -> b.beanClassVisibility(null)); + assertThrows(IllegalArgumentException.class, () -> b.beanConstructorVisibility(null)); + assertThrows(IllegalArgumentException.class, () -> b.beanFieldVisibility(null)); + assertThrows(IllegalArgumentException.class, () -> b.beanMethodVisibility(null)); + } + + @Test + void c02_setters_rejectNullPropertyNamer() { + var b = BeanConfigContext.create(); + assertThrows(IllegalArgumentException.class, () -> b.propertyNamer(null)); + } + + @Test + void c03_setters_rejectNullTypeName() { + var b = BeanConfigContext.create(); + assertThrows(IllegalArgumentException.class, () -> b.beanTypePropertyName(null)); + } + + @Test + void c04_setters_rejectNullVarargArrays() { + var b = BeanConfigContext.create(); + assertThrows(IllegalArgumentException.class, () -> b.notBeanPackageNames((String[])null)); + assertThrows(IllegalArgumentException.class, () -> b.notBeanPackagePrefixes((String[])null)); + assertThrows(IllegalArgumentException.class, () -> b.notBeanClasses((Class<?>[])null)); + } + + @Test + void c05_setters_rejectNullAnnotationProvider() { + var b = BeanConfigContext.create(); + assertThrows(IllegalArgumentException.class, () -> b.annotationProvider(null)); + } + + //==================================================================================================== + // copy() + //==================================================================================================== + + @Test + void d01_copy_preservesAllValues() { + var store = new BasicBeanStore(null); + var ap = AnnotationProvider.create().build(); + var dlc = new PropertyNamerDLC(); + var src = BeanConfigContext.create() + .beanClassVisibility(Visibility.PROTECTED) + .beanConstructorVisibility(Visibility.PRIVATE) + .beanFieldVisibility(Visibility.DEFAULT) + .beanMethodVisibility(Visibility.PROTECTED) + .beansRequireDefaultConstructor(true) + .beansRequireSerializable(true) + .beansRequireSettersForGetters(true) + .beansRequireSomeProperties(false) + .findFluentSetters(true) + .ignoreMissingSetters(false) + .ignoreTransientFields(false) + .ignoreUnknownBeanProperties(true) + .unsortedProperties(true) + .useInterfaceProxies(false) + .useJavaBeanIntrospector(true) + .propertyNamer(dlc) + .beanTypePropertyName("kind") + .notBeanPackageNames("pkg.a") + .notBeanPackagePrefixes("pkg.b.") + .notBeanClasses(String.class) + .beanStore(store) + .annotationProvider(ap) + .notABeanPredicate(ci -> false) + .build(); + + var copy = src.copy().build(); + + assertEquals(src.getBeanClassVisibility(), copy.getBeanClassVisibility()); + assertEquals(src.getBeanConstructorVisibility(), copy.getBeanConstructorVisibility()); + assertEquals(src.getBeanFieldVisibility(), copy.getBeanFieldVisibility()); + assertEquals(src.getBeanMethodVisibility(), copy.getBeanMethodVisibility()); + assertEquals(src.isBeansRequireDefaultConstructor(), copy.isBeansRequireDefaultConstructor()); + assertEquals(src.isBeansRequireSerializable(), copy.isBeansRequireSerializable()); + assertEquals(src.isBeansRequireSettersForGetters(), copy.isBeansRequireSettersForGetters()); + assertEquals(src.isBeansRequireSomeProperties(), copy.isBeansRequireSomeProperties()); + assertEquals(src.isFindFluentSetters(), copy.isFindFluentSetters()); + assertEquals(src.isIgnoreMissingSetters(), copy.isIgnoreMissingSetters()); + assertEquals(src.isIgnoreTransientFields(), copy.isIgnoreTransientFields()); + assertEquals(src.isIgnoreUnknownBeanProperties(), copy.isIgnoreUnknownBeanProperties()); + assertEquals(src.isUnsortedProperties(), copy.isUnsortedProperties()); + assertEquals(src.isUseInterfaceProxies(), copy.isUseInterfaceProxies()); + assertEquals(src.isUseJavaBeanIntrospector(), copy.isUseJavaBeanIntrospector()); + assertSame(src.getPropertyNamer(), copy.getPropertyNamer()); + assertEquals(src.getBeanTypePropertyName(), copy.getBeanTypePropertyName()); + assertEquals(src.getNotBeanPackageNames(), copy.getNotBeanPackageNames()); + assertEquals(src.getNotBeanPackagePrefixes(), copy.getNotBeanPackagePrefixes()); + assertEquals(src.getNotBeanClasses(), copy.getNotBeanClasses()); + assertSame(src.getBeanStore(), copy.getBeanStore()); + assertSame(src.getAnnotationProvider(), copy.getAnnotationProvider()); + // custom predicate path delegates to user predicate => false for any input + assertFalse(copy.isNotABean(info(String.class))); + } + + @Test + void d02_copy_isIndependent() { + var src = BeanConfigContext.create().findFluentSetters(true).build(); + var copy = src.copy().findFluentSetters(false).build(); + assertTrue(src.isFindFluentSetters()); + assertFalse(copy.isFindFluentSetters()); + } + + @Test + void d03_copy_collectionsDoNotShareInstance() { + var src = BeanConfigContext.create().notBeanPackageNames("pkg.a").build(); + var copy = src.copy().notBeanPackageNames("pkg.b").build(); + assertEquals(Set.of("pkg.a"), src.getNotBeanPackageNames()); + assertEquals(Set.of("pkg.a", "pkg.b"), copy.getNotBeanPackageNames()); + } + + //==================================================================================================== + // Returned collections are unmodifiable + //==================================================================================================== + + @Test + void e01_collectionsAreUnmodifiable() { + var ctx = BeanConfigContext.create() + .notBeanPackageNames("pkg.a") + .notBeanPackagePrefixes("pkg.b.") + .notBeanClasses(String.class) + .build(); + assertThrows(UnsupportedOperationException.class, () -> ctx.getNotBeanPackageNames().add("x")); + assertThrows(UnsupportedOperationException.class, () -> ctx.getNotBeanPackagePrefixes().add("y")); + assertThrows(UnsupportedOperationException.class, () -> ctx.getNotBeanClasses().add(Object.class)); + } + + //==================================================================================================== + // isNotABean default behavior + //==================================================================================================== + + @Test + void f01_isNotABean_rejectsNonClassKinds() { + var ctx = BeanConfigContext.DEFAULT; + assertTrue(ctx.isNotABean(info(int.class))); + assertTrue(ctx.isNotABean(info(int[].class))); + assertTrue(ctx.isNotABean(info(java.lang.annotation.Retention.class))); + assertTrue(ctx.isNotABean(info(java.time.DayOfWeek.class))); + } + + @Test + void f02_isNotABean_acceptsRegularClasses() { + var ctx = BeanConfigContext.DEFAULT; + assertFalse(ctx.isNotABean(info(BasicBeanStore.class))); + } + + @Test + void f03_isNotABean_packageName_excludes() { + var ctx = BeanConfigContext.create() + .notBeanPackageNames(BasicBeanStore.class.getPackage().getName()) + .build(); + assertTrue(ctx.isNotABean(info(BasicBeanStore.class))); + } + + @Test + void f04_isNotABean_packagePrefix_excludes() { + var ctx = BeanConfigContext.create() + .notBeanPackagePrefixes("org.apache.juneau.commons.") + .build(); + assertTrue(ctx.isNotABean(info(BasicBeanStore.class))); + } + + @Test + void f05_isNotABean_classExclude() { + var ctx = BeanConfigContext.create().notBeanClasses(BasicBeanStore.class).build(); + assertTrue(ctx.isNotABean(info(BasicBeanStore.class))); + } + + @Test + void f06_isNotABean_classExclude_includesSubtypes() { + var ctx = BeanConfigContext.create().notBeanClasses(CharSequence.class).build(); + assertTrue(ctx.isNotABean(info(String.class))); + } + + @Test + void f07_isNotABean_customPredicateOverridesDefault() { + var ctx = BeanConfigContext.create() + .notABeanPredicate(ci -> ci.is(String.class)) + .build(); + assertTrue(ctx.isNotABean(info(String.class))); + assertFalse(ctx.isNotABean(info(int.class))); // default would say true; predicate overrides + } + + @Test + void f08_isNotABean_customPredicateClearedToNullRevertsToDefault() { + var ctx = BeanConfigContext.create() + .notABeanPredicate(ci -> false) + .notABeanPredicate(null) + .build(); + assertTrue(ctx.isNotABean(info(int.class))); + } + + @Test + void f09_isNotABean_rejectsNullArg() { + var ctx = BeanConfigContext.DEFAULT; + assertThrows(IllegalArgumentException.class, () -> ctx.isNotABean(null)); + } + + @Test + void f10_isNotABean_packageName_nonMatchingEntryIsSkipped() { + var ctx = BeanConfigContext.create() + .notBeanPackageNames("never.matches.this") + .build(); + assertFalse(ctx.isNotABean(info(BasicBeanStore.class))); + } + + @Test + void f11_isNotABean_packagePrefix_nonMatchingEntryIsSkipped() { + var ctx = BeanConfigContext.create() + .notBeanPackagePrefixes("never.matches.") + .build(); + assertFalse(ctx.isNotABean(info(BasicBeanStore.class))); + } + + @Test + void f12_isNotABean_classExclude_nonMatchingEntryIsSkipped() { + var ctx = BeanConfigContext.create() + .notBeanClasses(java.util.UUID.class) + .build(); + assertFalse(ctx.isNotABean(info(BasicBeanStore.class))); + } +} diff --git a/todo/TODO-5-bean-runtime-types-to-commons.md b/todo/TODO-5-bean-runtime-types-to-commons.md index d6f418a8e7..b086acfdbe 100644 --- a/todo/TODO-5-bean-runtime-types-to-commons.md +++ b/todo/TODO-5-bean-runtime-types-to-commons.md @@ -4,6 +4,25 @@ This is the remaining work from **Phase 5 of the bean-layer split**. Phase 5a (t --- +## Status (as of Phase 5b 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. + +- [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 [...] +- [ ] **Step 2** — Replace `ClassMeta` with `ClassInfo` in `BeanMeta` / `BeanPropertyMeta`. Most `cm.*` calls (`isAnonymousClass`, `isMemberClass`, `isAssignableTo`, `getModifiers`, `getRecordComponents`, `inner`, …) are pure reflection that already exists on `ClassInfo`. Two outliers — `cm.getProxyInvocationHandler()` (replace with a `BeanConfigContext` hook or move proxy creation into `BeanMeta`) and `cm.getMarshallingContext().string()` (use a plain `Class<String>`/`ClassInfo`). +- [ ] **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. +- [ ] **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. +- [ ] **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 2 is the recommended next checkpoint — it removes one of the two big blockers (`ClassMeta` coupling) without yet attempting the swap/registry/session decoupling that requires reworking serializer/parser code paths. + +--- + ## Goal Move these eight `BeanXxx` runtime types out of `org.apache.juneau` (in `juneau-marshall`) into `org.apache.juneau.commons.bean` (in `juneau-commons`) so the **bean-modeling runtime** is independently usable without dragging in the full marshalling stack:
