001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.bean;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.commons.bean.BeanMeta.MethodType.*;
021import static org.apache.juneau.commons.function.Suppliers.*;
022import static org.apache.juneau.commons.reflect.AnnotationTraversal.*;
023import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
024import static org.apache.juneau.commons.utils.CollectionUtils.*;
025import static org.apache.juneau.commons.utils.Shorts.*;
026import static org.apache.juneau.commons.utils.Shorts.neq;
027import static org.apache.juneau.commons.utils.Shorts.eq;
028import static org.apache.juneau.commons.utils.StringUtils.*;
029import static org.apache.juneau.commons.utils.ThrowableUtils.*;
030
031import java.beans.*;
032import java.io.*;
033import java.lang.reflect.*;
034import java.util.*;
035import java.util.function.*;
036
037import org.apache.juneau.commons.collections.*;
038import org.apache.juneau.commons.function.*;
039import org.apache.juneau.commons.inject.*;
040import org.apache.juneau.commons.lang.*;
041import org.apache.juneau.commons.reflect.*;
042import org.apache.juneau.commons.reflect.Visibility;
043import org.apache.juneau.commons.utils.*;
044
045/**
046 * Encapsulates all access to the properties of a bean class (like a souped-up {@link BeanInfo}).
047 *
048 * <h5 class='topic'>Description</h5>
049 *
050 * Uses introspection to find all the properties associated with this class.  If the
051 * {@link BeanType @BeanType} annotation is present on the class (or the marshalling-side
052 * {@code @Marshalled} sibling), that information is used to determine the properties on the class.
053 * Otherwise, the {@code BeanInfo} functionality in Java is used to determine the properties on the class.
054 *
055 * <h5 class='topic'>Bean property ordering</h5>
056 *
057 * The order of the properties is as follows:
058 * <ul class='spaced-list'>
059 *    <li>
060 *       If {@link BeanType#properties() @BeanType(properties)} is specified on the class, the order matches the
061 *       list of properties in the annotation.
062 *    <li>
063 *       Otherwise, by default, properties are sorted alphabetically.
064 *    <li>
065 *       When unsorted properties are enabled ({@link BeanConfigContext#isUnsortedProperties()},
066 *       {@link BeanType#unsorted() @BeanType(unsorted=true)}, or a bean filter), properties use declaration order:
067 *       <ul>
068 *          <li>Records: record component order.
069 *          <li>Other classes: field declaration order, superclass fields first.  Getter/setter properties are ranked
070 *             by their same-named field (even a private one), which is used as an ordering hint only.
071 *          <li>Method-only properties (no backing field) follow, alphabetically.
072 *       </ul>
073 *    <li>
074 *       {@link BeanConfigContext#isRecordComponentOrder()} gives records component order even when other beans are
075 *       sorted.
076 * </ul>
077 *
078 * <p>
079 * Field order comes from {@link Class#getDeclaredFields()}, which is declaration order on HotSpot and OpenJ9 but is
080 * not guaranteed by the JLS.
081 *
082 * <h5 class='topic'>Thread safety</h5>
083 *
084 * Instances are effectively immutable after construction and are safe for concurrent read access.
085 * Construction is not thread-safe and should be completed before sharing an instance across threads.
086 *
087 *
088 * @param <T> The class type that this metadata applies to.
089 */
090@SuppressWarnings({
091   "java:S107", // The private constructor and validateAndRegisterProperty() thread the fixed introspection context (8 parameters each); a holder object would only add indirection
092   "java:S135", // The property-discovery loops in findBeanMethods() and the constructor use independent continue guard clauses to skip ineligible members
093   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
094   "java:S1200", // Central bean-introspection type; high coupling to annotations/reflect/utils is inherent to its role
095   "java:S3776", // findBeanMethods() and the BeanMeta constructor apply the bean getter/setter/field discovery rules in a single pass; splitting them would scatter those rules
096   "java:S6539", // Monster class; BeanMeta is intentionally a single cohesive bean-introspection metadata cache
097   "javabugs:S2259", // The annotation lookups are guarded by isType()/hasConstructor() checks that flow analysis does not track.
098   "rawtypes", // Raw Class is used in the BeanFactory instantiation ((Class)fc) and in Class[]::new for record constructor lookup
099   "unchecked" // Casts to T/BeanFactory in the factory-create, Proxy.newProxyInstance and BeanInstantiator paths are guaranteed by the bean class being instantiated
100})
101public class BeanMeta<T> {
102
103   /**
104    * Represents the result of creating a BeanMeta, including the bean metadata and any reason why it's not a bean.
105    *
106    * @param <T> The bean type.
107    * @param beanMeta The bean metadata, or <jk>null</jk> if the class is not a bean.
108    * @param notABeanReason The reason why the class is not a bean, or <jk>null</jk> if it is a bean.
109    */
110   public record BeanMetaValue<T>(BeanMeta<T> beanMeta, String notABeanReason) {
111      public Optional<BeanMeta<T>> optBeanMeta() { return o(beanMeta()); }
112      Optional<String> optNotABeanReason() { return o(notABeanReason()); }
113   }
114
115   /**
116    * Possible property method types.
117    */
118   enum MethodType {
119      UNKNOWN, GETTER, SETTER, EXTRAKEYS;
120   }
121
122   /*
123    * Temporary getter/setter method struct used for calculating bean methods.
124    */
125   private static class BeanMethod {
126      private final String propertyName;
127      private final MethodType methodType;
128      private final Method method;
129      private final ClassInfo type;
130
131      private BeanMethod(String propertyName, MethodType type, Method method) {
132         this.propertyName = propertyName;
133         this.methodType = type;
134         this.method = method;
135         this.type = info(type == SETTER ? method.getParameterTypes()[0] : method.getReturnType());
136      }
137
138      @Override /* Overridden from Object */
139      public String toString() {
140         return method.toString();
141      }
142
143      /*
144       * Returns true if this method matches the class type of the specified property.
145       * Only meant to be used for setters.
146       */
147      private boolean matchesPropertyType(BeanPropertyMeta.Builder b) {
148         if (b == null)
149            return false;
150
151         // Don't do further validation if this is the "*" bean property.
152         if (eq(b.name, "*"))
153            return true;
154
155         // Get the bean property type from the getter/field.
156         Class<?> pt = null;  // Uses raw Class<?> here because assignment checks are performed against Class.
157         if (nn(b.getter))
158            pt = b.getter.getReturnType().inner();
159         else if (nn(b.field))
160            pt = b.field.inner().getType();
161
162         // Matches if only a setter is defined.
163         if (pt == null)
164            return true;
165
166         // Doesn't match if not same type or super type as getter/field.
167         if (! type.isAssignableFrom(pt))
168            return false;
169
170         // If a setter was previously set, only use this setter if it's a closer
171         // match (e.g. prev type is a superclass of this type).
172         if (b.setter == null)
173            return true;
174
175         return type.isStrictChildOf(b.setter.getParameterTypes().get(0).inner());
176      }
177   }
178
179   /**
180    * Represents a bean constructor with its associated property names.
181    *
182    * @param constructor The constructor information.
183    * @param propertyNames The list of property names that correspond to the constructor parameters.
184    */
185   private record BeanConstructor(Optional<ConstructorInfo> constructor, List<String> args) {}
186
187   /**
188    * Creates a {@link BeanMeta} instance for the specified bean type info.
189    *
190    * <p>
191    * This is the marshalling-side factory entry point.  It attempts to create bean metadata for a class; if the class is
192    * determined to be a bean, the returned {@link BeanMetaValue} carries the {@link BeanMeta} instance and a
193    * <jk>null</jk> reason.  Otherwise it carries <jk>null</jk> for the bean metadata and a non-empty string explaining
194    * why the class is not a bean.
195    *
196    * <p>
197    * The bean filter is resolved internally from the {@code @Marshalled} / {@link BeanType @BeanType} annotations on the
198    * class via {@link BeanMetaInitializer#buildBeanFilter(BeanInfo)}.  See {@link #of(Class, BeanConfigContext)} for
199    * the pure bean-modeling-side entry point that does not require a {@link BeanInfo}.
200    *
201    * <h5 class='section'>Exception handling:</h5>
202    * <p>
203    * If a {@link RuntimeException} is thrown during bean metadata creation, it is caught and the exception message
204    * is returned as the {@code notABeanReason} with <jk>null</jk> for the bean metadata.
205    *
206    * <h5 class='section'>Example:</h5>
207    * <p class='bjava'>
208    *    <jc>// Create bean metadata for a class via the marshalling-side context.</jc>
209    *    BeanInfo&lt;Person&gt; <jv>cm</jv> = <jv>marshallingContext</jv>.getClassMeta(Person.<jk>class</jk>);
210    *    BeanMetaHolder&lt;Person&gt; <jv>result</jv> = BeanMeta.<jsm>create</jsm>(<jv>cm</jv>, <jk>null</jk>);
211    *
212    *    <jc>// Check if it's a bean</jc>
213    *    <jk>if</jk> (<jv>result</jv>.beanMeta() != <jk>null</jk>) {
214    *       BeanMeta&lt;Person&gt; <jv>bm</jv> = <jv>result</jv>.beanMeta();
215    *       <jc>// Use the bean metadata...</jc>
216    *    } <jk>else</jk> {
217    *       String <jv>reason</jv> = <jv>result</jv>.notABeanReason();
218    *       <jc>// Handle the case where it's not a bean...</jc>
219    *    }
220    * </p>
221    *
222    * @param <T> The class type.
223    * @param cm The bean type info for the class to create bean metadata for.  Must not be <jk>null</jk>.
224    * @param implClass Optional implementation class info to use when looking for a no-arg constructor.  Can be <jk>null</jk> (the bean class's own no-arg constructor is used instead).
225    * @return A {@link BeanMetaValue} containing the bean metadata (if successful) or a reason why it's not a bean.
226    */
227   public static <T> BeanMetaValue<T> create(BeanInfo<T> cm, ClassInfo implClass) {
228      return create(cm, implClass, null);
229   }
230
231   /**
232    * Same as {@link #create(BeanInfo, ClassInfo)} but allows an explicit {@link PropertyNamer} to override the one
233    * that would otherwise be derived from the bean filter or {@link BeanConfigContext#getPropertyNamer()}.
234    *
235    * <p>
236    * The returned {@link BeanMeta} is <b>not</b> cached anywhere — callers that need a namer-specific view (for
237    * example, {@code MarshallingSession#toBeanMap(Object, PropertyNamer)}) build one on demand so the shared,
238    * {@code Class}-keyed default {@link BeanMeta} cache is never polluted with alternate-namer property keys.
239    *
240    * @param <T> The class type.
241    * @param cm The bean type info for the class to create bean metadata for.  Must not be <jk>null</jk>.
242    * @param implClass Optional implementation class info to use when looking for a no-arg constructor.  Can be <jk>null</jk> (the bean class's own no-arg constructor is used instead).
243    * @param propertyNamer Optional property namer to use for deriving property names.  If <jk>null</jk>, the namer is
244    *    resolved the normal way (bean filter, then {@link BeanConfigContext#getPropertyNamer()}).
245    * @return A {@link BeanMetaValue} containing the bean metadata (if successful) or a reason why it's not a bean.
246    */
247   public static <T> BeanMetaValue<T> create(BeanInfo<T> cm, ClassInfo implClass, PropertyNamer propertyNamer) {
248      try {
249         var cfg = cm.getBeanConfigContext();
250         var ap = cfg.getAnnotationProvider();
251         var bmi = cfg.getBeanMetaInitializer();
252
253         // Sanity checks first.
254         if (cfg.isNotABean(cm))
255            return notABean("Class matches exclude-class list");
256
257         if (cfg.isBeansRequireSerializable() && ! cm.isAssignableTo(Serializable.class) && ! bmi.hasBeanRegistrationAnnotation(cfg, cm))
258            return notABean("Class is not serializable");
259
260         if (ap.has(BeanIgnore.class, cm))
261            return notABean("Class is annotated with @BeanIgnore");
262
263      if ((! cfg.getBeanClassVisibility().isVisible(cm.getModifiers()) || cm.isAnonymousClass()) && ! bmi.hasBeanRegistrationAnnotation(cfg, cm))
264         return notABean("Class is not public");
265
266         var bm = new BeanMeta<>(cm, bmi.buildBeanFilter(cm), null, implClass, propertyNamer);
267
268         if (nn(bm.notABeanReason))
269            return notABean(bm.notABeanReason);
270
271         return new BeanMetaValue<>(bm, null);
272      } catch (RuntimeException e) {
273         return new BeanMetaValue<>(null, e.getMessage());
274      }
275   }
276
277   /**
278    * Returns the property namer that should be used to derive property names for this bean.
279    *
280    * <p>
281    * Precedence: an explicit per-instance {@link #propertyNamerOverride} (used by the
282    * {@code toBeanMap(Object, PropertyNamer)} path) wins over everything; otherwise the bean filter's namer, then the
283    * {@link BeanConfigContext#getPropertyNamer() configured default}.  Preserving this fallback order keeps the
284    * default-cache construction path behaviorally identical to before the override was introduced.
285    */
286   private PropertyNamer effectivePropertyNamer() {
287      if (nn(propertyNamerOverride))
288         return propertyNamerOverride;
289      return o(beanFilter).map(x -> x.getPropertyNamer()).orElse(config.getPropertyNamer());
290   }
291
292   /**
293    * Extracts the property name from {@link BeanProp @BeanProp} or {@link Name @Name} annotations.
294    *
295    * <p>
296    * If {@link Name @Name} annotations are present, returns the value from the last one.
297    * Otherwise, searches through {@link BeanProp @BeanProp} annotations and returns the first non-empty
298    * {@link BeanProp#value() value()} or {@link BeanProp#name() name()} found.
299    *
300    * @param p List of {@link BeanProp @BeanProp} annotations.
301    * @param n List of {@link Name @Name} annotations.
302    * @return The property name, or <jk>null</jk> if no name is found.
303    */
304   private static String bpName(List<BeanProp> p, List<Name> n) {
305      if (p.isEmpty() && n.isEmpty())
306         return null;
307      if (! n.isEmpty())
308         return last(n).value();
309
310      var name = Holder.of(p.isEmpty() ? null : "");
311      p.forEach(x -> {
312         if (! x.value().isEmpty())
313            name.set(x.value());
314         if (! x.name().isEmpty())
315            name.set(x.name());
316      });
317
318      return name.orElse(null);
319   }
320
321   /**
322    * Extracts the property name from a single {@link BeanProp @BeanProp} or {@link Name @Name} annotation.
323    *
324    * <p>
325    * For {@link BeanProp @BeanProp} annotations, returns the first non-empty value found in the following order:
326    * <ol>
327    *    <li>{@link BeanProp#name() name()}
328    *    <li>{@link BeanProp#value() value()}
329    * </ol>
330    *
331    * <p>
332    * For {@link Name @Name} annotations, returns the {@link Name#value() value()}.
333    *
334    * <p>
335    * This method is used to extract property names from individual annotations, typically when processing
336    * annotation lists in stream operations.
337    *
338    * @param ai The annotation info containing either a {@link BeanProp @BeanProp} or {@link Name @Name} annotation.
339    * @return The property name extracted from the annotation, or <jk>null</jk> if no name is found.
340    * @see #bpName(List, List)
341    */
342   private static String name(AnnotationInfo<?> ai) {
343      if (ai.isType(BeanProp.class)) {
344         var p = ai.cast(BeanProp.class).inner();
345         if (ine(p.name()))
346            return p.name();
347         if (ine(p.value()))
348            return p.value();
349      } else {
350         var n = ai.cast(Name.class).inner();
351         if (ine(n.value()))
352            return n.value();
353      }
354      return null;
355   }
356
357   /*
358    * Shortcut for creating a BeanMetaValue with a not-a-bean reason.
359    */
360   private static <T> BeanMetaValue<T> notABean(String reason) {
361      return new BeanMetaValue<>(null, reason);
362   }
363
364   private BeanConstructor beanConstructor;                                   // The constructor for this bean.
365   private final Object marshallingContext;                                   // MarshallingContext, but Object-typed so the field can live in commons.bean.  Cast to MarshallingContext at marshalling-side use sites.  Null when constructed via {@link #of(Class, BeanConfigContext)}.
366   private final BeanConfigContext config;                                    // Bean-modeling settings facade — always non-null.  Sources: marshallingContext.getBeanConfigContext() (marshalling-side) or the explicit BeanConfigContext (commons-side).
367   private final BeanFilter beanFilter;                                       // Optional bean filter associated with the target class.  Typed as the bean-modeling-side SPI seam; marshalling-side callers narrow back to MarshalledFilter (the only concrete in-tree implementation).
368   private final NullableSupplier<InvocationHandler> beanProxyInvocationHandler;  // The invocation handler for this bean (if it's an interface).
369   private final Supplier<BeanRegistryLookup> beanRegistry;                   // The bean registry for this bean.  Exposed through the BeanRegistryLookup SPI seam.
370   private final Supplier<List<ClassInfo>> classHierarchy;                    // List of all classes traversed in the class hierarchy.
371   private final BeanInfo<T> classMeta;                                       // The target class type that this meta object describes.  Null when constructed via {@link #of(Class, BeanConfigContext)}.  Typed against the bean-modeling SPI seam.
372   private final ClassInfo classInfo;                                         // Pure-reflection view of the bean class that decouples bean modeling from ClassMeta.  Always non-null.
373   private final Supplier<String> dictionaryName;                             // The @Marshalled(typeName) annotation defined on this bean class.
374   private final BeanPropertyMeta dynaProperty;                               // "extras" property.
375   private final Class<? extends BeanFactory> factoryClass;  // @BeanType(factory=X.class) — null means no factory.
376   private final boolean fluentSetters;                                       // Whether fluent setters are enabled.
377   private final Map<Method,String> getterProps;                              // The getter properties on the target class.
378   private final Map<String,BeanPropertyMeta> hiddenProperties;               // The hidden properties on the target class.
379   private final ConstructorInfo implClassConstructor;                        // Optional constructor to use if one cannot be found.
380   final String notABeanReason;                                       // Readable string explaining why this class wasn't a bean.
381   private final BeanPropertyMeta[] propertyArray;                           // The properties as an array, for allocation-free iteration.
382   private final Map<String,BeanPropertyMeta> properties;                     // The properties on the target class.
383   private final Map<Method,String> setterProps;                              // The setter properties on the target class.
384   private final List<String> requiredPropertyNames;                         // Names of @BeanProp(required=true) properties, in property order.
385   private final boolean unsortedProperties;                                  // Whether properties use declaration order instead of alphabetical order.
386   private final PropertyNamer propertyNamerOverride;                         // Optional per-instance namer override.  Null means resolve the normal way (bean filter, then config).  Non-null takes precedence over both.
387   private final ClassInfo stopClass;                                         // The stop class for hierarchy traversal.
388   private final BeanPropertyMeta typeProperty;                               // "_type" mock bean property.
389   private final String typePropertyName;                                     // "_type" property actual name.
390   private final Map<BeanPropertyMeta,BeanRegistryLookup> propertyBeanRegistries;  // Per-property bean-registry side-map that keeps registry implementation details off BeanPropertyMeta itself. Typed against the commons.bean SPI seam.
391
392   /**
393    * Creates a {@link BeanMeta} for the specified class using the supplied {@link BeanConfigContext}.
394    *
395    * <p>
396    * This is the bean-modeling entry point — it constructs a {@link BeanMeta} purely from a {@link Class}
397    * and a {@link BeanConfigContext}, without touching any marshalling-side type infrastructure.
398    * The returned {@link BeanMeta} carries enough information to do raw getter/setter invocation and property
399    * iteration; marshalling-aware reads ({@link #getBeanInfo()}, type-resolution on each
400    * {@link BeanPropertyMeta#getBeanInfo()}, the per-property registry lookup) remain <jk>null</jk>.
401    *
402    * <h5 class='section'>Example:</h5>
403    * <p class='bjava'>
404    *    BeanMeta&lt;Person&gt; <jv>bm</jv> = BeanMeta.<jsm>of</jsm>(Person.<jk>class</jk>, BeanConfigContext.<jsf>DEFAULT</jsf>);
405    *    BeanPropertyMeta <jv>name</jv> = <jv>bm</jv>.getProperties().get(<js>"name"</js>);
406    * </p>
407    *
408    * @param <T> The bean class type.
409    * @param beanClass The class to build a {@link BeanMeta} for.  Must not be <jk>null</jk>.
410    * @param config The bean-modeling configuration to apply.  Must not be <jk>null</jk>.
411    * @return A new {@link BeanMeta} for the specified class.
412    */
413   public static <T> BeanMeta<T> of(Class<T> beanClass, BeanConfigContext config) {
414      return new BeanMeta<>(beanClass, config);
415   }
416
417   /**
418    * Same as {@link #of(Class, BeanConfigContext)} using {@link BeanConfigContext#DEFAULT}.
419    *
420    * @param <T> The bean class type.
421    * @param beanClass The class to build a {@link BeanMeta} for.  Must not be <jk>null</jk>.
422    * @return A new {@link BeanMeta} for the specified class.
423    */
424   public static <T> BeanMeta<T> of(Class<T> beanClass) {
425      return of(beanClass, BeanConfigContext.DEFAULT);
426   }
427
428   /**
429    * Constructor.
430    *
431    * <p>
432    * Creates a new {@link BeanMeta} instance for the specified class metadata. This constructor performs
433    * introspection to discover all bean properties, methods, and fields in the class hierarchy.
434    *
435    * <p>
436    * The bean metadata is built by:
437    * <ul>
438    *    <li>Finding all bean fields in the class hierarchy
439    *    <li>Finding all bean methods (getters, setters, extraKeys) in the class hierarchy
440    *    <li>Determining the appropriate constructor for bean instantiation
441    *    <li>Building the class hierarchy for property discovery
442    *    <li>Creating the bean registry for dictionary name resolution
443    *    <li>Validating and filtering properties based on bean filter settings
444    * </ul>
445    *
446    * @param cm The class metadata for the bean class.  Must not be <jk>null</jk>.
447    * @param bf Optional bean filter to apply. Can be <jk>null</jk> (no bean filter customizations are applied; the default stop class of {@code Object} is used).
448    * @param pNames Explicit list of property names and order. If <jk>null</jk>, properties are determined automatically.
449    * @param implClass Optional implementation class constructor to use if one cannot be found. Can be <jk>null</jk> (the bean class's own no-arg constructor is used instead).
450    */
451   protected BeanMeta(BeanInfo<T> cm, BeanFilter bf, String[] pNames, ClassInfo implClass) {
452      this(cm, cm, cm.getBeanConfigContext(), cm.getMarshallingContext(), bf, pNames, implClass, null);
453   }
454
455   /**
456    * Same as {@link #BeanMeta(BeanInfo, BeanFilter, String[], ClassInfo)} but allows an explicit
457    * {@link PropertyNamer} override.
458    *
459    * @param cm The class metadata for the bean class.  Must not be <jk>null</jk>.
460    * @param bf Optional bean filter to apply. Can be <jk>null</jk> (no bean filter customizations are applied; the default stop class of {@code Object} is used).
461    * @param pNames Explicit list of property names and order. If <jk>null</jk>, properties are determined automatically.
462    * @param implClass Optional implementation class constructor to use if one cannot be found. Can be <jk>null</jk> (the bean class's own no-arg constructor is used instead).
463    * @param propertyNamerOverride Optional namer that, when non-<jk>null</jk>, takes precedence over the bean filter
464    *    and {@link BeanConfigContext#getPropertyNamer()} for deriving property names.
465    */
466   protected BeanMeta(BeanInfo<T> cm, BeanFilter bf, String[] pNames, ClassInfo implClass, PropertyNamer propertyNamerOverride) {
467      this(cm, cm, cm.getBeanConfigContext(), cm.getMarshallingContext(), bf, pNames, implClass, propertyNamerOverride);
468   }
469
470   /**
471    * Bean-modeling constructor — builds a {@link BeanMeta} without marshalling context state.
472    *
473    * <p>
474    * See {@link #of(Class, BeanConfigContext)} for the public entry point.  The {@link #getBeanInfo()},
475    * {@link #getBeanTypeResolver() bean type resolver}, and per-property registry lookups are
476    * left <jk>null</jk>; the per-property {@link BeanPropertyMeta#getBeanInfo() rawTypeMeta}/{@code typeMeta}
477    * fields are also <jk>null</jk> (no type-resolution is performed in this path).
478    *
479    * @param beanClass The bean class.  Must not be <jk>null</jk>.
480    * @param config The bean-modeling configuration.  Must not be <jk>null</jk>.
481    */
482   protected BeanMeta(Class<T> beanClass, BeanConfigContext config) {
483      this(null, info(reqnn("beanClass", beanClass)), reqnn("config", config), null, null, null, null, null);
484   }
485
486   private BeanMeta(BeanInfo<T> cm, ClassInfo ci0, BeanConfigContext config, Object mc, BeanFilter bf, String[] pNames, ClassInfo implClass, PropertyNamer propertyNamerOverride) {
487      classMeta = cm;
488      classInfo = ci0;
489      this.config = config;
490      marshallingContext = mc;
491      beanFilter = bf;
492      this.propertyNamerOverride = propertyNamerOverride;
493      implClassConstructor = o(implClass).map(x -> x.getPublicConstructor(x2 -> x2.hasNumParameters(0)).orElse(null)).orElse(null);
494      fluentSetters = config.isFindFluentSetters() || (nn(bf) && bf.isFluentSetters());
495      stopClass = o(bf).map(x -> x.getStopClass()).orElse(info(Object.class));
496      beanRegistry = memoize(this::findBeanRegistry);
497      classHierarchy = memoize(this::findClassHierarchy);
498      beanConstructor = findBeanConstructor();
499
500      // Local variables for initialization
501      var ap = config.getAnnotationProvider();
502      var c = classInfo.inner();
503      var ci = classInfo;
504      String notABeanReasonTemp = null;
505      var propertiesValue = Holder.<Map<String,BeanPropertyMeta>>empty();
506      var hiddenPropertiesMap = CollectionUtils.<String,BeanPropertyMeta>map();
507      var getterPropsMap = CollectionUtils.<Method,String>map();  // Convert to MethodInfo keys
508      var setterPropsMap = CollectionUtils.<Method,String>map();
509      var dynaPropertyValue = Holder.<BeanPropertyMeta>empty();
510      var propertyBeanRegistriesTemp = CollectionUtils.<BeanPropertyMeta,BeanRegistryLookup>map();  // Per-property BeanRegistry side-map.
511      var unsortedPropertiesTemp = false;
512      var btList = ap.find(BeanType.class, classInfo);
513      var propertyNamer = effectivePropertyNamer();
514
515      // resolveTypePropertyName may return null on the commons-side NOOP path; fall back to the configured default.
516      var resolvedTypePropertyName = config.getBeanMetaInitializer().resolveTypePropertyName(config, classInfo);
517      this.typePropertyName = nn(resolvedTypePropertyName) ? resolvedTypePropertyName : config.getBeanTypePropertyName();
518
519      // Check if constructor is required but not found (records always resolve their canonical constructor above, so they never reach this)
520      if (! beanConstructor.constructor().isPresent() && bf == null && config.isBeansRequireDefaultConstructor())
521         notABeanReasonTemp = "Class does not have the required no-arg constructor";
522
523      var bfo = o(bf);
524      var fixedBeanProps = bfo.map(x -> x.getProperties()).orElse(emptySet());
525
526      try {
527         Map<String,BeanPropertyMeta.Builder> normalProps = map();  // NOAI
528
529         // First populate the properties with those specified in the bean annotation to
530         // ensure that ordering first.
531         fixedBeanProps.forEach(x -> normalProps.put(x, BeanPropertyMeta.builder(this, x)));
532
533         if (config.isUseJavaBeanIntrospector()) {
534            var c2 = bfo.map(x -> x.getInterfaceClass()).filter(Objects::nonNull).orElse(classInfo);
535            java.beans.BeanInfo bi = null;
536            if (! c2.isInterface())
537               bi = Introspector.getBeanInfo(c2.inner(), stopClass.inner());
538            else
539               bi = Introspector.getBeanInfo(c2.inner(), null);
540            if (nn(bi))
541               mergeJavaBeanPropertyDescriptorsIntoNormalProps(bi, normalProps, propertyNamer);
542
543         } else /* Use 'better' introspection */ {
544
545            findBeanFields().forEach(x -> {
546               var name = ap.find(x).stream()
547                  .filter(x2 -> x2.isType(BeanProp.class) || x2.isType(Name.class))
548                  .map(BeanMeta::name)
549                  .filter(Objects::nonNull)
550                  .findFirst()
551                  .orElse(propertyNamer.getPropertyName(x.getName()));
552               name = resolveBeanFieldPropertyName(x, name, propertyNamer);
553               if (nn(name)) {
554                  normalProps.computeIfAbsent(name, n->BeanPropertyMeta.builder(this, n)).setField(x);
555               }
556            });
557
558            var bms = findBeanMethods();
559
560            // Iterate through all the getters.
561            bms.forEach(x -> {
562               var pn = x.propertyName;
563               var m = x.method;  // Method is read first for name/annotation precedence checks, then wrapped as MethodInfo.
564               var mi = info(m);
565               var bpm = normalProps.computeIfAbsent(pn, k -> new BeanPropertyMeta.Builder(this, k));
566
567               if (x.methodType == GETTER) {
568                  // Two getters.  Pick the best.
569                  if (nn(bpm.getter)
570                     && ((! ap.has(BeanProp.class, mi) && ap.has(BeanProp.class, bpm.getter))
571                        || (m.getName().startsWith("is") && bpm.getter.getNameSimple().startsWith("get")))) {
572                     // @BeanProp on existing getter takes precedence; else getX() overrides isX().
573                     m = bpm.getter.inner();
574                  }
575                  bpm.setGetter(info(m));
576               }
577            });
578
579            // Now iterate through all the setters.
580            bms.stream().filter(x -> eq(x.methodType, SETTER)).forEach(x -> {
581               var bpm = normalProps.get(x.propertyName);
582               if (x.matchesPropertyType(bpm))
583                  bpm.setSetter(info(x.method));
584            });
585
586            // Now iterate through all the extraKeys.
587            bms.stream().filter(x -> eq(x.methodType, EXTRAKEYS)).forEach(x -> normalProps.get(x.propertyName).setExtraKeys(info(x.method)));
588         }
589
590         var typeVarImpls = TypeVariables.of(c);
591
592         // Eliminate invalid properties, and set the contents of getterProps and setterProps.
593         var readOnlyProps = bfo.map(x -> x.getReadOnlyProperties()).orElse(emptySet());
594         var writeOnlyProps = bfo.map(x -> x.getWriteOnlyProperties()).orElse(emptySet());
595         for (var i = normalProps.values().iterator(); i.hasNext();) {
596            var p = i.next();
597            validateAndRegisterProperty(p, c, typeVarImpls, readOnlyProps, writeOnlyProps, i, getterPropsMap, setterPropsMap);
598         }
599
600         // Check for missing properties.
601         fixedBeanProps.stream().filter(x -> ! normalProps.containsKey(x)).findFirst().ifPresent(x -> { throw brex(c, "The property '%s' was defined on the @BeanType(properties=X) annotation of class '%s' but was not found on the class definition.", x, ci.getNameSimple()); });
602
603         // For records with renamed properties, remap constructor args to use the actual property names.
604         if (ci.isRecord() && ine(beanConstructor.args())) {
605            var components = ci.getRecordComponents();
606            var remappedArgs = new ArrayList<String>(beanConstructor.args().size());
607            for (int idx = 0; idx < beanConstructor.args().size(); idx++) {
608               var componentName = beanConstructor.args().get(idx);
609               if (normalProps.containsKey(componentName)) {
610                  remappedArgs.add(componentName);
611               } else {
612                  var rcName = idx < components.size() ? components.get(idx).getName() : componentName;
613                  var found = normalProps.entrySet().stream()
614                     .filter(e -> (e.getValue().field != null && e.getValue().field.hasName(rcName))
615                        || (e.getValue().getter != null && e.getValue().getter.hasName(rcName)))
616                     .map(Map.Entry::getKey)
617                     .findFirst()
618                     .orElse(componentName);
619                  remappedArgs.add(found);
620               }
621            }
622            beanConstructor = new BeanConstructor(beanConstructor.constructor(), remappedArgs);
623         }
624
625         // Mark constructor arg properties.
626         for (var fp : beanConstructor.args()) {
627            var m = normalProps.get(fp);
628            if (m == null)
629               throw brex(c, "The property '%s' was defined on the @BeanCtor(properties=X) annotation but was not found on the class definition.", fp);
630            m.setAsConstructorArg();
631         }
632
633         // @BeanProp(required=true) must be satisfiable from input.
634         for (var p : normalProps.values())
635            if (p.isRequired() && ! p.canSatisfyRequired())
636               throw brex(c, "Property '%s' on class '%s' is marked @BeanProp(required=true) but cannot be written (read-only, dynamic, or no field, setter or constructor argument).", p.name, ci.getNameSimple());
637
638         // Make sure at least one property was found (records with no components are exempt).
639         if (bf == null && config.isBeansRequireSomeProperties() && normalProps.isEmpty() && ! ci.isRecord())
640            notABeanReasonTemp = "No properties detected on bean class";
641
642         // Records opt into component order via recordComponentOrder(); that is unsorted mode scoped to records.
643         var recordComponentOrder = ci.isRecord() && config.isRecordComponentOrder();
644         unsortedPropertiesTemp = recordComponentOrder || config.isUnsortedProperties() || bfo.map(x -> x.isUnsortedProperties()).orElse(false) || !fixedBeanProps.isEmpty();
645
646         // Declaration-order seeding.  Skipped when an explicit @BeanType(properties) list exists
647         // (that list keeps absolute precedence) and on the java.beans.Introspector path.
648         if (unsortedPropertiesTemp && fixedBeanProps.isEmpty() && ! config.isUseJavaBeanIntrospector())
649            orderByDeclaration(normalProps);
650
651         propertiesValue.set(unsortedPropertiesTemp ? map() : sortedMap());
652
653      normalProps.forEach((k, v) -> {
654         var pMeta = v.build();
655         if (pMeta.isDyna())
656            dynaPropertyValue.set(pMeta);
657         propertiesValue.get().put(k, pMeta);
658         // Build the property-level BeanRegistry side-map entry from the builder's accumulated
659         // dictionary classes.  Parents to the bean-level registry so @MarshalledProp(dictionary={})
660         // entries chain on top of bean and global dictionaries.  Skipped on the commons-side path
661         // (no marshallingContext means no BeanRegistry construction).
662         if (nn(marshallingContext) && nn(v.dictionaryClasses))
663            propertyBeanRegistriesTemp.put(pMeta, config.getBeanMetaInitializer().buildPropertyBeanRegistry(marshallingContext, beanRegistry.get(), v.dictionaryClasses));
664      });
665
666         // If a beanFilter is defined, look for inclusion and exclusion lists.
667         if (bf != null) {
668
669            // Eliminated excluded properties if MarshalledFilter.excludeKeys is specified.
670            var bfbpi = bf.getProperties();
671            var bfbpx = bf.getExcludeProperties();
672            var p = propertiesValue.get();
673
674            if (! bfbpi.isEmpty()) {
675               // Only include specified properties if MarshalledFilter.includeKeys is specified.
676               // Note that the order must match includeKeys.
677               Map<String,BeanPropertyMeta> p2 = map();  // NOAI
678               bfbpi.stream().filter(p::containsKey).forEach(x -> p2.put(x, p.remove(x)));
679               hiddenPropertiesMap.putAll(p);
680               propertiesValue.set(p2);
681            }
682
683            bfbpx.forEach(x -> hiddenPropertiesMap.put(x, propertiesValue.get().remove(x)));
684         }
685
686         if (nn(pNames)) {
687            var p = propertiesValue.get();
688            Map<String,BeanPropertyMeta> p2 = map();
689            for (var k : pNames) {
690               if (p.containsKey(k))
691                  p2.put(k, p.get(k));
692               else
693                  hiddenPropertiesMap.put(k, p.get(k));
694            }
695            propertiesValue.set(p2);
696         }
697
698      } catch (BeanRuntimeException e) {
699         throw e;
700      } catch (Exception e) {
701         notABeanReasonTemp = "Exception:  " + getStackTrace(e);
702      }
703
704      notABeanReason = notABeanReasonTemp;
705      properties = u(propertiesValue.get());
706      propertyArray = properties == null ? new BeanPropertyMeta[0] : properties.values().toArray(new BeanPropertyMeta[0]);
707      hiddenProperties = u(hiddenPropertiesMap);
708      getterProps = u(getterPropsMap);
709      setterProps = u(setterPropsMap);
710      dynaProperty = dynaPropertyValue.get();
711      unsortedProperties = unsortedPropertiesTemp;
712      requiredPropertyNames = properties == null ? List.<String>of() : properties.values().stream().filter(BeanPropertyMeta::isRequired).map(BeanPropertyMeta::getName).toList();
713      typeProperty = BeanPropertyMeta.builder(this, typePropertyName).canRead().canWrite().rawMetaType(String.class).build();
714      // Map the synthetic "_type" property to the bean-level BeanRegistry so consumers calling
715      // typeProperty.getBeanRegistry() (currently none in-tree, but a public API path) get the same
716      // registry the property previously carried as a field.  Skipped on the commons-side path.
717      if (nn(marshallingContext))
718         propertyBeanRegistriesTemp.put(typeProperty, beanRegistry.get());
719      propertyBeanRegistries = u(propertyBeanRegistriesTemp);
720      dictionaryName = memoize(this::findDictionaryName);
721      beanProxyInvocationHandler = memoize(() -> config.isUseInterfaceProxies() && classInfo.isInterface() ? new BeanProxyInvocationHandler<>(this) : null);
722      var factoryClassTemp = btList.stream().map(x -> x.inner().factory()).filter(x -> x != BeanFactory.Void.class).findFirst().orElse(null);
723      factoryClass = factoryClassTemp;
724   }
725
726   private void validateAndRegisterProperty(BeanPropertyMeta.Builder p, Class<?> c, TypeVariables typeVarImpls, Set<String> readOnlyProps, Set<String> writeOnlyProps, Iterator<BeanPropertyMeta.Builder> i, Map<Method,String> getterProps, Map<Method,String> setterProps) {
727      try {
728         if (p.field == null)
729            findInnerBeanField(p.name).ifPresent(p::setInnerField);
730
731         // When constructed via the commons-side path, marshallingContext is null and validate() runs in
732         // raw-reflection mode (no ClassMeta/ObjectSwap/BeanRegistry resolution).
733         if (p.validate((BeanTypeResolver) marshallingContext, typeVarImpls, readOnlyProps, writeOnlyProps)) {
734
735            // Marshalling-side post-processor — applies @MarshalledProp / @Swap annotation effects
736            // (swap detection, properties override, dictionary classes) and installs swap-aware
737            // read/write transforms.  Skipped on the commons-side path: those annotations require a
738            // MarshallingContext to resolve ObjectSwap/StringFormatSwap/Surrogate types and the swap
739            // class meta.
740            if (nn(marshallingContext))
741               config.getBeanPropertyPostProcessor().process(marshallingContext, p);
742
743            if (nn(p.getter))
744               getterProps.put(p.getter.inner(), p.name);
745
746            if (nn(p.setter))
747               setterProps.put(p.setter.inner(), p.name);
748
749         } else {
750            i.remove();
751         }
752      } catch (Exception e) {
753         throw brex(c, localizedMessage(e));
754      }
755   }
756
757   /**
758    * Re-keys the discovered properties into declaration order.
759    *
760    * <ul class='spaced-list'>
761    *    <li>Records: record component order ({@link ClassInfo#getRecordComponents()}, defined by the JLS).  A component
762    *       is matched to its property through the property's field or accessor name, so renamed components keep
763    *       their position.
764    *    <li>Other classes: the position of the property's backing field (the visible field, or the same-named private
765    *       field recorded as {@code innerField}) in a superclass-first walk of
766    *       {@link ClassInfo#getDeclaredFieldsInDeclarationOrder()}.
767    *    <li>Properties with no backing field or component (method-only) go after all ranked properties, alphabetically.
768    * </ul>
769    *
770    * <p>
771    * Visibility is unchanged: private fields only <i>rank</i> properties, they never become properties.
772    *
773    * @param props The discovered properties.  Re-ordered in place.
774    */
775   private void orderByDeclaration(Map<String,BeanPropertyMeta.Builder> props) {
776      var rank = new HashMap<String,Integer>();
777      if (classInfo.isRecord()) {
778         var components = classInfo.getRecordComponents();
779         for (var i = 0; i < components.size(); i++) {
780            var rcName = components.get(i).getName();
781            for (var e : props.entrySet()) {
782               var p = e.getValue();
783               if ((nn(p.field) && p.field.hasName(rcName)) || (nn(p.getter) && p.getter.hasName(rcName)))
784                  rank.putIfAbsent(e.getKey(), i);
785            }
786         }
787      } else {
788         var fieldRank = new HashMap<Field,Integer>();
789         for (var c2 : classHierarchy.get())
790            for (var f : c2.getDeclaredFieldsInDeclarationOrder())
791               fieldRank.putIfAbsent(f.inner(), fieldRank.size());
792         props.forEach((k, p) -> {
793            var f = nn(p.field) ? p.field : p.innerField;
794            if (nn(f) && fieldRank.containsKey(f.inner()))
795               rank.put(k, fieldRank.get(f.inner()));
796         });
797      }
798      var keys = new ArrayList<>(props.keySet());
799      keys.sort(Comparator.<String>comparingInt(k -> rank.getOrDefault(k, Integer.MAX_VALUE)).thenComparing(Comparator.naturalOrder()));
800      var copy = new LinkedHashMap<>(props);
801      props.clear();
802      keys.forEach(k -> props.put(k, copy.get(k)));
803   }
804
805   @Override /* Overridden from Object */
806   public boolean equals(Object o) {
807      return (o instanceof BeanMeta<?> o2) && eq(this, o2, (x, y) -> eq(x.classInfo, y.classInfo));
808   }
809
810   /**
811    * Returns the bean filter associated with this bean.
812    *
813    * <p>
814    * Bean filters are used to control aspects of how beans are handled during serialization and parsing, such as
815    * property inclusion/exclusion, property ordering, and type name mapping.
816    *
817    * <p>
818    * On the marshalling-side construction path, the bean filter is typically built from the {@code @Marshalled} and
819    * {@link BeanType @BeanType} annotations on the class.  If neither annotation is present (or this {@link BeanMeta}
820    * was built via the commons-side path), this method returns <jk>null</jk>.
821    *
822    * @return The bean filter for this bean, or <jk>null</jk> if no bean filter is associated with this bean.
823    */
824   public BeanFilter getBeanFilter() {
825      // The field is typed as the bean-modeling-side BeanFilter SPI; marshalling-side callers
826      // narrow back to MarshalledFilter (the only concrete implementation in-tree).
827      return beanFilter;
828   }
829
830   /**
831    * Returns the proxy invocation handler for this bean if it's an interface.
832    *
833    * @return The invocation handler, or <jk>null</jk> if this is not an interface or interface proxies are disabled.
834    */
835   public InvocationHandler getBeanProxyInvocationHandler() {
836      return beanProxyInvocationHandler.get();
837   }
838
839   /**
840    * Returns the bean registry for this bean.
841    *
842    * <p>
843    * The bean registry is used to resolve dictionary names to class types. It's created when a bean class has a
844    * {@code @Marshalled(dictionary)} annotation that specifies a list of possible subclasses.
845    *
846    * <p>
847    * Returns the bean-modeling-side SPI type ({@link BeanRegistryLookup}).
848    *
849    * @return The bean registry for this bean, or <jk>null</jk> if no bean registry is associated with it.
850    */
851   public BeanRegistryLookup getBeanRegistry() { return beanRegistry.get(); }
852
853   /**
854    * Returns the per-property registry lookup associated with the given {@link BeanPropertyMeta}.
855    *
856    * <p>
857    * {@link BeanPropertyMeta} no longer carries a concrete registry field — the per-property
858    * registry now lives in a side-map on this {@link BeanMeta} keyed by the property meta itself.  The serializer
859    * and parser sides still need to look up the property-level registry for polymorphic dispatch; they now route through
860    * this accessor (directly or via the deprecated-style {@link BeanPropertyMeta#getBeanRegistry()} delegate).
861    *
862    * <p>
863    * The returned registry chains the property's
864    * {@code @MarshalledProp(dictionary)} entries on top of
865    * the bean-level registry, which in turn chains on top of the global bean dictionary.
866    *
867    * @param p The bean property meta to look up.  Can be a normal property, the synthetic <js>"_type"</js> property,
868    *    or any other property meta produced by this bean.
869    * @return The bean registry for the specified property, or <jk>null</jk> if none was registered (e.g. the property
870    *    belongs to a different bean meta).
871    */
872   public BeanRegistryLookup getPropertyBeanRegistry(BeanPropertyMeta p) {
873      return propertyBeanRegistries.get(p);
874   }
875
876   /**
877    * Returns the {@link BeanInfo} of this bean.
878    *
879    * <p>
880    * Returns <jk>null</jk> when this {@link BeanMeta} was constructed via the commons-side path
881    * ({@link #of(Class, BeanConfigContext)}) — in that case {@link #getClassInfo()} carries the
882    * pure-reflection view of the bean class.
883    *
884    * <p>
885    * Returns the bean-modeling-side SPI type ({@link BeanInfo}).
886    *
887    * @return The {@link BeanInfo} of this bean, or <jk>null</jk> for bean-modeling-only construction.
888    */
889   public BeanInfo<T> getBeanInfo() { return classMeta; }
890
891   /**
892    * Returns the {@link ClassInfo} of this bean.
893    *
894    * <p>
895    * Pure-reflection view of the bean class.  Always non-null, regardless of construction path.  Use this in
896    * preference to {@link #getBeanInfo()} for any bean-modeling read that does not depend on marshalling-aware
897    * type metadata; the commons-side construction path leaves {@link #getBeanInfo()} <jk>null</jk>.
898    *
899    * @return The class info for this bean.  Never <jk>null</jk>.
900    */
901   public ClassInfo getClassInfo() { return classInfo; }
902
903   /**
904    * Returns the bean-modeling configuration snapshot used to build this {@link BeanMeta}.
905    *
906    * <p>
907    * For marshalling-side construction, this is sourced from the owning marshalling context.
908    * For commons-side construction (via {@link #of(Class, BeanConfigContext)}), this is the {@link BeanConfigContext}
909    * passed to the factory.  Always non-null.
910    *
911    * @return The bean-modeling configuration.  Never <jk>null</jk>.
912    */
913   public BeanConfigContext getConfig() { return config; }
914
915   /**
916    * Returns the dictionary name for this bean as defined through the {@code @Marshalled(typeName)} annotation.
917    *
918    * @return The dictionary name for this bean, or <jk>null</jk> if it has no dictionary name defined.
919    */
920   public String getDictionaryName() { return dictionaryName.get(); }
921
922   /**
923    * Returns a map of all properties on this bean.
924    *
925    * <p>
926    * The map is keyed by property name and contains {@link BeanPropertyMeta} objects that provide metadata about each
927    * property, including its type, getter/setter methods, field information, and serialization/parsing behavior.
928    *
929    * <p>
930    * This map contains only the normal (non-hidden) properties of the bean. Hidden properties can be accessed via
931    * {@link #getHiddenProperties()}.
932    *
933    * @return A map of property names to their metadata. The map is unmodifiable.
934    * @see #getPropertyMeta(String)
935    * @see #getHiddenProperties()
936    */
937   public Map<String,BeanPropertyMeta> getProperties() { return properties; }
938
939   /**
940    * Returns the properties as an array in property order.
941    *
942    * <p>
943    * The returned array is shared and must not be modified.
944    *
945    * @return The properties array.
946    */
947   BeanPropertyMeta[] getPropertyArray() { return propertyArray; }
948
949   /**
950    * Returns metadata about the specified property.
951    *
952    * @param name The name of the property on this bean.  Can be <jk>null</jk> to return the dynamic (<js>"*"</js>) property.
953    * @return The metadata about the property, or <jk>null</jk> if no such property exists on this bean.
954    */
955   public BeanPropertyMeta getPropertyMeta(String name) {
956      if (name == null)
957         return dynaProperty;
958      var bpm = properties.get(name);
959      if (bpm == null)
960         bpm = hiddenProperties.get(name);
961      if (bpm == null)
962         bpm = dynaProperty;
963      return bpm;
964   }
965
966   /**
967    * Returns a mock bean property that resolves to the name <js>"_type"</js> and whose value always resolves to the
968    * dictionary name of the bean.
969    *
970    * @return The type name property.
971    */
972   public BeanPropertyMeta getTypeProperty() { return typeProperty; }
973
974   /**
975    * Returns the type property name for this bean.
976    *
977    * <p>
978    * This is the name of the bean property used to store the dictionary name of a bean type so that the parser knows
979    * the data type to reconstruct.
980    *
981    * <p>
982    * If <jk>null</jk>, <js>"_type"</js> should be assumed.
983    *
984    * <p>
985    * The value is determined from:
986    * <ul>
987    *    <li>The marshalling-side {@code @Marshalled(typePropertyName)} on the class, if present
988    *       (resolved via {@link BeanMetaInitializer#resolveTypePropertyName(BeanConfigContext, ClassInfo)}).
989    *    <li>Otherwise, the default from {@link BeanConfigContext#getBeanTypePropertyName()}.
990    * </ul>
991    *
992    * @return
993    *    The type property name associated with this bean, or <jk>null</jk> if the default <js>"_type"</js> should be used.
994    * @see BeanConfigContext#getBeanTypePropertyName()
995    */
996   public String getTypePropertyName() { return typePropertyName; }
997
998   @Override /* Overridden from Object */
999   public int hashCode() {
1000      return classInfo.hashCode();
1001   }
1002
1003   /**
1004    * Property read interceptor.
1005    *
1006    * <p>
1007    * Called immediately after calling the getter to allow the value to be overridden.
1008    *
1009    * @param bean The bean from which the property was read.
1010    * @param name The property name.
1011    * @param value The value just extracted from calling the bean getter.
1012    * @return The value to serialize.  Default is just to return the existing value.
1013    */
1014   public Object onReadProperty(Object bean, String name, Object value) {
1015      return beanFilter == null ? value : beanFilter.readProperty(bean, name, value);
1016   }
1017
1018   /**
1019    * Property write interceptor.
1020    *
1021    * <p>
1022    * Called immediately before calling theh setter to allow value to be overwridden.
1023    *
1024    * @param bean The bean from which the property was read.
1025    * @param name The property name.
1026    * @param value The value just parsed.
1027    * @return The value to serialize.  Default is just to return the existing value.
1028    */
1029   public Object onWriteProperty(Object bean, String name, Object value) {
1030      return beanFilter == null ? value : beanFilter.writeProperty(bean, name, value);
1031   }
1032
1033   protected FluentMap<String,Object> properties() {
1034      // @formatter:off
1035      return filteredBeanPropertyMap()
1036         .a("class", classInfo.getName())
1037         .a("properties", properties);
1038      // @formatter:on
1039   }
1040
1041   @Override /* Overridden from Object */
1042   public String toString() {
1043      return r(properties());
1044   }
1045
1046   /**
1047    * Returns the bean-modeling {@link BeanTypeResolver} that this metadata was constructed against.
1048    *
1049    * <p>
1050    * On the marshalling-side construction path, the underlying object is the marshalling context (which
1051    * implements {@link BeanTypeResolver}); marshalling-side callers that need the concrete narrowing must cast
1052    * (e.g. {@code (MarshallingContext) bm.getBeanTypeResolver()}).
1053    *
1054    * <p>
1055    * Returns <jk>null</jk> when this {@link BeanMeta} was constructed via the commons-side path
1056    * ({@link #of(Class, BeanConfigContext)}).  Use {@link #getConfig()} for the always-non-null bean-modeling
1057    * configuration facade.
1058    *
1059    * @return The bean-modeling type resolver, or <jk>null</jk> for bean-modeling-only construction.
1060    */
1061   protected BeanTypeResolver getBeanTypeResolver() { return (BeanTypeResolver) marshallingContext; }
1062
1063   /**
1064    * Returns the constructor for this bean, if one was found.
1065    *
1066    * <p>
1067    * The constructor is determined by {@link #findBeanConstructor()} and may be:
1068    * <ul>
1069    *    <li>A constructor annotated with {@link BeanCtor @BeanCtor}
1070    *    <li>An implementation class constructor (if provided)
1071    *    <li>A no-argument constructor
1072    *    <li><jk>null</jk> if no suitable constructor was found
1073    * </ul>
1074    *
1075    * @return The constructor for this bean, or <jk>null</jk> if no constructor is available.
1076    * @see #getConstructorArgs()
1077    * @see #hasConstructor()
1078    */
1079   public ConstructorInfo getConstructor() {
1080      return beanConstructor.constructor().orElse(null);
1081   }
1082
1083   /**
1084    * Returns the list of property names that correspond to the constructor parameters.
1085    *
1086    * <p>
1087    * The property names are in the same order as the constructor parameters. These names are used to map
1088    * parsed property values to constructor arguments when creating bean instances.
1089    *
1090    * <p>
1091    * The property names are determined from:
1092    * <ul>
1093    *    <li>The {@link BeanCtor#properties() properties()} value in the {@link BeanCtor @BeanCtor} annotation (if present)
1094    *    <li>Otherwise, the parameter names from the constructor (if available in bytecode)
1095    * </ul>
1096    *
1097    * <p>
1098    * If the bean has no constructor or uses a no-argument constructor, this list will be empty.
1099    *
1100    * @return A list of property names corresponding to constructor parameters, in parameter order.
1101    * @see #getConstructor()
1102    * @see #hasConstructor()
1103    */
1104   public List<String> getConstructorArgs() {
1105      return beanConstructor.args();
1106   }
1107
1108   /**
1109    * Returns the "extras" property for dynamic bean properties.
1110    *
1111    * @return The dynamic property, or <jk>null</jk> if not present.
1112    */
1113   protected BeanPropertyMeta getDynaProperty() { return dynaProperty; }
1114
1115   /**
1116    * Returns the map of getter methods to property names.
1117    *
1118    * @return The getter properties map.
1119    */
1120   protected Map<Method,String> getGetterProps() { return getterProps; }
1121
1122   /**
1123    * Returns the map of hidden properties on this bean.
1124    *
1125    * <p>
1126    * Hidden properties are properties that exist on the bean but are not included in the normal property list.
1127    * These properties are typically excluded from serialization but may still be accessible programmatically.
1128    *
1129    * <p>
1130    * Hidden properties can be defined through:
1131    * <ul>
1132    *    <li>{@link BeanFilter#getExcludeProperties() Bean filter exclude properties}
1133    *    <li>Properties that fail validation during bean metadata creation
1134    * </ul>
1135    *
1136    * @return A map of hidden property names to their metadata. The map is unmodifiable.
1137    * @see #getProperties()
1138    */
1139   protected Map<String,BeanPropertyMeta> getHiddenProperties() {
1140      return hiddenProperties;
1141   }
1142
1143   /**
1144    * Returns the map of setter methods to property names.
1145    *
1146    * @return The setter properties map.
1147    */
1148   protected Map<Method,String> getSetterProps() { return setterProps; }
1149
1150   /**
1151    * Returns whether this bean has a constructor available for instantiation.
1152    *
1153    * <p>
1154    * A bean has a constructor if {@link #findBeanConstructor()} was able to find a suitable constructor,
1155    * which may be:
1156    * <ul>
1157    *    <li>A constructor annotated with {@link BeanCtor @BeanCtor}
1158    *    <li>An implementation class constructor (if provided)
1159    *    <li>A no-argument constructor
1160    * </ul>
1161    *
1162    * <p>
1163    * If this method returns <jk>false</jk>, the bean cannot be instantiated using {@link #newBean(Object)},
1164    * and may need to be created through other means (e.g., interface proxies).
1165    *
1166    * @return <jk>true</jk> if a constructor is available, <jk>false</jk> otherwise.
1167    * @see #getConstructor()
1168    * @see #newBean(Object)
1169    */
1170   public boolean hasConstructor() {
1171      return beanConstructor.constructor().isPresent();
1172   }
1173
1174   /**
1175    * Returns whether any property of this bean is marked {@code @BeanProp(required=true)}.
1176    *
1177    * @return <jk>true</jk> if at least one property is required.
1178    */
1179   public boolean hasRequiredProperties() { return ! requiredPropertyNames.isEmpty(); }
1180
1181   /**
1182    * Returns the names of the properties marked {@code @BeanProp(required=true)}.
1183    *
1184    * <p>
1185    * Hidden and excluded properties are not included, since input can never set them.
1186    *
1187    * <h5 class='section'>Example:</h5>
1188    * <p class='bjava'>
1189    *    <jk>public record</jk> Person(<ja>@BeanProp</ja>(required=<jk>true</jk>) String <jv>name</jv>, Integer <jv>age</jv>) {}
1190    *
1191    *    <jc>// [name]</jc>
1192    *    List&lt;String&gt; <jv>required</jv> = BeanMeta.<jsm>of</jsm>(Person.<jk>class</jk>, BeanConfigContext.<jsf>DEFAULT</jsf>).getRequiredPropertyNames();
1193    * </p>
1194    *
1195    * @return An unmodifiable list of property names, in property order.  Never <jk>null</jk>.
1196    */
1197   public List<String> getRequiredPropertyNames() { return requiredPropertyNames; }
1198
1199   /**
1200    * Returns whether this bean has opted out of alphabetical property sorting.
1201    *
1202    * @return <jk>true</jk> if properties use declaration order (see the class Javadoc).
1203    */
1204   protected boolean isUnsortedProperties() { return unsortedProperties; }
1205
1206   /**
1207    * Creates a new instance of this bean.
1208    *
1209    * @param outer The outer object if bean class is a non-static inner member class.  Can be <jk>null</jk> if not applicable.
1210    * @return A new instance of this bean if possible, or <jk>null</jk> if not.
1211    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
1212    */
1213   public T newBean(Object outer) throws ExecutableException {
1214      if (factoryClass != null) {
1215         try {
1216            BeanFactory factory = resolveFactory(factoryClass);
1217            return (T) factory.create();
1218         } catch (ExecutableException e) {
1219            throw e;
1220         } catch (Exception e) {
1221            throw new ExecutableException(e);
1222         }
1223      }
1224      if (classInfo.isMemberClass() && classInfo.isNotStatic()) {
1225         if (hasConstructor())
1226            return getConstructor().<T>newInstance(outer);
1227      } else {
1228         if (hasConstructor())
1229            return getConstructor().<T>newInstance();
1230         var h = beanProxyInvocationHandler.get();
1231         if (nn(h)) {
1232            var inner = classInfo.inner();
1233            return (T)Proxy.newProxyInstance(inner.getClassLoader(), a(inner, Serializable.class), h);
1234         }
1235      }
1236      return null;
1237   }
1238
1239   private BeanFactory resolveFactory(Class<? extends BeanFactory> fc) {
1240      var bs = config.getBeanStore();
1241      if (bs != null) {
1242         var opt = bs.getBean(fc);
1243         if (opt.isPresent())
1244            return opt.get();
1245      }
1246      return (BeanFactory) BeanInstantiator.of((Class)fc).run();
1247   }
1248
1249   /**
1250    * Finds the appropriate constructor for this bean and determines the property names for constructor arguments.
1251    *
1252    * <p>
1253    * This method searches for a constructor in the following order of precedence:
1254    * <ol>
1255    *    <li><b>{@link BeanCtor @BeanCtor} annotated constructor:</b> If a constructor is annotated with {@link BeanCtor @BeanCtor},
1256    *       it is used. The property names are determined from:
1257    *       <ul>
1258    *          <li>The {@link BeanCtor#properties() properties()} value in the annotation, if specified
1259    *          <li>Otherwise, the parameter names from the constructor (if available in bytecode)
1260    *       </ul>
1261    *       If multiple constructors are annotated with {@link BeanCtor @BeanCtor}, an exception is thrown.
1262    *    <li><b>Implementation class constructor:</b> If an {@link #implClassConstructor} was provided during bean
1263    *       metadata creation, it is used with an empty property list.
1264    *    <li><b>No-arg constructor:</b> Searches for a no-argument constructor. The visibility required depends on
1265    *       whether the class carries a bean-registration annotation (e.g. {@link BeanType @BeanType} or the
1266    *       marshalling-side {@code @Marshalled}) — see
1267    *       {@link BeanMetaInitializer#hasBeanRegistrationAnnotation(BeanConfigContext, ClassInfo)}:
1268    *       <ul>
1269    *          <li>If a registration annotation is present, private constructors are allowed
1270    *          <li>Otherwise, the visibility is determined by {@link BeanConfigContext#getBeanConstructorVisibility()}
1271    *       </ul>
1272    *    <li><b>No constructor:</b> Returns an empty {@link Optional} if no suitable constructor is found.
1273    * </ol>
1274    *
1275    * <p>
1276    * The returned {@link BeanConstructor} contains:
1277    * <ul>
1278    *    <li>The constructor (if found), wrapped in an {@link Optional}
1279    *    <li>A list of property names that correspond to the constructor parameters, in order
1280    * </ul>
1281    *
1282    * @return A {@link BeanConstructor} containing the found constructor and its associated property names.
1283    * @throws BeanRuntimeException If multiple constructors are annotated with {@link BeanCtor @BeanCtor}, or if
1284    *    the number of properties specified in {@link BeanCtor @BeanCtor} doesn't match the number of constructor parameters,
1285    *    or if parameter names cannot be determined from the bytecode.
1286    */
1287   private BeanConstructor findBeanConstructor() {
1288      var ap = config.getAnnotationProvider();
1289      var vis = config.getBeanConstructorVisibility();
1290      var ci = classInfo;
1291
1292      var l = ci.getPublicConstructors().stream().filter(x -> ap.has(BeanCtor.class, x)).toList();
1293      if (l.isEmpty())
1294         l = ci.getDeclaredConstructors().stream().filter(x -> ap.has(BeanCtor.class, x)).toList();
1295      if (l.size() > 1)
1296         throw brex(ci, "Multiple instances of '@BeanCtor' found.");
1297      if (l.size() == 1) {
1298         var con = first(l).orElseThrow(() -> brex(ci, "No constructor found.")).accessible();
1299         var args = ap.find(BeanCtor.class, con).stream().map(x -> x.inner().properties()).filter(StringUtils::isNotBlank).map(x -> split(x)).findFirst().orElse(emptyList());
1300         if (! con.hasNumParameters(args.size())) {
1301            if (ine(args))
1302               throw brex(ci, "Number of properties defined in '@BeanCtor' annotation does not match number of parameters in constructor.");
1303            args = con.getParameters().stream().map(x -> x.getName()).toList();
1304            for (int i = 0; i < args.size(); i++) {
1305               if (isBlank(args.get(i)))
1306                  throw brex(ci, "Could not find name for parameter #%s of constructor '%s'", i, con.getNameFull());
1307            }
1308         }
1309         return new BeanConstructor(o(con), args);
1310      }
1311
1312      if (ci.isRecord()) {
1313         var components = ci.getRecordComponents();
1314         var paramTypes = components.stream().map(RecordComponent::getType).toArray(Class[]::new);
1315         // A record's canonical constructor has the same access as the record class (JLS 8.10.4), so look it up
1316         // among the declared constructors; the bean-class visibility check already governs the record itself.
1317         var rcon = ci.getDeclaredConstructor(x -> x.hasParameterTypes(paramTypes)).orElse(null);
1318         if (rcon != null)
1319            return new BeanConstructor(o(makeRecordConstructorAccessible(ci, rcon)), components.stream().map(RecordComponent::getName).toList());
1320      }
1321
1322      if (implClassConstructor != null)
1323         return new BeanConstructor(o(implClassConstructor.accessible()), emptyList());
1324
1325      var con = ci.getNoArgConstructor(config.getBeanMetaInitializer().hasBeanRegistrationAnnotation(config, classInfo) ? Visibility.PRIVATE : vis).orElse(null);
1326      if (con != null)
1327         return new BeanConstructor(o(con.accessible()), emptyList());
1328
1329      return new BeanConstructor(oe(), emptyList());
1330   }
1331
1332   /**
1333    * Resolves the bean property name for a field when {@link BeanProp @BeanProp} or {@link Name @Name} supplies
1334    * <js>"*"</js>.
1335    *
1336    * <p>
1337    * On a {@link Map} field, <js>"*"</js> remains the dyna-property name. On any other field type, <js>"*"</js> is
1338    * treated like an unnamed {@code @BeanProp} (apply other attributes) but the property name is taken from the field
1339    * (via {@link PropertyNamer#getPropertyName(String)}).
1340    * </p>
1341    *
1342    * @param x Field being registered.
1343    * @param nameFromAnnotations Name from {@code @BeanProp}/{@code @Name}, or already-resolved default from the field name.
1344    * @param propertyNamer Namer for raw field names.
1345    * @return Property key to use in {@link BeanMeta}.
1346    */
1347   private static String resolveBeanFieldPropertyName(FieldInfo x, String nameFromAnnotations, PropertyNamer propertyNamer) {
1348      if (neq(nameFromAnnotations, "*"))
1349         return nameFromAnnotations;
1350      if (x.getFieldType().isAssignableTo(Map.class))
1351         return "*";
1352      return propertyNamer.getPropertyName(x.getName());
1353   }
1354
1355   /**
1356    * Finds all bean fields in the class hierarchy.
1357    *
1358    * <p>
1359    * Traverses the complete class hierarchy (as defined by {@link #classHierarchy}) and collects all fields that
1360    * meet the bean field criteria:
1361    * <ul>
1362    *    <li>Not static
1363    *    <li>Not transient (unless transient fields are not ignored)
1364    *    <li>Not annotated with {@link Transient @Transient} (unless transient fields are not ignored)
1365    *    <li>Not annotated with {@link BeanIgnore @BeanIgnore}
1366    *    <li>Visible according to the specified visibility level, or annotated with {@link BeanProp @BeanProp}
1367    * </ul>
1368    *
1369    * @return A collection of all bean fields found in the class hierarchy.
1370    */
1371   private Collection<FieldInfo> findBeanFields() {
1372      var v = config.getBeanFieldVisibility();
1373      var noIgnoreTransients = ! config.isIgnoreTransientFields();
1374      var ap = config.getAnnotationProvider();
1375      var isRecord = classInfo.isRecord();
1376      var recordComponentNames = isRecord
1377         ? classInfo.getRecordComponents().stream().map(RecordComponent::getName).collect(java.util.stream.Collectors.toSet())
1378         : Set.<String>of();
1379      // @formatter:off
1380      return classHierarchy.get().stream()
1381         .flatMap(c2 -> c2.getDeclaredFields().stream())
1382         .filter(x -> x.isNotStatic()
1383            && (x.isNotTransient() || noIgnoreTransients)
1384            && (! x.hasAnnotation(Transient.class) || noIgnoreTransients)
1385            && ! ap.has(BeanIgnore.class, x)
1386            && (v.isVisible(x.inner()) || ap.has(BeanProp.class, x)
1387               || (isRecord && recordComponentNames.contains(x.getName()))))
1388         .toList();
1389      // @formatter:on
1390   }
1391
1392   /**
1393    * Finds all bean methods (getters, setters, and extraKeys) in the class hierarchy.
1394    *
1395    * <p>
1396    * Traverses the complete class hierarchy (as defined by {@link #classHierarchy}) and identifies methods that
1397    * represent bean properties. Methods are identified as:
1398    * <ul>
1399    *    <li><b>Getters:</b> Methods with no parameters that return a value, matching patterns like:
1400    *       <ul>
1401    *          <li><c>getX()</c> - standard getter pattern
1402    *          <li><c>isX()</c> - boolean getter pattern (returns boolean or Boolean)
1403    *          <li>Methods annotated with {@link BeanProp @BeanProp} or {@link Name @Name}
1404    *          <li>Methods with {@link BeanProp @BeanProp} annotation with value <js>"*"</js> that return a Map
1405    *       </ul>
1406    *    <li><b>Setters:</b> Methods with one parameter that match patterns like:
1407    *       <ul>
1408    *          <li><c>setX(value)</c> - standard setter pattern
1409    *          <li><c>withX(value)</c> - fluent setter pattern (returns the bean type)
1410    *          <li>Methods annotated with {@link BeanProp @BeanProp} or {@link Name @Name}
1411    *          <li>Methods with {@link BeanProp @BeanProp} annotation with value <js>"*"</js> that accept a Map
1412    *          <li>Fluent setters (if enabled) - methods that return the bean type and accept one parameter
1413    *       </ul>
1414    *    <li><b>ExtraKeys:</b> Methods with {@link BeanProp @BeanProp} annotation with value <js>"*"</js> that return a Collection
1415    * </ul>
1416    *
1417    * <p>
1418    * Methods are filtered based on:
1419    * <ul>
1420    *    <li>Not static, not bridge methods
1421    *    <li>Parameter count ≤ 2
1422    *    <li>Not annotated with {@link BeanIgnore @BeanIgnore}
1423    *    <li>Not annotated with {@link Transient @Transient}
1424    *    <li>Visible according to the specified visibility level, or annotated with {@link BeanProp @BeanProp} or {@link Name @Name}
1425    * </ul>
1426    *
1427    * <p>
1428    * Property names are determined from:
1429    * <ul>
1430    *    <li>{@link BeanProp @BeanProp} or {@link Name @Name} annotations (if present)
1431    *    <li>Otherwise, derived from the method name using the provided {@link PropertyNamer}
1432    * </ul>
1433    *
1434    * @return A list of {@link BeanMethod} objects representing all found bean methods.
1435    */
1436   private List<BeanMethod> findBeanMethods() {
1437      var l = new LinkedList<BeanMethod>();
1438      var ap = config.getAnnotationProvider();
1439      var ci = classInfo;
1440      var v = config.getBeanMethodVisibility();
1441      var pn = effectivePropertyNamer();
1442      var suppressedFromBeanIgnoredFields = findSuppressedPropertyNamesFromIgnoredFields(pn);
1443
1444      classHierarchy.get().forEach(c2 -> {
1445         for (var m : c2.getDeclaredMethods()) {
1446            var mm = m.getMatchingMethods();
1447            var beanps = ap.find(BeanProp.class, m).stream().map(AnnotationInfo::inner).toList();
1448            var names = ap.find(Name.class, m).stream().map(AnnotationInfo::inner).toList();
1449            // Skip static, bridge, or methods with >2 params; skip if ignored, transient, or not visible
1450            if (m.isStatic() || m.isBridge() || m.getParameterCount() > 2
1451               || mm.stream().anyMatch(m2 -> ap.has(BeanIgnore.class, m2, SELF))
1452               || mm.stream().anyMatch(m2 -> ap.find(Transient.class, m2, SELF).stream().map(x -> x.inner().value()).findFirst().orElse(false))
1453               || ! (m.isVisible(v) || ine(beanps) || ine(names)))
1454               continue;
1455
1456            var n = m.getNameSimple();
1457
1458            var params = m.getParameters();
1459            var rt = m.getReturnType();
1460            var methodType = UNKNOWN;
1461            var bpName = bpName(beanps, names);
1462
1463            if (params.isEmpty()) {
1464               if (eq(bpName, "*")) {
1465                  if (rt.isAssignableTo(Collection.class)) {
1466                     methodType = EXTRAKEYS;
1467                  } else if (rt.isAssignableTo(Map.class)) {
1468                     methodType = GETTER;
1469                  }
1470                  n = bpName;
1471               } else if (n.startsWith("get") && (! rt.is(Void.TYPE))) {
1472                  methodType = GETTER;
1473                  n = n.substring(3);
1474               } else if (n.startsWith("is") && (rt.is(Boolean.TYPE) || rt.is(Boolean.class))) {
1475                  methodType = GETTER;
1476                  n = n.substring(2);
1477               } else if (ci.isRecord() && isRecordAccessor(m, ci)) {
1478                  methodType = GETTER;
1479               } else if (nn(bpName)) {
1480                  methodType = GETTER;
1481                  if (bpName.isEmpty()) {
1482                     if (n.startsWith("get"))
1483                        n = n.substring(3);
1484                     else if (n.startsWith("is"))
1485                        n = n.substring(2);
1486                     bpName = n;
1487                  } else {
1488                     n = bpName;
1489                  }
1490               }
1491            } else if (params.size() == 1) {
1492               if (eq(bpName, "*")) {
1493                  if (params.get(0).getParameterType().isAssignableTo(Map.class)) {
1494                     methodType = SETTER;
1495                     n = bpName;
1496                  } else if (params.get(0).getParameterType().is(String.class)) {
1497                     methodType = GETTER;
1498                     n = bpName;
1499                  }
1500               } else if (n.startsWith("set") && (rt.isAssignableFrom(ci) || rt.is(Void.TYPE))) {
1501                  methodType = SETTER;
1502                  n = n.substring(3);
1503               } else if (n.startsWith("with") && (rt.isAssignableFrom(ci))) {
1504                  methodType = SETTER;
1505                  n = n.substring(4);
1506               } else if (nn(bpName)) {
1507                  methodType = SETTER;
1508                  if (bpName.isEmpty()) {
1509                     if (n.startsWith("set"))
1510                        n = n.substring(3);
1511                     bpName = n;
1512                  } else {
1513                     n = bpName;
1514                  }
1515               } else if (fluentSetters && rt.isAssignableFrom(ci)) {
1516                  methodType = SETTER;
1517               }
1518            } else if (params.size() == 2) {
1519               if (eq(bpName, "*") && params.get(0).getParameterType().is(String.class) && n.startsWith("set") && (rt.isAssignableFrom(ci) || rt.is(Void.TYPE))) {
1520                  methodType = SETTER;
1521               } else {
1522                  methodType = GETTER;
1523               }
1524               n = bpName;
1525            }
1526            n = pn.getPropertyName(n);
1527
1528            if (eq(bpName, "*") && methodType == UNKNOWN)
1529               throw brex(ci, "Found @BeanProp(\"*\") but could not determine method type on method '%s'.", m.getNameSimple());
1530
1531            if (methodType != UNKNOWN) {
1532               if (nn(bpName) && ! bpName.isEmpty())
1533                  n = bpName;
1534               if (nn(n) && ! suppressedFromBeanIgnoredFields.contains(n))
1535                  l.add(new BeanMethod(n, methodType, m.inner()));
1536            }
1537         }
1538      });
1539      return l;
1540   }
1541
1542   /**
1543    * Makes a record's canonical constructor accessible, failing with a clear message if the module system refuses.
1544    *
1545    * @param ci The record class.
1546    * @param rcon The canonical constructor.
1547    * @return The same constructor.
1548    * @throws BeanRuntimeException If the record is non-public and its package is not opened to Juneau.
1549    */
1550   static ConstructorInfo makeRecordConstructorAccessible(ClassInfo ci, ConstructorInfo rcon) {
1551      return makeRecordConstructorAccessible(ci, rcon, ConstructorInfo::setAccessible);
1552   }
1553
1554   // Package-private seam so tests can force setAccessible() to report failure.
1555   static ConstructorInfo makeRecordConstructorAccessible(ClassInfo ci, ConstructorInfo rcon, Predicate<ConstructorInfo> accessor) {
1556      // Always attempt to make the constructor accessible, since a public record nested in a non-public class still needs it.
1557      // Only fail loudly when the record is not effectively public.
1558      var ok = accessor.test(rcon);
1559      if (! ok && ! (rcon.isPublic() && isEffectivelyPublic(ci))) {
1560         var pkg = ci.getPackage() == null ? "" : ci.getPackage().getName();
1561         throw brex(ci, "Could not make the canonical constructor of non-public record '%s' accessible. The package '%s' must be opened to Juneau, e.g. 'opens %s to org.apache.juneau.commons;' in module-info or '--add-opens <module>/%s=ALL-UNNAMED'.", ci.getNameFull(), pkg, pkg, pkg);
1562      }
1563      return rcon;
1564   }
1565
1566   private static boolean isEffectivelyPublic(ClassInfo ci) {
1567      for (Class<?> c = ci.inner(); c != null; c = c.getEnclosingClass())
1568         if (! Modifier.isPublic(c.getModifiers()))
1569            return false;
1570      return true;
1571   }
1572
1573   private static boolean isRecordAccessor(MethodInfo m, ClassInfo ci) {
1574      return ci.getRecordComponents().stream()
1575         .anyMatch(rc -> rc.getName().equals(m.getNameSimple())
1576            && rc.getType().equals(m.getReturnType().inner()));
1577   }
1578
1579   /**
1580    * Creates a bean registry for this bean class.
1581    *
1582    * <p>
1583    * The bean registry is used to resolve dictionary names (type names) to actual class types. This is essential
1584    * for polymorphic bean serialization and parsing, where a dictionary name in the serialized form needs to be
1585    * mapped back to the correct subclass.
1586    *
1587    * <p>
1588    * The registry is built from:
1589    * <ul>
1590    *    <li><b>Bean filter dictionary:</b> Classes specified in the {@link BeanFilter#getBeanDictionary() bean filter's dictionary}
1591    *       (if a bean filter is present)
1592    *    <li><b>{@code @Marshalled} annotation:</b> If the class has a {@code @Marshalled} annotation with a non-empty
1593    *       {@code typeName()}, the class itself is added to the dictionary
1594    * </ul>
1595    *
1596    * <p>
1597    * The registry is used by parsers to determine which class to instantiate when deserializing polymorphic beans.
1598    *
1599    * @return A new registry lookup containing the dictionary classes for this bean, or an empty lookup if
1600    *    no dictionary classes are found.
1601    */
1602   private BeanRegistryLookup findBeanRegistry() {
1603      // BeanRegistry is a marshalling-side concern (polymorphic dispatch via @Marshalled(typeName)/dictionary).
1604      // Lifted out to the BeanMetaInitializer SPI carried on BeanConfigContext so this class no longer references
1605      // MarshalledBeanMetaInitializer directly.  Returns null on the commons-side path (no marshallingContext).
1606      return config.getBeanMetaInitializer().buildBeanRegistry(marshallingContext, beanFilter, classInfo, config);
1607   }
1608
1609   /**
1610    * Builds a list of all classes in the class hierarchy for this bean.
1611    *
1612    * <p>
1613    * Traverses the complete inheritance hierarchy (classes and interfaces) starting from the bean class (or the
1614    * interface class specified in the bean filter) and collects all classes up to (but not including) the stop class.
1615    *
1616    * <p>
1617    * The traversal order follows a depth-first approach:
1618    * <ol>
1619    *    <li>First, recursively traverses the superclass hierarchy
1620    *    <li>Then, recursively traverses all implemented interfaces
1621    *    <li>Finally, adds the current class itself
1622    * </ol>
1623    *
1624    * <p>
1625    * If a {@link BeanFilter#getInterfaceClass() bean filter interface class} is specified, the traversal starts
1626    * from that interface class instead of the bean class itself. This allows beans to use properties defined on
1627    * a parent interface rather than the concrete implementation class.
1628    *
1629    * <p>
1630    * The resulting list is used to find bean properties, methods, and fields across the entire class hierarchy.
1631    *
1632    * @return An unmodifiable list of {@link ClassInfo} objects representing all classes in the hierarchy, in
1633    *    traversal order (superclasses first, then interfaces, then the class itself).
1634    */
1635   private List<ClassInfo> findClassHierarchy() {
1636      var result = new LinkedList<ClassInfo>();
1637      // If @BeanType(interfaceClass) is specified on the parent class, then we want
1638      // to use the properties defined on that class, not the subclass.
1639      var c2 = (nn(beanFilter) && nn(beanFilter.getInterfaceClass()) ? beanFilter.getInterfaceClass() : classInfo);
1640      findClassHierarchy(c2, stopClass, result::add);
1641      return u(result);
1642   }
1643
1644   /**
1645    * Recursively traverses the class hierarchy and invokes the consumer for each class found.
1646    *
1647    * <p>
1648    * This is a helper method that performs a depth-first traversal of the class hierarchy:
1649    * <ol>
1650    *    <li>Recursively processes the superclass (if present and not the stop class)
1651    *    <li>Recursively processes all implemented interfaces
1652    *    <li>Invokes the consumer with the current class
1653    * </ol>
1654    *
1655    * <p>
1656    * The traversal stops when it reaches the stop class (which is not included in the traversal).
1657    *
1658    * @param c The class to start traversal from.
1659    * @param stopClass The class to stop traversal at (exclusive). Traversal will not proceed beyond this class.
1660    * @param consumer The consumer to invoke for each class in the hierarchy.
1661    */
1662   private void findClassHierarchy(ClassInfo c, ClassInfo stopClass, Consumer<ClassInfo> consumer) {
1663      var sc = c.getSuperclass();
1664      if (nn(sc) && ! sc.is(stopClass.inner()))
1665         findClassHierarchy(sc, stopClass, consumer);
1666      c.getInterfaces().forEach(x -> findClassHierarchy(x, stopClass, consumer));
1667      consumer.accept(c);
1668   }
1669
1670   /**
1671    * Finds the dictionary name (type name) for this bean class.
1672    *
1673    * <p>
1674    * The dictionary name is used in serialized forms to identify the specific type of a bean instance, enabling
1675    * polymorphic serialization and deserialization. This is especially important when serializing/parsing beans
1676    * that are part of an inheritance hierarchy.
1677    *
1678    * <p>
1679    * The dictionary name is determined by searching in the following order of precedence:
1680    * <ol>
1681    *    <li><b>Bean filter type name:</b> If a bean filter is present and has a type name specified via
1682    *       {@link BeanFilter#getTypeName()}, that value is used.
1683    *    <li><b>Bean registry lookup:</b> If a bean registry exists for this bean, it is queried for the type name
1684    *       of this class.
1685    *    <li><b>Parent class registry lookup:</b> Searches through parent classes and interfaces (starting from the
1686    *       second one, skipping the class itself) and checks if any of their bean registries contain a type name
1687    *       for this class.
1688    *    <li><b>{@code @Marshalled} annotation:</b> If the class has a {@code @Marshalled} annotation with a non-empty
1689    *       {@code typeName()}, that value is used.
1690    *    <li><b>No dictionary name:</b> Returns <jk>null</jk> if no dictionary name is found.
1691    * </ol>
1692    *
1693    * <p>
1694    * If a dictionary name is found, it will be used in serialized output (typically as a special property like
1695    * <js>"_type"</js>) so that parsers can determine the correct class to instantiate when deserializing.
1696    *
1697    * @return The dictionary name for this bean, or <jk>null</jk> if no dictionary name is defined.
1698    */
1699   private String findDictionaryName() {
1700      if (nn(beanFilter) && nn(beanFilter.getTypeName()))
1701         return beanFilter.getTypeName();
1702
1703      // Pure-reflection class identity for BeanRegistry.getTypeName(...) — this method now uses
1704      // surviving `this.classMeta` references inside this method to `classInfo.inner()` so the dictionary
1705      // lookup does not require a ClassMeta for the bean's own class.  BeanRegistry still lives in
1706      // juneau-marshall; it just exposes a raw-Class overload for callers that have a ClassInfo.
1707      var rawClass = classInfo.inner();
1708
1709      var br = getBeanRegistry();
1710      if (nn(br)) {
1711         String s = br.getTypeName(rawClass);
1712         if (nn(s))
1713            return s;
1714      }
1715
1716      // Parent-class BeanRegistry lookup is a marshalling-side concern: it walks parents and interfaces to
1717      // see if any of THEIR ClassMeta-backed BeanRegistries declares a typeName for our raw class.  Lifted
1718      // out to the BeanMetaInitializer SPI carried on BeanConfigContext.  Returns null on the commons-side path.
1719      var bmi = config.getBeanMetaInitializer();
1720      var n = bmi.findTypeNameInParents(marshallingContext, classInfo, rawClass);
1721      if (n != null)
1722         return n;
1723
1724      return bmi.findMarshalledTypeName(config, classInfo);
1725   }
1726
1727   /**
1728    * Merges standard JavaBeans {@link BeanInfo} property descriptors into {@code normalProps}, skipping the class
1729    * pseudo-property and logical names suppressed when {@link BeanIgnore#ignoreAccessors()} is <jk>true</jk> on a field.
1730    */
1731   private void mergeJavaBeanPropertyDescriptorsIntoNormalProps(java.beans.BeanInfo bi, Map<String,BeanPropertyMeta.Builder> normalProps,
1732         PropertyNamer propertyNamer) {
1733      var suppressedFromBeanIgnoredFields = findSuppressedPropertyNamesFromIgnoredFields(propertyNamer);
1734      for (var pd : bi.getPropertyDescriptors()) {
1735         if (eq(pd.getName(), "class"))
1736            continue;
1737         if (suppressedFromBeanIgnoredFields.contains(pd.getName()))
1738            continue;
1739         var builder = normalProps.computeIfAbsent(pd.getName(), n -> BeanPropertyMeta.builder(this, n));
1740         if (pd.getReadMethod() != null)
1741            builder.setGetter(info(pd.getReadMethod()));
1742         if (pd.getWriteMethod() != null)
1743            builder.setSetter(info(pd.getWriteMethod()));
1744      }
1745   }
1746
1747   /**
1748    * Property names suppressed from getter/setter discovery because a non-static field with that logical name is
1749    * annotated with {@link BeanIgnore @BeanIgnore} and {@link BeanIgnore#ignoreAccessors()} is <jk>true</jk>.
1750    *
1751    * <p>
1752    * When {@link BeanIgnore#ignoreAccessors()} is <jk>false</jk> (the default), ignored fields do not suppress
1753    * JavaBean accessors so patterns such as {@code @BeanIgnore} on a private field with a public {@code getX()} still
1754    * expose {@code x} when field visibility excludes the field.
1755    */
1756   private Set<String> findSuppressedPropertyNamesFromIgnoredFields(PropertyNamer propertyNamer) {
1757      var s = new HashSet<String>();
1758      var ap = config.getAnnotationProvider();
1759      for (var c2 : classHierarchy.get()) {
1760         for (var x : c2.getDeclaredFields()) {
1761            if (! x.isNotStatic() || ! ap.has(BeanIgnore.class, x))
1762               continue;
1763            if (! fieldBeanIgnoreIgnoresAccessors(x))
1764               continue;
1765            var name = ap.find(x).stream()
1766               .filter(x2 -> x2.isType(BeanProp.class) || x2.isType(Name.class))
1767               .map(BeanMeta::name)
1768               .filter(Objects::nonNull)
1769               .findFirst()
1770               .orElse(propertyNamer.getPropertyName(x.getName()));
1771            name = resolveBeanFieldPropertyName(x, name, propertyNamer);
1772            if (nn(name))
1773               s.add(name);
1774         }
1775      }
1776      return s;
1777   }
1778
1779   private static boolean fieldBeanIgnoreIgnoresAccessors(FieldInfo x) {
1780      for (var bi : x.inner().getAnnotationsByType(BeanIgnore.class))
1781         if (bi.ignoreAccessors())
1782            return true;
1783      return false;
1784   }
1785
1786   /**
1787    * Finds a bean field by name in the class hierarchy.
1788    *
1789    * <p>
1790    * Searches through the complete class hierarchy (as defined by {@link #classHierarchy}) to find a field
1791    * with the specified name. The search is performed in the order classes appear in the hierarchy, and the
1792    * first matching field is returned.
1793    *
1794    * <p>
1795    * A field is considered a match if it:
1796    * <ul>
1797    *    <li>Has the exact name specified
1798    *    <li>Is not static
1799    *    <li>Is not transient (unless transient fields are not ignored)
1800    *    <li>Is not annotated with {@link Transient @Transient} (unless transient fields are not ignored)
1801    *    <li>Is not annotated with {@link BeanIgnore @BeanIgnore}
1802    * </ul>
1803    *
1804    * <p>
1805    * This method is used to find fields for bean properties when a field reference is needed but wasn't
1806    * discovered during the initial property discovery phase (e.g., for properties defined only through
1807    * getters/setters).
1808    *
1809    * @param name The name of the field to find.
1810    * @return The {@link FieldInfo} for the field if found, or <jk>null</jk> if no matching field exists
1811    *    in the class hierarchy.
1812    */
1813   private Optional<FieldInfo> findInnerBeanField(String name) {
1814      var noIgnoreTransients = ! config.isIgnoreTransientFields();
1815      var ap = config.getAnnotationProvider();
1816
1817      // @formatter:off
1818      return classHierarchy.get().stream()
1819         .flatMap(c2 -> c2.getDeclaredField(
1820            x -> x.isNotStatic()
1821               && (x.isNotTransient() || noIgnoreTransients)
1822               && (! x.hasAnnotation(Transient.class) || noIgnoreTransients)
1823               && ! ap.has(BeanIgnore.class, x)
1824               && x.hasName(name)
1825         ).stream())
1826         .findFirst();
1827      // @formatter:on
1828   }
1829}