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;
018
019import static org.apache.juneau.BeanMeta.MethodType.*;
020import static org.apache.juneau.commons.reflect.AnnotationTraversal.*;
021import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
022import static org.apache.juneau.commons.utils.CollectionUtils.*;
023import static org.apache.juneau.commons.utils.StringUtils.*;
024import static org.apache.juneau.commons.utils.ThrowableUtils.*;
025import static org.apache.juneau.commons.utils.Utils.*;
026
027import java.beans.*;
028import java.io.*;
029import java.lang.reflect.*;
030import java.util.*;
031import java.util.function.*;
032
033import org.apache.juneau.annotation.*;
034import org.apache.juneau.commons.collections.*;
035import org.apache.juneau.commons.function.OptionalSupplier;
036import org.apache.juneau.commons.lang.*;
037import org.apache.juneau.commons.reflect.*;
038import org.apache.juneau.commons.reflect.Visibility;
039import org.apache.juneau.commons.utils.*;
040
041/**
042 * Encapsulates all access to the properties of a bean class (like a souped-up {@link java.beans.BeanInfo}).
043 *
044 * <h5 class='topic'>Description</h5>
045 *
046 * Uses introspection to find all the properties associated with this class.  If the {@link Bean @Bean} annotation
047 *    is present on the class, then that information is used to determine the properties on the class.
048 * Otherwise, the {@code BeanInfo} functionality in Java is used to determine the properties on the class.
049 *
050 * <h5 class='topic'>Bean property ordering</h5>
051 *
052 * The order of the properties are as follows:
053 * <ul class='spaced-list'>
054 *    <li>
055 *       If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties
056 *       in the annotation.
057 *    <li>
058 *       If {@link Bean @Bean} annotation is not specified on the class, then the order is based on the following.
059 *       <ul>
060 *          <li>Public fields (same order as {@code Class.getFields()}).
061 *          <li>Properties returned by {@code BeanInfo.getPropertyDescriptors()}.
062 *          <li>Non-standard getters/setters with {@link Beanp @Beanp} annotation defined on them.
063 *       </ul>
064 * </ul>
065 *
066 *
067 * @param <T> The class type that this metadata applies to.
068 */
069public class BeanMeta<T> {
070
071   /**
072    * Represents the result of creating a BeanMeta, including the bean metadata and any reason why it's not a bean.
073    *
074    * @param <T> The bean type.
075    * @param beanMeta The bean metadata, or <jk>null</jk> if the class is not a bean.
076    * @param notABeanReason The reason why the class is not a bean, or <jk>null</jk> if it is a bean.
077    */
078   record BeanMetaValue<T>(BeanMeta<T> beanMeta, String notABeanReason) {
079      Optional<BeanMeta<T>> optBeanMeta() { return opt(beanMeta()); }
080      Optional<String> optNotABeanReason() { return opt(notABeanReason()); }
081   }
082
083   /**
084    * Possible property method types.
085    */
086   enum MethodType {
087      UNKNOWN, GETTER, SETTER, EXTRAKEYS;
088   }
089
090   /*
091    * Temporary getter/setter method struct used for calculating bean methods.
092    */
093   private static class BeanMethod {
094      private String propertyName;
095      private MethodType methodType;
096      private Method method;
097      private ClassInfo type;
098
099      private BeanMethod(String propertyName, MethodType type, Method method) {
100         this.propertyName = propertyName;
101         this.methodType = type;
102         this.method = method;
103         this.type = info(type == SETTER ? method.getParameterTypes()[0] : method.getReturnType());
104      }
105
106      @Override /* Overridden from Object */
107      public String toString() {
108         return method.toString();
109      }
110
111      /*
112       * Returns true if this method matches the class type of the specified property.
113       * Only meant to be used for setters.
114       */
115      private boolean matchesPropertyType(BeanPropertyMeta.Builder b) {
116         if (b == null)
117            return false;
118
119         // Don't do further validation if this is the "*" bean property.
120         if ("*".equals(b.name))
121            return true;
122
123         // Get the bean property type from the getter/field.
124         var pt = (Class<?>)null;  // TODO - Convert to ClassInfo
125         if (nn(b.getter))
126            pt = b.getter.getReturnType().inner();
127         else if (nn(b.field))
128            pt = b.field.inner().getType();
129
130         // Matches if only a setter is defined.
131         if (pt == null)
132            return true;
133
134         // Doesn't match if not same type or super type as getter/field.
135         if (! type.isParentOf(pt))
136            return false;
137
138         // If a setter was previously set, only use this setter if it's a closer
139         // match (e.g. prev type is a superclass of this type).
140         if (b.setter == null)
141            return true;
142
143         return type.isStrictChildOf(b.setter.getParameterTypes().get(0).inner());
144      }
145   }
146
147   /*
148    * Represents a bean constructor with its associated property names.
149    *
150    * @param constructor The constructor information.
151    * @param propertyNames The list of property names that correspond to the constructor parameters.
152    */
153   private record BeanConstructor(Optional<ConstructorInfo> constructor, List<String> args) {}
154
155   /**
156    * Creates a {@link BeanMeta} instance for the specified class metadata.
157    *
158    * <p>
159    * This is a factory method that attempts to create bean metadata for a class. If the class is determined to be a bean,
160    * the returned {@link BeanMetaValue} will contain the {@link BeanMeta} instance and a <jk>null</jk> reason.
161    * If the class is not a bean, the returned value will contain <jk>null</jk> for the bean metadata and a non-null
162    * string explaining why it's not a bean.
163    *
164    * <h5 class='section'>Parameters:</h5>
165    * <ul class='spaced-list'>
166    *    <li><b>cm</b> - The class metadata for the class to create bean metadata for.
167    *    <li><b>bf</b> - Optional bean filter to apply. Can be <jk>null</jk>.
168    *    <li><b>pNames</b> - Explicit list of property names and order. If <jk>null</jk>, properties are determined automatically.
169    *    <li><b>implClassConstructor</b> - Optional constructor to use if one cannot be found. Can be <jk>null</jk>.
170    * </ul>
171    *
172    * <h5 class='section'>Return Value:</h5>
173    * <p>
174    * Returns a {@link BeanMetaValue} containing:
175    * <ul>
176    *    <li><b>beanMeta</b> - The bean metadata if the class is a bean, or <jk>null</jk> if it's not.
177    *    <li><b>notABeanReason</b> - A string explaining why the class is not a bean, or <jk>null</jk> if it is a bean.
178    * </ul>
179    *
180    * <h5 class='section'>Exception Handling:</h5>
181    * <p>
182    * If a {@link RuntimeException} is thrown during bean metadata creation, it is caught and the exception message
183    * is returned as the <c>notABeanReason</c> with <jk>null</jk> for the bean metadata.
184    *
185    * <h5 class='section'>Example:</h5>
186    * <p class='bjava'>
187    *    <jc>// Create bean metadata for a class</jc>
188    *    ClassMeta&lt;Person&gt; <jv>cm</jv> = <jv>beanContext</jv>.getClassMeta(Person.<jk>class</jk>);
189    *    BeanMetaValue&lt;Person&gt; <jv>result</jv> = BeanMeta.<jsm>create</jsm>(<jv>cm</jv>, <jk>null</jk>, <jk>null</jk>, <jk>null</jk>);
190    *
191    *    <jc>// Check if it's a bean</jc>
192    *    <jk>if</jk> (<jv>result</jv>.beanMeta() != <jk>null</jk>) {
193    *       BeanMeta&lt;Person&gt; <jv>bm</jv> = <jv>result</jv>.beanMeta();
194    *       <jc>// Use the bean metadata...</jc>
195    *    } <jk>else</jk> {
196    *       String <jv>reason</jv> = <jv>result</jv>.notABeanReason();
197    *       <jc>// Handle the case where it's not a bean...</jc>
198    *    }
199    * </p>
200    *
201 * @param <T> The class type.
202 * @param cm The class metadata for the class to create bean metadata for.
203 * @param implClass The implementation class info.
204 * @return A {@link BeanMetaValue} containing the bean metadata (if successful) or a reason why it's not a bean.
205    */
206   public static <T> BeanMetaValue<T> create(ClassMeta<T> cm, ClassInfo implClass) {
207      try {
208         var bc = cm.getBeanContext();
209         var ap = bc.getAnnotationProvider();
210
211         // Sanity checks first.
212         if (bc.isNotABean(cm))
213            return notABean("Class matches exclude-class list");
214
215         if (bc.isBeansRequireSerializable() && ! cm.isChildOf(Serializable.class) && ! ap.has(Bean.class, cm))
216            return notABean("Class is not serializable");
217
218         if (ap.has(BeanIgnore.class, cm))
219            return notABean("Class is annotated with @BeanIgnore");
220
221         if ((! bc.getBeanClassVisibility().isVisible(cm.getModifiers()) || cm.isAnonymousClass()) && ! ap.has(Bean.class, cm))
222            return notABean("Class is not public");
223
224         var bm = new BeanMeta<>(cm, findBeanFilter(cm), null, implClass);
225
226         if (nn(bm.notABeanReason))
227            return notABean(bm.notABeanReason);
228
229         return new BeanMetaValue<>(bm, null);
230      } catch (RuntimeException e) {
231         return new BeanMetaValue<>(null, e.getMessage());
232      }
233   }
234
235   /*
236    * Extracts the property name from {@link Beanp @Beanp} or {@link Name @Name} annotations.
237    *
238    * <p>
239    * If {@link Name @Name} annotations are present, returns the value from the last one.
240    * Otherwise, searches through {@link Beanp @Beanp} annotations and returns the first non-empty
241    * {@link Beanp#value() value()} or {@link Beanp#name() name()} found.
242    *
243    * @param p List of {@link Beanp @Beanp} annotations.
244    * @param n List of {@link Name @Name} annotations.
245    * @return The property name, or <jk>null</jk> if no name is found.
246    */
247   private static String bpName(List<Beanp> p, List<Name> n) {
248      if (p.isEmpty() && n.isEmpty())
249         return null;
250      if (! n.isEmpty())
251         return last(n).value();
252
253      var name = Value.of(p.isEmpty() ? null : "");
254      p.forEach(x -> {
255         if (! x.value().isEmpty())
256            name.set(x.value());
257         if (! x.name().isEmpty())
258            name.set(x.name());
259      });
260
261      return name.orElse(null);
262   }
263
264   /*
265    * Finds and creates the bean filter for the specified class metadata.
266    *
267    * <p>
268    * Searches for {@link Bean @Bean} annotations on the class and its parent classes/interfaces. If found, creates a
269    * {@link BeanFilter} that applies the configuration from those annotations.
270    *
271    * <p>
272    * When multiple {@link Bean @Bean} annotations are found (e.g., on a parent class and a child class), they are
273    * applied in reverse order (parent classes first, then child classes). This ensures that child class annotations
274    * override parent class annotations, allowing child classes to customize or extend the bean configuration.
275    *
276    * <p>
277    * The bean filter controls various aspects of bean serialization and parsing, such as:
278    * <ul>
279    *    <li>Property inclusion/exclusion lists
280    *    <li>Property ordering and sorting
281    *    <li>Type name mapping for dictionary lookups
282    *    <li>Fluent setter detection
283    *    <li>Read-only and write-only property definitions
284    * </ul>
285    *
286    * @param <T> The class type.
287    * @param cm The class metadata to find the filter for.
288    * @return The bean filter, or <jk>null</jk> if no {@link Bean @Bean} annotations are found on the class or its hierarchy.
289    * @see Bean
290    * @see BeanFilter
291    */
292   private static <T> BeanFilter findBeanFilter(ClassMeta<T> cm) {
293      var ap = cm.getBeanContext().getAnnotationProvider();
294      var l = ap.find(Bean.class, cm);
295      if (l.isEmpty())
296         return null;
297      return BeanFilter.create(cm).applyAnnotations(reverse(l.stream().map(AnnotationInfo::inner).toList())).build();
298   }
299
300   /*
301    * Extracts the property name from a single {@link Beanp @Beanp} or {@link Name @Name} annotation.
302    *
303    * <p>
304    * For {@link Beanp @Beanp} annotations, returns the first non-empty value found in the following order:
305    * <ol>
306    *    <li>{@link Beanp#name() name()}
307    *    <li>{@link Beanp#value() value()}
308    * </ol>
309    *
310    * <p>
311    * For {@link Name @Name} annotations, returns the {@link Name#value() value()}.
312    *
313    * <p>
314    * This method is used to extract property names from individual annotations, typically when processing
315    * annotation lists in stream operations.
316    *
317    * @param ai The annotation info containing either a {@link Beanp @Beanp} or {@link Name @Name} annotation.
318    * @return The property name extracted from the annotation, or <jk>null</jk> if no name is found.
319    * @see #bpName(List, List)
320    */
321   private static String name(AnnotationInfo<?> ai) {
322      if (ai.isType(Beanp.class)) {
323         var p = ai.cast(Beanp.class).inner();
324         if (ne(p.name()))
325            return p.name();
326         if (ne(p.value()))
327            return p.value();
328      } else {
329         var n = ai.cast(Name.class).inner();
330         if (ne(n.value()))
331            return n.value();
332      }
333      return null;
334   }
335
336   /*
337    * Shortcut for creating a BeanMetaValue with a not-a-bean reason.
338    */
339   private static <T> BeanMetaValue<T> notABean(String reason) {
340      return new BeanMetaValue<>(null, reason);
341   }
342
343   private final BeanConstructor beanConstructor;                             // The constructor for this bean.
344   private final BeanContext beanContext;                                     // The bean context that created this metadata object.
345   private final BeanFilter beanFilter;                                       // Optional bean filter associated with the target class.
346   private final OptionalSupplier<InvocationHandler> beanProxyInvocationHandler;  // The invocation handler for this bean (if it's an interface).
347   private final Supplier<BeanRegistry> beanRegistry;                         // The bean registry for this bean.
348   private final Supplier<List<ClassInfo>> classHierarchy;                    // List of all classes traversed in the class hierarchy.
349   private final ClassMeta<T> classMeta;                                      // The target class type that this meta object describes.
350   private final Supplier<String> dictionaryName;                             // The @Bean(typeName) annotation defined on this bean class.
351   private final BeanPropertyMeta dynaProperty;                               // "extras" property.
352   private final boolean fluentSetters;                                       // Whether fluent setters are enabled.
353   private final Map<Method,String> getterProps;                              // The getter properties on the target class.
354   private final Map<String,BeanPropertyMeta> hiddenProperties;               // The hidden properties on the target class.
355   private final ConstructorInfo implClassConstructor;                        // Optional constructor to use if one cannot be found.
356   private final String notABeanReason;                                       // Readable string explaining why this class wasn't a bean.
357   private final Map<String,BeanPropertyMeta> properties;                     // The properties on the target class.
358   private final Map<Method,String> setterProps;                              // The setter properties on the target class.
359   private final boolean sortProperties;                                      // Whether properties should be sorted.
360   private final ClassInfo stopClass;                                          // The stop class for hierarchy traversal.
361   private final BeanPropertyMeta typeProperty;                               // "_type" mock bean property.
362   private final String typePropertyName;                                     // "_type" property actual name.
363
364   /**
365    * Constructor.
366    *
367    * <p>
368    * Creates a new {@link BeanMeta} instance for the specified class metadata. This constructor performs
369    * introspection to discover all bean properties, methods, and fields in the class hierarchy.
370    *
371    * <p>
372    * The bean metadata is built by:
373    * <ul>
374    *    <li>Finding all bean fields in the class hierarchy
375    *    <li>Finding all bean methods (getters, setters, extraKeys) in the class hierarchy
376    *    <li>Determining the appropriate constructor for bean instantiation
377    *    <li>Building the class hierarchy for property discovery
378    *    <li>Creating the bean registry for dictionary name resolution
379    *    <li>Validating and filtering properties based on bean filter settings
380    * </ul>
381    *
382    * @param cm The class metadata for the bean class.
383    * @param bf Optional bean filter to apply. Can be <jk>null</jk>.
384    * @param pNames Explicit list of property names and order. If <jk>null</jk>, properties are determined automatically.
385    * @param implClass Optional implementation class constructor to use if one cannot be found. Can be <jk>null</jk>.
386    */
387   protected BeanMeta(ClassMeta<T> cm, BeanFilter bf, String[] pNames, ClassInfo implClass) {
388      classMeta = cm;
389      beanContext = cm.getBeanContext();
390      beanFilter = bf;
391      implClassConstructor = opt(implClass).map(x -> x.getPublicConstructor(x2 -> x2.hasNumParameters(0)).orElse(null)).orElse(null);
392      fluentSetters = beanContext.isFindFluentSetters() || (nn(bf) && bf.isFluentSetters());
393      stopClass = opt(bf).map(x -> x.getStopClass()).orElse(info(Object.class));
394      beanRegistry = mem(()->findBeanRegistry());
395      classHierarchy = mem(()->findClassHierarchy());
396      beanConstructor = findBeanConstructor();
397
398      // Local variables for initialization
399      var ap = beanContext.getAnnotationProvider();
400      var c = cm.inner();
401      var ci = cm;
402      var _notABeanReason = (String)null;
403      var _properties = Value.<Map<String,BeanPropertyMeta>>empty();
404      var _hiddenProperties = CollectionUtils.<String,BeanPropertyMeta>map();
405      var _getterProps = CollectionUtils.<Method,String>map();  // Convert to MethodInfo keys
406      var _setterProps = CollectionUtils.<Method,String>map();
407      var _dynaProperty = Value.<BeanPropertyMeta>empty();
408      var _sortProperties = false;
409      var ba = ap.find(Bean.class, cm);
410      var propertyNamer = opt(bf).map(x -> x.getPropertyNamer()).orElse(beanContext.getPropertyNamer());
411
412      this.typePropertyName = ba.stream().map(x -> x.inner().typePropertyName()).filter(Utils::ne).findFirst().orElseGet(() -> beanContext.getBeanTypePropertyName());
413
414      // Check if constructor is required but not found
415      if (! beanConstructor.constructor().isPresent() && bf == null && beanContext.isBeansRequireDefaultConstructor())
416         _notABeanReason = "Class does not have the required no-arg constructor";
417
418      var bfo = opt(bf);
419      var fixedBeanProps = bfo.map(x -> x.getProperties()).orElse(sete());
420
421      try {
422         Map<String,BeanPropertyMeta.Builder> normalProps = map();  // NOAI
423
424         // First populate the properties with those specified in the bean annotation to
425         // ensure that ordering first.
426         fixedBeanProps.forEach(x -> normalProps.put(x, BeanPropertyMeta.builder(this, x)));
427
428         if (beanContext.isUseJavaBeanIntrospector()) {
429            var c2 = bfo.map(x -> x.getInterfaceClass()).filter(Objects::nonNull).orElse(classMeta);
430            var bi = (BeanInfo)null;
431            if (! c2.isInterface())
432               bi = Introspector.getBeanInfo(c2.inner(), stopClass.inner());
433            else
434               bi = Introspector.getBeanInfo(c2.inner(), null);
435            if (nn(bi)) {
436               for (var pd : bi.getPropertyDescriptors()) {
437                  var builder = normalProps.computeIfAbsent(pd.getName(), n -> BeanPropertyMeta.builder(this, n));
438                  if (pd.getReadMethod() != null)
439                     builder.setGetter(info(pd.getReadMethod()));
440                  if (pd.getWriteMethod() != null)
441                     builder.setSetter(info(pd.getWriteMethod()));
442               }
443            }
444
445         } else /* Use 'better' introspection */ {
446
447            findBeanFields().forEach(x -> {
448               var name = ap.find(x).stream()
449                  .filter(x2 -> x2.isType(Beanp.class) || x2.isType(Name.class))
450                  .map(x2 -> name(x2))
451                  .filter(Objects::nonNull)
452                  .findFirst()
453                  .orElse(propertyNamer.getPropertyName(x.getName()));
454               if (nn(name)) {
455                  normalProps.computeIfAbsent(name, n->BeanPropertyMeta.builder(this, n)).setField(x);
456               }
457            });
458
459            var bms = findBeanMethods();
460
461            // Iterate through all the getters.
462            bms.forEach(x -> {
463               var pn = x.propertyName;
464               var m = x.method;  // TODO - Convert to MethodInfo
465               var mi = info(m);
466               var bpm = normalProps.computeIfAbsent(pn, k -> new BeanPropertyMeta.Builder(this, k));
467
468               if (x.methodType == GETTER) {
469                  // Two getters.  Pick the best.
470                  if (nn(bpm.getter)) {
471                     if (! ap.has(Beanp.class, mi) && ap.has(Beanp.class, bpm.getter)) {
472                        m = bpm.getter.inner();  // @Beanp annotated method takes precedence.
473                     } else if (m.getName().startsWith("is") && bpm.getter.getSimpleName().startsWith("get")) {
474                        m = bpm.getter.inner();  // getX() overrides isX().
475                     }
476                  }
477                  bpm.setGetter(info(m));
478               }
479            });
480
481            // Now iterate through all the setters.
482            bms.stream().filter(x -> eq(x.methodType, SETTER)).forEach(x -> {
483               var bpm = normalProps.get(x.propertyName);
484               if (x.matchesPropertyType(bpm))
485                  bpm.setSetter(info(x.method));
486            });
487
488            // Now iterate through all the extraKeys.
489            bms.stream().filter(x -> eq(x.methodType, EXTRAKEYS)).forEach(x -> normalProps.get(x.propertyName).setExtraKeys(info(x.method)));
490         }
491
492         var typeVarImpls = TypeVariables.of(c);
493
494         // Eliminate invalid properties, and set the contents of getterProps and setterProps.
495         var readOnlyProps = bfo.map(x -> x.getReadOnlyProperties()).orElse(sete());
496         var writeOnlyProps = bfo.map(x -> x.getWriteOnlyProperties()).orElse(sete());
497         for (var i = normalProps.values().iterator(); i.hasNext();) {
498            var p = i.next();
499            try {
500               if (p.field == null)
501                  findInnerBeanField(p.name).ifPresent(x -> p.setInnerField(x));
502
503               if (p.validate(beanContext, beanRegistry.get(), typeVarImpls, readOnlyProps, writeOnlyProps)) {
504
505                  if (nn(p.getter))
506                     _getterProps.put(p.getter.inner(), p.name);
507
508                  if (nn(p.setter))
509                     _setterProps.put(p.setter.inner(), p.name);
510
511               } else {
512                  i.remove();
513               }
514            } catch (ClassNotFoundException e) {
515               throw bex(c, lm(e));
516            }
517         }
518
519         // Check for missing properties.
520         fixedBeanProps.stream().filter(x -> ! normalProps.containsKey(x)).findFirst().ifPresent(x -> { throw bex(c, "The property ''{0}'' was defined on the @Bean(properties=X) annotation of class ''{1}'' but was not found on the class definition.", x, ci.getNameSimple()); });
521
522         // Mark constructor arg properties.
523         for (var fp : beanConstructor.args()) {
524            var m = normalProps.get(fp);
525            if (m == null)
526               throw bex(c, "The property ''{0}'' was defined on the @Beanc(properties=X) annotation but was not found on the class definition.", fp);
527            m.setAsConstructorArg();
528         }
529
530         // Make sure at least one property was found.
531         if (bf == null && beanContext.isBeansRequireSomeProperties() && normalProps.isEmpty())
532            _notABeanReason = "No properties detected on bean class";
533
534         _sortProperties = beanContext.isSortProperties() || bfo.map(x -> x.isSortProperties()).orElse(false) && fixedBeanProps.isEmpty();
535
536         _properties.set(_sortProperties ? sortedMap() : map());
537
538         normalProps.forEach((k, v) -> {
539            var pMeta = v.build();
540            if (pMeta.isDyna())
541               _dynaProperty.set(pMeta);
542            _properties.get().put(k, pMeta);
543         });
544
545         // If a beanFilter is defined, look for inclusion and exclusion lists.
546         if (bf != null) {
547
548            // Eliminated excluded properties if BeanFilter.excludeKeys is specified.
549            var bfbpi = bf.getProperties();
550            var bfbpx = bf.getExcludeProperties();
551            var p = _properties.get();
552
553            if (! bfbpi.isEmpty()) {
554               // Only include specified properties if BeanFilter.includeKeys is specified.
555               // Note that the order must match includeKeys.
556               Map<String,BeanPropertyMeta> p2 = map();  // NOAI
557               bfbpi.stream().filter(x -> p.containsKey(x)).forEach(x -> p2.put(x, p.remove(x)));
558               _hiddenProperties.putAll(p);
559               _properties.set(p2);
560            }
561
562            bfbpx.forEach(x -> _hiddenProperties.put(x, _properties.get().remove(x)));
563         }
564
565         if (nn(pNames)) {
566            var p = _properties.get();
567            Map<String,BeanPropertyMeta> p2 = map();
568            for (var k : pNames) {
569               if (p.containsKey(k))
570                  p2.put(k, p.get(k));
571               else
572                  _hiddenProperties.put(k, p.get(k));
573            }
574            _properties.set(p2);
575         }
576
577      } catch (BeanRuntimeException e) {
578         throw e;
579      } catch (Exception e) {
580         _notABeanReason = "Exception:  " + getStackTrace(e);
581      }
582
583      notABeanReason = _notABeanReason;
584      properties = u(_properties.get());
585      hiddenProperties = u(_hiddenProperties);
586      getterProps = u(_getterProps);
587      setterProps = u(_setterProps);
588      dynaProperty = _dynaProperty.get();
589      sortProperties = _sortProperties;
590      typeProperty = BeanPropertyMeta.builder(this, typePropertyName).canRead().canWrite().rawMetaType(beanContext.string()).beanRegistry(beanRegistry.get()).build();
591      dictionaryName = mem(()->findDictionaryName());
592      beanProxyInvocationHandler = mem(()->beanContext.isUseInterfaceProxies() && c.isInterface() ? new BeanProxyInvocationHandler<>(this) : null);
593   }
594
595   @Override /* Overridden from Object */
596   public boolean equals(Object o) {
597      return (o instanceof BeanMeta<?> o2) && eq(this, o2, (x, y) -> eq(x.classMeta, y.classMeta));
598   }
599
600   /**
601    * Returns the bean filter associated with this bean.
602    *
603    * <p>
604    * Bean filters are used to control aspects of how beans are handled during serialization and parsing, such as
605    * property inclusion/exclusion, property ordering, and type name mapping.
606    *
607    * <p>
608    * The bean filter is typically created from the {@link Bean @Bean} annotation on the class. If no {@link Bean @Bean}
609    * annotation is present, this method returns <jk>null</jk>.
610    *
611    * @return The bean filter for this bean, or <jk>null</jk> if no bean filter is associated with this bean.
612    * @see Bean
613    */
614   public BeanFilter getBeanFilter() {
615      return beanFilter;
616   }
617
618   /**
619    * Returns the proxy invocation handler for this bean if it's an interface.
620    *
621    * @return The invocation handler, or <jk>null</jk> if this is not an interface or interface proxies are disabled.
622    */
623   public InvocationHandler getBeanProxyInvocationHandler() {
624      return beanProxyInvocationHandler.get();
625   }
626
627   /**
628    * Returns the bean registry for this bean.
629    *
630    * <p>
631    * The bean registry is used to resolve dictionary names to class types. It's created when a bean class has a
632    * {@link Bean#dictionary() @Bean(dictionary)} annotation that specifies a list of possible subclasses.
633    *
634    * @return The bean registry for this bean, or <jk>null</jk> if no bean registry is associated with it.
635    */
636   public BeanRegistry getBeanRegistry() { return beanRegistry.get(); }
637
638   /**
639    * Returns the {@link ClassMeta} of this bean.
640    *
641    * @return The {@link ClassMeta} of this bean.
642    */
643   public ClassMeta<T> getClassMeta() { return classMeta; }
644
645   /**
646    * Returns the dictionary name for this bean as defined through the {@link Bean#typeName() @Bean(typeName)} annotation.
647    *
648    * @return The dictionary name for this bean, or <jk>null</jk> if it has no dictionary name defined.
649    */
650   public String getDictionaryName() { return dictionaryName.get(); }
651
652   /**
653    * Returns a map of all properties on this bean.
654    *
655    * <p>
656    * The map is keyed by property name and contains {@link BeanPropertyMeta} objects that provide metadata about each
657    * property, including its type, getter/setter methods, field information, and serialization/parsing behavior.
658    *
659    * <p>
660    * This map contains only the normal (non-hidden) properties of the bean. Hidden properties can be accessed via
661    * {@link #getHiddenProperties()}.
662    *
663    * @return A map of property names to their metadata. The map is unmodifiable.
664    * @see #getPropertyMeta(String)
665    * @see #getHiddenProperties()
666    */
667   public Map<String,BeanPropertyMeta> getProperties() { return properties; }
668
669   /**
670    * Returns metadata about the specified property.
671    *
672    * @param name The name of the property on this bean.
673    * @return The metadata about the property, or <jk>null</jk> if no such property exists on this bean.
674    */
675   public BeanPropertyMeta getPropertyMeta(String name) {
676      var bpm = properties.get(name);
677      if (bpm == null)
678         bpm = hiddenProperties.get(name);
679      if (bpm == null)
680         bpm = dynaProperty;
681      return bpm;
682   }
683
684   /**
685    * Returns a mock bean property that resolves to the name <js>"_type"</js> and whose value always resolves to the
686    * dictionary name of the bean.
687    *
688    * @return The type name property.
689    */
690   public BeanPropertyMeta getTypeProperty() { return typeProperty; }
691
692   /**
693    * Returns the type property name for this bean.
694    *
695    * <p>
696    * This is the name of the bean property used to store the dictionary name of a bean type so that the parser knows
697    * the data type to reconstruct.
698    *
699    * <p>
700    * If <jk>null</jk>, <js>"_type"</js> should be assumed.
701    *
702    * <p>
703    * The value is determined from:
704    * <ul>
705    *    <li>The {@link Bean#typePropertyName() @Bean(typePropertyName)} annotation on the class, if present.
706    *    <li>Otherwise, the default value from {@link BeanContext#getBeanTypePropertyName()}.
707    * </ul>
708    *
709    * @return
710    *    The type property name associated with this bean, or <jk>null</jk> if the default <js>"_type"</js> should be used.
711    * @see BeanContext#getBeanTypePropertyName()
712    */
713   public String getTypePropertyName() { return typePropertyName; }
714
715   @Override /* Overridden from Object */
716   public int hashCode() {
717      return classMeta.hashCode();
718   }
719
720   /**
721    * Property read interceptor.
722    *
723    * <p>
724    * Called immediately after calling the getter to allow the value to be overridden.
725    *
726    * @param bean The bean from which the property was read.
727    * @param name The property name.
728    * @param value The value just extracted from calling the bean getter.
729    * @return The value to serialize.  Default is just to return the existing value.
730    */
731   public Object onReadProperty(Object bean, String name, Object value) {
732      return beanFilter == null ? value : beanFilter.readProperty(bean, name, value);
733   }
734
735   /**
736    * Property write interceptor.
737    *
738    * <p>
739    * Called immediately before calling theh setter to allow value to be overwridden.
740    *
741    * @param bean The bean from which the property was read.
742    * @param name The property name.
743    * @param value The value just parsed.
744    * @return The value to serialize.  Default is just to return the existing value.
745    */
746   public Object onWriteProperty(Object bean, String name, Object value) {
747      return beanFilter == null ? value : beanFilter.writeProperty(bean, name, value);
748   }
749
750   protected FluentMap<String,Object> properties() {
751      // @formatter:off
752      return filteredBeanPropertyMap()
753         .a("class", classMeta.getName())
754         .a("properties", properties);
755      // @formatter:on
756   }
757
758   @Override /* Overridden from Object */
759   public String toString() {
760      return r(properties());
761   }
762
763   /**
764    * Returns the bean context that created this metadata object.
765    *
766    * @return The bean context.
767    */
768   protected BeanContext getBeanContext() { return beanContext; }
769
770   /**
771    * Returns the constructor for this bean, if one was found.
772    *
773    * <p>
774    * The constructor is determined by {@link #findBeanConstructor()} and may be:
775    * <ul>
776    *    <li>A constructor annotated with {@link Beanc @Beanc}
777    *    <li>An implementation class constructor (if provided)
778    *    <li>A no-argument constructor
779    *    <li><jk>null</jk> if no suitable constructor was found
780    * </ul>
781    *
782    * @return The constructor for this bean, or <jk>null</jk> if no constructor is available.
783    * @see #getConstructorArgs()
784    * @see #hasConstructor()
785    */
786   protected ConstructorInfo getConstructor() {
787      return beanConstructor.constructor().orElse(null);
788   }
789
790   /**
791    * Returns the list of property names that correspond to the constructor parameters.
792    *
793    * <p>
794    * The property names are in the same order as the constructor parameters. These names are used to map
795    * parsed property values to constructor arguments when creating bean instances.
796    *
797    * <p>
798    * The property names are determined from:
799    * <ul>
800    *    <li>The {@link Beanc#properties() properties()} value in the {@link Beanc @Beanc} annotation (if present)
801    *    <li>Otherwise, the parameter names from the constructor (if available in bytecode)
802    * </ul>
803    *
804    * <p>
805    * If the bean has no constructor or uses a no-argument constructor, this list will be empty.
806    *
807    * @return A list of property names corresponding to constructor parameters, in parameter order.
808    * @see #getConstructor()
809    * @see #hasConstructor()
810    */
811   protected List<String> getConstructorArgs() {
812      return beanConstructor.args();
813   }
814
815   /**
816    * Returns the "extras" property for dynamic bean properties.
817    *
818    * @return The dynamic property, or <jk>null</jk> if not present.
819    */
820   protected BeanPropertyMeta getDynaProperty() { return dynaProperty; }
821
822   /**
823    * Returns the map of getter methods to property names.
824    *
825    * @return The getter properties map.
826    */
827   protected Map<Method,String> getGetterProps() { return getterProps; }
828
829   /**
830    * Returns the map of hidden properties on this bean.
831    *
832    * <p>
833    * Hidden properties are properties that exist on the bean but are not included in the normal property list.
834    * These properties are typically excluded from serialization but may still be accessible programmatically.
835    *
836    * <p>
837    * Hidden properties can be defined through:
838    * <ul>
839    *    <li>{@link BeanFilter#getExcludeProperties() Bean filter exclude properties}
840    *    <li>Properties that fail validation during bean metadata creation
841    * </ul>
842    *
843    * @return A map of hidden property names to their metadata. The map is unmodifiable.
844    * @see #getProperties()
845    */
846   protected Map<String,BeanPropertyMeta> getHiddenProperties() {
847      return hiddenProperties;
848   }
849
850   /**
851    * Returns the map of setter methods to property names.
852    *
853    * @return The setter properties map.
854    */
855   protected Map<Method,String> getSetterProps() { return setterProps; }
856
857   /**
858    * Returns whether this bean has a constructor available for instantiation.
859    *
860    * <p>
861    * A bean has a constructor if {@link #findBeanConstructor()} was able to find a suitable constructor,
862    * which may be:
863    * <ul>
864    *    <li>A constructor annotated with {@link Beanc @Beanc}
865    *    <li>An implementation class constructor (if provided)
866    *    <li>A no-argument constructor
867    * </ul>
868    *
869    * <p>
870    * If this method returns <jk>false</jk>, the bean cannot be instantiated using {@link #newBean(Object)},
871    * and may need to be created through other means (e.g., interface proxies).
872    *
873    * @return <jk>true</jk> if a constructor is available, <jk>false</jk> otherwise.
874    * @see #getConstructor()
875    * @see #newBean(Object)
876    */
877   protected boolean hasConstructor() {
878      return beanConstructor.constructor().isPresent();
879   }
880
881   /**
882    * Returns whether properties should be sorted for this bean.
883    *
884    * @return <jk>true</jk> if properties should be sorted.
885    */
886   protected boolean isSortProperties() { return sortProperties; }
887
888   /**
889    * Creates a new instance of this bean.
890    *
891    * @param outer The outer object if bean class is a non-static inner member class.
892    * @return A new instance of this bean if possible, or <jk>null</jk> if not.
893    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
894    */
895   @SuppressWarnings("unchecked")
896   protected T newBean(Object outer) throws ExecutableException {
897      if (classMeta.isMemberClass() && classMeta.isNotStatic()) {
898         if (hasConstructor())
899            return getConstructor().<T>newInstance(outer);
900      } else {
901         if (hasConstructor())
902            return getConstructor().<T>newInstance();
903         var h = classMeta.getProxyInvocationHandler();
904         if (nn(h)) {
905            var cl = classMeta.getClassLoader();
906            return (T)Proxy.newProxyInstance(cl, a(classMeta.inner(), java.io.Serializable.class), h);
907         }
908      }
909      return null;
910   }
911
912   /*
913    * Finds the appropriate constructor for this bean and determines the property names for constructor arguments.
914    *
915    * <p>
916    * This method searches for a constructor in the following order of precedence:
917    * <ol>
918    *    <li><b>{@link Beanc @Beanc} annotated constructor:</b> If a constructor is annotated with {@link Beanc @Beanc},
919    *       it is used. The property names are determined from:
920    *       <ul>
921    *          <li>The {@link Beanc#properties() properties()} value in the annotation, if specified
922    *          <li>Otherwise, the parameter names from the constructor (if available in bytecode)
923    *       </ul>
924    *       If multiple constructors are annotated with {@link Beanc @Beanc}, an exception is thrown.
925    *    <li><b>Implementation class constructor:</b> If an {@link #implClassConstructor} was provided during bean
926    *       metadata creation, it is used with an empty property list.
927    *    <li><b>No-arg constructor:</b> Searches for a no-argument constructor. The visibility required depends on
928    *       whether the class has a {@link Bean @Bean} annotation:
929    *       <ul>
930    *          <li>If {@link Bean @Bean} is present, private constructors are allowed
931    *          <li>Otherwise, the visibility is determined by {@link BeanContext#getBeanConstructorVisibility()}
932    *       </ul>
933    *    <li><b>No constructor:</b> Returns an empty {@link Optional} if no suitable constructor is found.
934    * </ol>
935    *
936    * <p>
937    * The returned {@link BeanConstructor} contains:
938    * <ul>
939    *    <li>The constructor (if found), wrapped in an {@link Optional}
940    *    <li>A list of property names that correspond to the constructor parameters, in order
941    * </ul>
942    *
943    * @return A {@link BeanConstructor} containing the found constructor and its associated property names.
944    * @throws BeanRuntimeException If multiple constructors are annotated with {@link Beanc @Beanc}, or if
945    *    the number of properties specified in {@link Beanc @Beanc} doesn't match the number of constructor parameters,
946    *    or if parameter names cannot be determined from the bytecode.
947    */
948   private BeanConstructor findBeanConstructor() {
949      var ap = beanContext.getAnnotationProvider();
950      var vis = beanContext.getBeanConstructorVisibility();
951      var ci = classMeta;
952
953      var l = ci.getPublicConstructors().stream().filter(x -> ap.has(Beanc.class, x)).toList();
954      if (l.isEmpty())
955         l = ci.getDeclaredConstructors().stream().filter(x -> ap.has(Beanc.class, x)).toList();
956      if (l.size() > 1)
957         throw bex(ci, "Multiple instances of '@Beanc' found.");
958      if (l.size() == 1) {
959         var con = l.get(0).accessible();
960         var args = ap.find(Beanc.class, con).stream().map(x -> x.inner().properties()).filter(StringUtils::isNotBlank).map(x -> split(x)).findFirst().orElse(liste());
961         if (! con.hasNumParameters(args.size())) {
962            if (ne(args))
963               throw bex(ci, "Number of properties defined in '@Beanc' annotation does not match number of parameters in constructor.");
964            args = con.getParameters().stream().map(x -> x.getName()).toList();
965            for (int i = 0; i < args.size(); i++) {
966               if (isBlank(args.get(i)))
967                  throw bex(ci, "Could not find name for parameter #{0} of constructor ''{1}''", i, con.getFullName());
968            }
969         }
970         return new BeanConstructor(opt(con), args);
971      }
972
973      if (implClassConstructor != null)
974         return new BeanConstructor(opt(implClassConstructor.accessible()), liste());
975
976      var ba = ap.find(Bean.class, classMeta);
977      var con = ci.getNoArgConstructor(! ba.isEmpty() ? Visibility.PRIVATE : vis).orElse(null);
978      if (con != null)
979         return new BeanConstructor(opt(con.accessible()), liste());
980
981      return new BeanConstructor(opte(), liste());
982   }
983
984   /*
985    * Finds all bean fields in the class hierarchy.
986    *
987    * <p>
988    * Traverses the complete class hierarchy (as defined by {@link #classHierarchy}) and collects all fields that
989    * meet the bean field criteria:
990    * <ul>
991    *    <li>Not static
992    *    <li>Not transient (unless transient fields are not ignored)
993    *    <li>Not annotated with {@link Transient @Transient} (unless transient fields are not ignored)
994    *    <li>Not annotated with {@link BeanIgnore @BeanIgnore}
995    *    <li>Visible according to the specified visibility level, or annotated with {@link Beanp @Beanp}
996    * </ul>
997    *
998    * @return A collection of all bean fields found in the class hierarchy.
999    */
1000   private Collection<FieldInfo> findBeanFields() {
1001      var v = beanContext.getBeanFieldVisibility();
1002      var noIgnoreTransients = ! beanContext.isIgnoreTransientFields();
1003      var ap = beanContext.getAnnotationProvider();
1004      // @formatter:off
1005      return classHierarchy.get().stream()
1006         .flatMap(c2 -> c2.getDeclaredFields().stream())
1007         .filter(x -> x.isNotStatic()
1008            && (x.isNotTransient() || noIgnoreTransients)
1009            && (! x.hasAnnotation(Transient.class) || noIgnoreTransients)
1010            && ! ap.has(BeanIgnore.class, x)
1011            && (v.isVisible(x.inner()) || ap.has(Beanp.class, x)))
1012         .toList();
1013      // @formatter:on
1014   }
1015
1016   /*
1017    * Finds all bean methods (getters, setters, and extraKeys) in the class hierarchy.
1018    *
1019    * <p>
1020    * Traverses the complete class hierarchy (as defined by {@link #classHierarchy}) and identifies methods that
1021    * represent bean properties. Methods are identified as:
1022    * <ul>
1023    *    <li><b>Getters:</b> Methods with no parameters that return a value, matching patterns like:
1024    *       <ul>
1025    *          <li><c>getX()</c> - standard getter pattern
1026    *          <li><c>isX()</c> - boolean getter pattern (returns boolean or Boolean)
1027    *          <li>Methods annotated with {@link Beanp @Beanp} or {@link Name @Name}
1028    *          <li>Methods with {@link Beanp @Beanp} annotation with value <js>"*"</js> that return a Map
1029    *       </ul>
1030    *    <li><b>Setters:</b> Methods with one parameter that match patterns like:
1031    *       <ul>
1032    *          <li><c>setX(value)</c> - standard setter pattern
1033    *          <li><c>withX(value)</c> - fluent setter pattern (returns the bean type)
1034    *          <li>Methods annotated with {@link Beanp @Beanp} or {@link Name @Name}
1035    *          <li>Methods with {@link Beanp @Beanp} annotation with value <js>"*"</js> that accept a Map
1036    *          <li>Fluent setters (if enabled) - methods that return the bean type and accept one parameter
1037    *       </ul>
1038    *    <li><b>ExtraKeys:</b> Methods with {@link Beanp @Beanp} annotation with value <js>"*"</js> that return a Collection
1039    * </ul>
1040    *
1041    * <p>
1042    * Methods are filtered based on:
1043    * <ul>
1044    *    <li>Not static, not bridge methods
1045    *    <li>Parameter count ≤ 2
1046    *    <li>Not annotated with {@link BeanIgnore @BeanIgnore}
1047    *    <li>Not annotated with {@link Transient @Transient}
1048    *    <li>Visible according to the specified visibility level, or annotated with {@link Beanp @Beanp} or {@link Name @Name}
1049    * </ul>
1050    *
1051    * <p>
1052    * Property names are determined from:
1053    * <ul>
1054    *    <li>{@link Beanp @Beanp} or {@link Name @Name} annotations (if present)
1055    *    <li>Otherwise, derived from the method name using the provided {@link PropertyNamer}
1056    * </ul>
1057    *
1058    * @return A list of {@link BeanMethod} objects representing all found bean methods.
1059    */
1060   private List<BeanMethod> findBeanMethods() {
1061      var l = new LinkedList<BeanMethod>();
1062      var ap = beanContext.getAnnotationProvider();
1063      var ci = classMeta;
1064      var v = beanContext.getBeanMethodVisibility();
1065      var pn = opt(beanFilter).map(x -> x.getPropertyNamer()).orElse(beanContext.getPropertyNamer());
1066
1067      classHierarchy.get().stream().forEach(c2 -> {
1068         for (var m : c2.getDeclaredMethods()) {
1069
1070            if (m.isStatic() || m.isBridge() || m.getParameterCount() > 2)
1071               continue;
1072
1073            var mm = m.getMatchingMethods();
1074
1075            if (mm.stream().anyMatch(m2 -> ap.has(BeanIgnore.class, m2, SELF)))
1076               continue;
1077
1078            if (mm.stream().anyMatch(m2 -> ap.find(Transient.class, m2, SELF).stream().map(x -> x.inner().value()).findFirst().orElse(false)))
1079               continue;
1080
1081            var beanps = ap.find(Beanp.class, m).stream().map(AnnotationInfo::inner).toList();
1082            var names = ap.find(Name.class, m).stream().map(AnnotationInfo::inner).toList();
1083
1084            if (! (m.isVisible(v) || ne(beanps) || ne(names)))
1085               continue;
1086
1087            var n = m.getSimpleName();
1088
1089            var params = m.getParameters();
1090            var rt = m.getReturnType();
1091            var methodType = UNKNOWN;
1092            var bpName = bpName(beanps, names);
1093
1094            if (params.isEmpty()) {
1095               if ("*".equals(bpName)) {
1096                  if (rt.isChildOf(Collection.class)) {
1097                     methodType = EXTRAKEYS;
1098                  } else if (rt.isChildOf(Map.class)) {
1099                     methodType = GETTER;
1100                  }
1101                  n = bpName;
1102               } else if (n.startsWith("get") && (! rt.is(Void.TYPE))) {
1103                  methodType = GETTER;
1104                  n = n.substring(3);
1105               } else if (n.startsWith("is") && (rt.is(Boolean.TYPE) || rt.is(Boolean.class))) {
1106                  methodType = GETTER;
1107                  n = n.substring(2);
1108               } else if (nn(bpName)) {
1109                  methodType = GETTER;
1110                  if (bpName.isEmpty()) {
1111                     if (n.startsWith("get"))
1112                        n = n.substring(3);
1113                     else if (n.startsWith("is"))
1114                        n = n.substring(2);
1115                     bpName = n;
1116                  } else {
1117                     n = bpName;
1118                  }
1119               }
1120            } else if (params.size() == 1) {
1121               if ("*".equals(bpName)) {
1122                  if (params.get(0).getParameterType().isChildOf(Map.class)) {
1123                     methodType = SETTER;
1124                     n = bpName;
1125                  } else if (params.get(0).getParameterType().is(String.class)) {
1126                     methodType = GETTER;
1127                     n = bpName;
1128                  }
1129               } else if (n.startsWith("set") && (rt.isParentOf(ci) || rt.is(Void.TYPE))) {
1130                  methodType = SETTER;
1131                  n = n.substring(3);
1132               } else if (n.startsWith("with") && (rt.isParentOf(ci))) {
1133                  methodType = SETTER;
1134                  n = n.substring(4);
1135               } else if (nn(bpName)) {
1136                  methodType = SETTER;
1137                  if (bpName.isEmpty()) {
1138                     if (n.startsWith("set"))
1139                        n = n.substring(3);
1140                     bpName = n;
1141                  } else {
1142                     n = bpName;
1143                  }
1144               } else if (fluentSetters && rt.isParentOf(ci)) {
1145                  methodType = SETTER;
1146               }
1147            } else if (params.size() == 2) {
1148               if ("*".equals(bpName) && params.get(0).getParameterType().is(String.class)) {
1149                  if (n.startsWith("set") && (rt.isParentOf(ci) || rt.is(Void.TYPE))) {
1150                     methodType = SETTER;
1151                  } else {
1152                     methodType = GETTER;
1153                  }
1154                  n = bpName;
1155               }
1156            }
1157            n = pn.getPropertyName(n);
1158
1159            if ("*".equals(bpName) && methodType == UNKNOWN)
1160               throw bex(ci, "Found @Beanp(\"*\") but could not determine method type on method ''{0}''.", m.getSimpleName());
1161
1162            if (methodType != UNKNOWN) {
1163               if (nn(bpName) && ! bpName.isEmpty())
1164                  n = bpName;
1165               if (nn(n))
1166                  l.add(new BeanMethod(n, methodType, m.inner()));
1167            }
1168         }
1169      });
1170      return l;
1171   }
1172
1173   /*
1174    * Creates a bean registry for this bean class.
1175    *
1176    * <p>
1177    * The bean registry is used to resolve dictionary names (type names) to actual class types. This is essential
1178    * for polymorphic bean serialization and parsing, where a dictionary name in the serialized form needs to be
1179    * mapped back to the correct subclass.
1180    *
1181    * <p>
1182    * The registry is built from:
1183    * <ul>
1184    *    <li><b>Bean filter dictionary:</b> Classes specified in the {@link BeanFilter#getBeanDictionary() bean filter's dictionary}
1185    *       (if a bean filter is present)
1186    *    <li><b>{@link Bean @Bean} annotation:</b> If the class has a {@link Bean @Bean} annotation with a non-empty
1187    *       {@link Bean#typeName() typeName()}, the class itself is added to the dictionary
1188    * </ul>
1189    *
1190    * <p>
1191    * The registry is used by parsers to determine which class to instantiate when deserializing polymorphic beans.
1192    *
1193    * @return A new {@link BeanRegistry} containing the dictionary classes for this bean, or an empty registry if
1194    *    no dictionary classes are found.
1195    */
1196   private BeanRegistry findBeanRegistry() {
1197      // Bean dictionary on bean filter.
1198      var beanDictionaryClasses = opt(beanFilter).map(x -> new ArrayList<>(x.getBeanDictionary())).orElse(new ArrayList<>());
1199
1200      // Bean dictionary from @Bean(typeName) annotation.
1201      var ba = beanContext.getAnnotationProvider().find(Bean.class, classMeta);
1202      ba.stream().map(x -> x.inner().typeName()).filter(Utils::ne).findFirst().ifPresent(x -> beanDictionaryClasses.add(classMeta));
1203
1204      return new BeanRegistry(beanContext, null, beanDictionaryClasses);
1205   }
1206
1207   /*
1208    * Builds a list of all classes in the class hierarchy for this bean.
1209    *
1210    * <p>
1211    * Traverses the complete inheritance hierarchy (classes and interfaces) starting from the bean class (or the
1212    * interface class specified in the bean filter) and collects all classes up to (but not including) the stop class.
1213    *
1214    * <p>
1215    * The traversal order follows a depth-first approach:
1216    * <ol>
1217    *    <li>First, recursively traverses the superclass hierarchy
1218    *    <li>Then, recursively traverses all implemented interfaces
1219    *    <li>Finally, adds the current class itself
1220    * </ol>
1221    *
1222    * <p>
1223    * If a {@link BeanFilter#getInterfaceClass() bean filter interface class} is specified, the traversal starts
1224    * from that interface class instead of the bean class itself. This allows beans to use properties defined on
1225    * a parent interface rather than the concrete implementation class.
1226    *
1227    * <p>
1228    * The resulting list is used to find bean properties, methods, and fields across the entire class hierarchy.
1229    *
1230    * @return An unmodifiable list of {@link ClassInfo} objects representing all classes in the hierarchy, in
1231    *    traversal order (superclasses first, then interfaces, then the class itself).
1232    */
1233   private List<ClassInfo> findClassHierarchy() {
1234      var result = new LinkedList<ClassInfo>();
1235      // If @Bean.interfaceClass is specified on the parent class, then we want
1236      // to use the properties defined on that class, not the subclass.
1237      var c2 = (nn(beanFilter) && nn(beanFilter.getInterfaceClass()) ? beanFilter.getInterfaceClass() : classMeta);
1238      findClassHierarchy(c2, stopClass, result::add);
1239      return u(result);
1240   }
1241
1242   /*
1243    * Recursively traverses the class hierarchy and invokes the consumer for each class found.
1244    *
1245    * <p>
1246    * This is a helper method that performs a depth-first traversal of the class hierarchy:
1247    * <ol>
1248    *    <li>Recursively processes the superclass (if present and not the stop class)
1249    *    <li>Recursively processes all implemented interfaces
1250    *    <li>Invokes the consumer with the current class
1251    * </ol>
1252    *
1253    * <p>
1254    * The traversal stops when it reaches the stop class (which is not included in the traversal).
1255    *
1256    * @param c The class to start traversal from.
1257    * @param stopClass The class to stop traversal at (exclusive). Traversal will not proceed beyond this class.
1258    * @param consumer The consumer to invoke for each class in the hierarchy.
1259    */
1260   private void findClassHierarchy(ClassInfo c, ClassInfo stopClass, Consumer<ClassInfo> consumer) {
1261      var sc = c.getSuperclass();
1262      if (nn(sc) && ! sc.is(stopClass.inner()))
1263         findClassHierarchy(sc, stopClass, consumer);
1264      c.getInterfaces().forEach(x -> findClassHierarchy(x, stopClass, consumer));
1265      consumer.accept(c);
1266   }
1267
1268   /*
1269    * Finds the dictionary name (type name) for this bean class.
1270    *
1271    * <p>
1272    * The dictionary name is used in serialized forms to identify the specific type of a bean instance, enabling
1273    * polymorphic serialization and deserialization. This is especially important when serializing/parsing beans
1274    * that are part of an inheritance hierarchy.
1275    *
1276    * <p>
1277    * The dictionary name is determined by searching in the following order of precedence:
1278    * <ol>
1279    *    <li><b>Bean filter type name:</b> If a bean filter is present and has a type name specified via
1280    *       {@link BeanFilter#getTypeName()}, that value is used.
1281    *    <li><b>Bean registry lookup:</b> If a bean registry exists for this bean, it is queried for the type name
1282    *       of this class.
1283    *    <li><b>Parent class registry lookup:</b> Searches through parent classes and interfaces (starting from the
1284    *       second one, skipping the class itself) and checks if any of their bean registries contain a type name
1285    *       for this class.
1286    *    <li><b>{@link Bean @Bean} annotation:</b> If the class has a {@link Bean @Bean} annotation with a non-empty
1287    *       {@link Bean#typeName() typeName()}, that value is used.
1288    *    <li><b>No dictionary name:</b> Returns <jk>null</jk> if no dictionary name is found.
1289    * </ol>
1290    *
1291    * <p>
1292    * If a dictionary name is found, it will be used in serialized output (typically as a special property like
1293    * <js>"_type"</js>) so that parsers can determine the correct class to instantiate when deserializing.
1294    *
1295    * @return The dictionary name for this bean, or <jk>null</jk> if no dictionary name is defined.
1296    */
1297   private String findDictionaryName() {
1298      if (nn(beanFilter) && nn(beanFilter.getTypeName()))
1299         return beanFilter.getTypeName();
1300
1301      var br = getBeanRegistry();
1302      if (nn(br)) {
1303         String s = br.getTypeName(this.classMeta);
1304         if (nn(s))
1305            return s;
1306      }
1307
1308      var n = classMeta
1309         .getParentsAndInterfaces()
1310         .stream()
1311         .skip(1)
1312         .map(x -> beanContext.getClassMeta(x))
1313         .map(x -> x.getBeanRegistry())
1314         .filter(Objects::nonNull)
1315         .map(x -> x.getTypeName(this.classMeta))
1316         .filter(Objects::nonNull)
1317         .findFirst()
1318         .orElse(null);
1319
1320      if (n != null)
1321         return n;
1322
1323      return classMeta.getBeanContext().getAnnotationProvider().find(Bean.class, classMeta)
1324         .stream()
1325         .map(AnnotationInfo::inner)
1326         .filter(x -> ! x.typeName().isEmpty())
1327         .map(x -> x.typeName())
1328         .findFirst()
1329         .orElse(null);
1330   }
1331
1332   /*
1333    * Finds a bean field by name in the class hierarchy.
1334    *
1335    * <p>
1336    * Searches through the complete class hierarchy (as defined by {@link #classHierarchy}) to find a field
1337    * with the specified name. The search is performed in the order classes appear in the hierarchy, and the
1338    * first matching field is returned.
1339    *
1340    * <p>
1341    * A field is considered a match if it:
1342    * <ul>
1343    *    <li>Has the exact name specified
1344    *    <li>Is not static
1345    *    <li>Is not transient (unless transient fields are not ignored)
1346    *    <li>Is not annotated with {@link Transient @Transient} (unless transient fields are not ignored)
1347    *    <li>Is not annotated with {@link BeanIgnore @BeanIgnore}
1348    * </ul>
1349    *
1350    * <p>
1351    * This method is used to find fields for bean properties when a field reference is needed but wasn't
1352    * discovered during the initial property discovery phase (e.g., for properties defined only through
1353    * getters/setters).
1354    *
1355    * @param name The name of the field to find.
1356    * @return The {@link FieldInfo} for the field if found, or <jk>null</jk> if no matching field exists
1357    *    in the class hierarchy.
1358    */
1359   private Optional<FieldInfo> findInnerBeanField(String name) {
1360      var noIgnoreTransients = ! beanContext.isIgnoreTransientFields();
1361      var ap = beanContext.getAnnotationProvider();
1362
1363      // @formatter:off
1364      return classHierarchy.get().stream()
1365         .flatMap(c2 -> c2.getDeclaredField(
1366            x -> x.isNotStatic()
1367               && (x.isNotTransient() || noIgnoreTransients)
1368               && (! x.hasAnnotation(Transient.class) || noIgnoreTransients)
1369               && ! ap.has(BeanIgnore.class, x)
1370               && x.hasName(name)
1371         ).stream())
1372         .findFirst();
1373      // @formatter:on
1374   }
1375}