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 org.apache.juneau.commons.utils.ClassUtils.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023import static org.apache.juneau.commons.utils.ThrowableUtils.*;
024
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.commons.reflect.*;
029
030/**
031 * Java bean wrapper class.
032 *
033 * <h5 class='topic'>Description</h5>
034 *
035 * A wrapper that wraps Java bean instances inside of a {@link Map} interface that allows properties on the wrapped
036 * object can be accessed using the {@link Map#get(Object) get()} and {@link Map#put(Object,Object) put()} methods.
037 *
038 * <p>
039 * Create instances through the bean API.  For the bean-modeling-only path use {@link BeanMap#of(Object)} or
040 * {@link BeanMap#of(Object, BeanMeta)}; the marshalling layer's {@code MarshallingSession#toBeanMap(Object)}
041 * additionally wires a {@link BeanSession} into the resulting map for session-aware behavior.
042 *
043 * <h5 class='topic'>Bean property order</h5>
044 *
045 * The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()}
046 * methods is as follows:
047 * <ul class='spaced-list'>
048 *    <li>
049 *       If the {@link BeanType @BeanType} annotation specifies an explicit property list via
050 *       {@link BeanType#properties() properties()} (or its synonym {@link BeanType#p() p()}),
051 *       the order matches the annotation.
052 *    <li>
053 *       Otherwise, if {@link BeanType#unsorted() @BeanType(unsorted=true)} is set on the class, or
054 *       {@link BeanConfigContext#isUnsortedProperties()} is enabled on the session, properties are returned
055 *       in their natural reflection order.
056 *    <li>
057 *       Otherwise, properties are returned in alphabetical order.
058 * </ul>
059 *
060 * <h5 class='topic'>POJO swaps</h5>
061 *
062 * If {@code ObjectSwap} transforms are defined on the class types of the properties of this bean or the bean properties
063 * themselves, the {@link #get(Object)} and {@link #put(String, Object)} methods will automatically transform the
064 * property value to and from the serialized form.
065 *
066 * <h5 class='topic'>Thread safety</h5>
067 *
068 * Instances are not thread-safe.
069 * This type is mutable (wrapped bean reference, property caches, and optional session pointer) and does not perform
070 * internal synchronization.
071 *
072 *
073 * @param <T> Specifies the type of object that this map encapsulates.
074 */
075@SuppressWarnings({
076   "java:S3776", // Cognitive complexity acceptable for bean property iteration and filtering
077   "unchecked" // of() casts bean.getClass() to Class<T>, setBean() casts to T, and getProperty() casts the value to T2; all are tied to the wrapped bean type
078})
079public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T> {
080
081   /**
082    * Convenience method for wrapping a bean inside a {@link BeanMap}.
083    *
084    * @param <T> The bean type.
085    * @param bean The bean being wrapped.  Must not be <jk>null</jk> (its runtime class is used to build the metadata).
086    * @return A new {@link BeanMap} instance wrapping the bean.
087    */
088   public static <T> BeanMap<T> of(T bean) {
089      return new BeanMap<>(bean, BeanMeta.of((Class<T>) bean.getClass()));
090   }
091
092   /**
093    * Convenience method for wrapping a bean inside a {@link BeanMap} using a pre-built {@link BeanMeta}.
094    *
095    * <p>
096    * This is the bean-modeling entry point — it builds a {@link BeanMap} without going through a
097    * marshalling session, paired with a {@link BeanMeta} typically produced by
098    * {@link BeanMeta#of(Class, BeanConfigContext)}.  No marshalling session is attached.
099    *
100    * @param <T> The bean type.
101    * @param bean The bean being wrapped.  Can be <jk>null</jk> for a read-only bean whose properties are supplied later and instantiated via {@link #getBean()}.
102    * @param meta The bean metadata.  Must not be <jk>null</jk>.
103    * @return A new {@link BeanMap} instance wrapping the bean.
104    */
105   public static <T> BeanMap<T> of(T bean, BeanMeta<T> meta) {
106      return new BeanMap<>(bean, meta);
107   }
108
109   /** The wrapped object. */
110   protected T bean;
111
112   /** Temporary holding cache for beans with read-only properties.  Normally null. */
113   protected Map<String,Object> propertyCache;
114
115   private Set<String> presentProperties;  // Names of required properties seen so far.  null when the bean has no required properties.
116
117   /** Temporary holding cache for bean properties of array types when the add() method is being used. */
118   protected Map<String,List<?>> arrayPropertyCache;
119
120   /** The BeanMeta associated with the class of the object. */
121   protected BeanMeta<T> meta;
122   private BeanSession session;
123
124   private final String typePropertyName;
125
126   /**
127    * Constructor.
128    *
129    * <p>
130    * Bean-modeling-only constructor. Does not carry a {@link BeanSession} reference.
131    * The marshalling layer wires the session in via {@link #setBeanSession(BeanSession)}
132    * immediately after construction.
133    *
134    * @param bean The bean to wrap inside this map.  Can be <jk>null</jk> for a read-only bean whose properties are supplied later and instantiated via {@link #getBean()}.
135    * @param meta The metadata associated with the bean class.  Must not be <jk>null</jk>.
136    */
137   public BeanMap(T bean, BeanMeta<T> meta) {
138      this.bean = bean;
139      this.meta = meta;
140      if (ine(meta.getConstructorArgs()))
141         propertyCache = new TreeMap<>();
142      this.typePropertyName = meta.getTypePropertyName();
143      if (meta.hasRequiredProperties())
144         presentProperties = new HashSet<>();
145   }
146
147   /**
148    * Records that a property was written from input.  Only tracked when the bean has required properties.
149    *
150    * @param name The property name.
151    */
152   void recordPresent(String name) {
153      if (nn(presentProperties))
154         presentProperties.add(name);
155   }
156
157   /**
158    * Returns the {@code @BeanProp(required=true)} properties that have not been written to this map.
159    *
160    * <p>
161    * A property counts as written once {@link #put(String, Object)} (or a collection {@code add}) has been called for
162    * it, even with a <jk>null</jk> value.  Parsers call this once per bean, before the bean is constructed.
163    *
164    * <h5 class='section'>Example:</h5>
165    * <p class='bjava'>
166    *    <jc>// MyBean has @BeanProp(required=true) on 'name' and 'zip'.</jc>
167    *    BeanMap&lt;MyBean&gt; <jv>m</jv> = <jv>session</jv>.newBeanMap(MyBean.<jk>class</jk>);
168    *    <jv>m</jv>.put(<js>"name"</js>, <jk>null</jk>);
169    *
170    *    <jc>// [zip]</jc>
171    *    List&lt;String&gt; <jv>missing</jv> = <jv>m</jv>.getMissingRequiredProperties();
172    * </p>
173    *
174    * @return The missing required property names, in property order.  Empty (never <jk>null</jk>) if none.
175    */
176   public List<String> getMissingRequiredProperties() {
177      if (presentProperties == null)
178         return List.of();
179      return meta.getRequiredPropertyNames().stream().filter(x -> ! presentProperties.contains(x)).toList();
180   }
181
182   /**
183    * Wires this bean map to a {@link BeanSession}.
184    *
185    * <p>
186    * Called by marshalling-side construction paths (for example,
187    * {@code MarshallingSession#toBeanMap(Object)}) to wire in the session used by session-aware
188    * operations (type conversion, child collection construction, and parser/serializer-backed
189    * conversions) after bean-modeling construction.
190    *
191    * @param value The bean session that produced this bean map.  Typically a marshalling-session implementation.  Can be <jk>null</jk> to detach the session.
192    */
193   public void setBeanSession(BeanSession value) {
194      session = value;
195   }
196
197   /**
198    * Add a value to a collection or array property.
199    *
200    * <p>
201    * As a general rule, adding to arrays is not recommended since the array must be recreate each time this method is
202    * called.
203    *
204    * @param property Property name or child-element name (if {@code @Xml(childName)} is specified).
205    * @param value The value to add to the collection or array.  Can be <jk>null</jk>.
206    */
207   public void add(String property, Object value) {
208      var p = getPropertyMeta(property);
209      if (p == null) {
210         if (meta.getConfig().isIgnoreUnknownBeanProperties())
211            return;
212         throw brex(meta.getClassInfo(), "Bean property '%s' not found.", property);
213      }
214      p.add(this, property, value);
215   }
216
217   @Override /* Overridden from Map */
218   public boolean containsKey(Object property) {
219      // JUNEAU-248: Match the behavior of keySet() - only check properties map, not hiddenProperties
220      var key = emptyIfNull(property);
221      if (meta.getProperties().containsKey(key) && neq(key, "*"))
222         return true;
223      if (nn(meta.getDynaProperty())) {
224         try {
225            return meta.getDynaProperty().getDynaMap(bean).containsKey(key);
226         } catch (Exception e) {
227            throw brex(e);
228         }
229      }
230      return false;
231   }
232
233   /**
234    * Returns all the properties associated with the bean.
235    *
236    * @return A new set.
237    */
238   @Override
239   public Set<Entry<String,Object>> entrySet() {
240
241      // If this bean has a dyna-property, then we need to construct the entire set before returning.
242      // Otherwise, we can create an iterator without a new data structure.
243      if (nn(meta.getDynaProperty())) {
244         Set<Entry<String,Object>> s = set();
245         forEachProperty(x -> true, x -> {
246            if (x.isDyna()) {
247               try {
248                  x.getDynaMap(bean).entrySet().forEach(y -> s.add(new BeanMapEntry(this, x, y.getKey())));
249               } catch (Exception e) {
250                  throw brex(e);
251               }
252            } else {
253               s.add(new BeanMapEntry(this, x, x.getName()));
254            }
255         });
256         return s;
257      }
258
259      // Construct our own anonymous set to implement this function.
260      return new AbstractSet<>() {
261
262         // Get the list of properties from the meta object.
263         // Note that the HashMap.values() method caches results, so this collection
264         // will really only be constructed once per bean type since the underlying
265         // map never changes.
266         final Collection<BeanPropertyMeta> pSet = getProperties();
267
268         @Override /* Overridden from Set */
269         public Iterator<Map.Entry<String,Object>> iterator() {
270
271            // Construct our own anonymous iterator that uses iterators against the meta.getProperties()
272            // map to maintain position.  This prevents us from having to construct any of our own
273            // collection objects.
274            return new Iterator<>() {
275
276               final Iterator<BeanPropertyMeta> pIterator = pSet.iterator();
277
278               @Override /* Overridden from Iterator */
279               public boolean hasNext() {
280                  return pIterator.hasNext();
281               }
282
283               @Override /* Overridden from Iterator */
284               public Map.Entry<String,Object> next() {
285                  return new BeanMapEntry(BeanMap.this, pIterator.next(), null);
286               }
287
288               @Override /* Overridden from Iterator */
289               public void remove() {
290                  throw uoex("Cannot remove item from iterator.");
291               }
292            };
293         }
294
295         @Override /* Overridden from Set */
296         public int size() {
297            return pSet.size();
298         }
299      };
300   }
301
302   /**
303    * Performs an action on each property in this bean map.
304    *
305    * <p>
306    * If a {@link BeanSession} is attached and has an active view, only properties that are visible under
307    * that view (as determined by {@link BeanSession#isPropertyInActiveView(BeanPropertyMeta)}) are visited.
308    *
309    * @param filter The filter to apply to properties.
310    * @param action The action.
311    * @return This object.
312    */
313   public BeanMap<T> forEachProperty(Predicate<BeanPropertyMeta> filter, Consumer<BeanPropertyMeta> action) {
314      for (var p : meta.getPropertyArray())
315         if (filter.test(p) && isInActiveView(p))
316            action.accept(p);
317      return this;
318   }
319
320   /**
321    * Returns <jk>true</jk> if the property is visible under the active view on this bean map's session.
322    *
323    * <p>
324    * When no session is attached, or no active view is set, all properties are visible.
325    *
326    * @param pMeta The property to test.
327    * @return <jk>true</jk> if the property is in-view.
328    */
329   protected boolean isInActiveView(BeanPropertyMeta pMeta) {
330      return session == null || session.isPropertyInActiveView(pMeta);
331   }
332
333   /**
334    * Invokes all the getters on this bean and consumes the results.
335    *
336    * @param valueFilter Filter to apply to value before applying action.
337    * @param action The action to perform.
338    * @return The list of all bean property values.
339    */
340   public BeanMap<T> forEachValue(Predicate<Object> valueFilter, BeanPropertyConsumer action) {
341
342      // Normal bean.
343      if (meta.getDynaProperty() == null) {
344         forEachProperty(BeanPropertyMeta::canRead, bpm -> {
345            try {
346               var val = bpm.get(this, null);
347               if (valueFilter.test(val))
348                  action.apply(bpm, bpm.getName(), val, null);
349            } catch (Exception t) {
350               action.apply(bpm, bpm.getName(), null, t);
351            }
352         });
353
354         // Bean with dyna properties.
355      } else {
356         Map<String,BeanPropertyValue> actions = (meta.isUnsortedProperties() ? map() : sortedMap());
357
358         forEachProperty(x -> ! x.isDyna() && x.canRead(), bpm -> {
359            try {
360               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), bpm.get(this, null), null));
361            } catch (Exception t) {
362               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), null, t));
363            }
364         });
365
366         forEachProperty(x -> x.isDyna() && x.canRead(), bpm -> {
367            Map<String,Object> dynaMap;
368            try {
369               dynaMap = bpm.getDynaMap(bean);
370            } catch (Exception e) {
371               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), null, e));
372               return;
373            }
374            if (nn(dynaMap)) {
375               dynaMap.forEach((k, v) -> {
376                  try {
377                     var val = bpm.get(this, k);
378                     actions.put(k, new BeanPropertyValue(bpm, k, val, null));
379                  } catch (Exception e) {
380                     actions.put(k, new BeanPropertyValue(bpm, k, null, e));
381                  }
382               });
383            }
384         });
385
386         actions.forEach((k, v) -> {
387            if (v.getThrown() != null || valueFilter.test(v.getValue()))
388               action.apply(v.getMeta(), v.getName(), v.getValue(), v.getThrown());
389         });
390      }
391
392      return this;
393   }
394
395   /**
396    * Gets a property on the bean.
397    *
398    * <p>
399    * If an {@code ObjectSwap}-style transform is associated with this bean property or bean property type class, then this method
400    * will return the transformed value.
401    * For example, if the bean property type class is a {@link Date} and the bean property has the
402    * {@code DateFormat.ISO_INSTANT} setting associated with it through the
403    * {@code @Swap(value)} annotation, this method will return a String containing an
404    * ISO8601 date-time string value.
405    *
406    * <h5 class='section'>Example:</h5>
407    * <p class='bjava'>
408    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
409    *    Person <jv>person</jv> = <jk>new</jk> Person();
410    *    <jv>person</jv>.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6));
411    *
412    *    <jc>// Create a marshalling context and add the ISO8601 date-time swap</jc>
413    *    MarshallingContext <jv>marshallingContext</jv> = MarshallingContext.<jsm>create</jsm>().dateFormat(DateFormat.<jsf>ISO_INSTANT</jsf>).build();
414    *
415    *    <jc>// Wrap our bean in a bean map</jc>
416    *    BeanMap&lt;Person&gt; <jv>beanMap</jv> = <jv>marshallingContext</jv>.toBeanMap(<jv>person</jv>);
417    *
418    *    <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc>
419    *    String <jv>birthDate</jv> = <jv>beanMap</jv>.get(<js>"birthDate"</js>);
420    * </p>
421    *
422    * @param property The name of the property to get.
423    * @return The property value.
424    * @throws RuntimeException if any of the following occur.
425    *    <ol>
426    *       <li>BeanMapEntry does not exist on the underlying object.
427    *       <li>Security settings prevent access to the underlying object getter method.
428    *       <li>An exception occurred inside the getter method.
429    *    </ol>
430    */
431   @Override /* Overridden from Map */
432   public Object get(Object property) {
433      var pName = s(property);
434      var p = getPropertyMeta(pName);
435      if (p == null)
436         return meta.onReadProperty(this.bean, pName, null);
437      return p.get(this, pName);
438   }
439
440   /**
441    * Same as {@link #get(Object)} but casts the value to the specific type.
442    *
443    * @param <T2> The type to cast to.
444    * @param property The name of the property to get.
445    * @param c The type to cast to.
446    * @return The property value.
447    * @throws RuntimeException if any of the following occur.
448    *    <ol>
449    *       <li>BeanMapEntry does not exist on the underlying object.
450    *       <li>Security settings prevent access to the underlying object getter method.
451    *       <li>An exception occurred inside the getter method.
452    *    </ol>
453    * @throws ClassCastException if property is not the specified type.
454    */
455   public <T2> T2 get(String property, Class<T2> c) {
456      var pName = s(property);
457      var p = getPropertyMeta(pName);
458      if (p == null)
459         return (T2)meta.onReadProperty(this.bean, pName, null);
460      return (T2)p.get(this, pName);
461   }
462
463   /**
464    * Returns the wrapped bean object.
465    *
466    * <p>
467    * Triggers bean creation if bean has read-only properties set through a constructor defined by the
468    * {@link BeanCtor @BeanCtor} annotation.
469    *
470    * <p>
471    * The post-creation Optional&lt;X&gt; initialization step (which seeds null {@link Optional} properties with
472    * {@link BeanInfo#getOptionalDefault()}) is skipped for properties built via the bean-modeling-only path
473    * ({@link BeanMeta#of(Class, BeanConfigContext)}) because per-property type metadata is unavailable;
474    * those properties are left untouched and any {@link Optional}-typed field stays at its constructor-assigned
475    * value.
476    *
477    * @return The inner bean object.
478    */
479   public T getBean() {
480      T b = getBean(true);
481
482      // If we have any arrays that need to be constructed, do it now.
483      if (nn(arrayPropertyCache)) {
484         arrayPropertyCache.forEach((k, v) -> {
485            try {
486               getPropertyMeta(k).setArray(b, v);
487            } catch (Exception e1) {
488               throw toRex(e1);
489            }
490         });
491         arrayPropertyCache = null;
492      }
493
494      // Initialize any null Optional<X> fields.  Skip properties whose ClassMeta is unavailable
495      // (bean-modeling-only path — Optional handling is a marshalling concern that requires type metadata).
496      meta.getProperties().forEach((k,v) -> {
497         var cm = v.getBeanInfo();
498         if (nn(cm) && cm.isOptional() && v.get(this, k) == null)
499            v.set(this, k, cm.getOptionalDefault());
500      });
501
502      // Do the same for hidden fields.
503      meta.getHiddenProperties().forEach((k, v) -> {
504         var cm = v.getBeanInfo();
505         if (nn(cm) && cm.isOptional() && v.get(this, k) == null)
506            v.set(this, k, cm.getOptionalDefault());
507      });
508
509      return b;
510   }
511
512   /**
513    * Returns the wrapped bean object.
514    *
515    * <p>
516    * If <c>create</c> is <jk>false</jk>, then this method may return <jk>null</jk> if the bean has read-only
517    * properties set through a constructor defined by the {@link BeanCtor @BeanCtor} annotation.
518    *
519    * <p>
520    * This method does NOT always return the bean in it's final state.
521    * Array properties temporary stored as ArrayLists are not finalized until the {@link #getBean()} method is called.
522    *
523    * @param create If bean hasn't been instantiated yet, then instantiate it.
524    * @return The inner bean object.
525    */
526   public T getBean(boolean create) {
527      /** If this is a read-only bean, then we need to create it. */
528      if (bean == null && create && ine(meta.getConstructorArgs())) {
529         var props = meta.getConstructorArgs();
530         var c = meta.getConstructor();
531         var args = new Object[props.size()];
532         for (var i = 0; i < props.size(); i++) {
533            var propName = props.get(i);
534            var rawVal = propertyCache.remove(propName);
535            // Convert value to expected property type if it's not already the correct type.
536            // Uses the same logic as MarshallingSession.convertToMemberType's early-return check:
537            // collections/maps with specific element/value types always need conversion.
538            if (rawVal != null) {
539               var pm = getPropertyMeta(propName);
540               if (pm != null) {
541                  var cm = pm.getBeanInfo();
542                  var needsConversion = !cm.inner().isInstance(rawVal)
543                     || (cm.isCollection() && !cm.getElementType().isObject())
544                     || (cm.isMap() && !cm.getValueType().is(Object.class));
545                  if (needsConversion)
546                     rawVal = session.convertToType(rawVal, cm);
547               }
548            }
549            args[i] = rawVal;
550         }
551         try {
552            bean = c.<T>newInstance(args);
553            propertyCache.forEach(this::put);
554            propertyCache = null;
555         } catch (IllegalArgumentException e) {
556            throw brex(e, meta.getBeanInfo().inner(), "IllegalArgumentException occurred on call to class constructor '%s' with argument types '%s'", c.getNameSimple(),
557               Arrays.toString(getClasses(args)));
558         } catch (Exception e) {
559            throw brex(e);
560         }
561      }
562      return bean;
563   }
564
565   /**
566    * Returns the bean session that created this bean map.
567    *
568    * <p>
569    * The returned value is the bean-modeling SPI seam.  Marshalling-side callers needing the concrete
570    * marshalling-session implementation can cast — every session wired in via
571    * {@link #setBeanSession(BeanSession)} on the marshalling-side path is currently a
572    * {@code MarshallingSession}.
573    *
574    * @return The bean session that created this bean map.
575    */
576   public final BeanSession getBeanSession() { return session; }
577
578   /**
579    * Returns the {@link BeanInfo} of the wrapped bean.
580    *
581    * @return The class type of the wrapped bean.
582    */
583   @Override /* Overridden from Delegate */
584   public BeanInfo<T> getBeanInfo() { return this.meta.getBeanInfo(); }
585
586   /**
587    * Returns the metadata associated with this bean map.
588    *
589    * @return The metadata associated with this bean map.
590    */
591   public BeanMeta<T> getMeta() { return meta; }
592
593   /**
594    * Extracts the specified field values from this bean and returns it as a simple Map.
595    *
596    * <p>
597    * The returned map is a <i>live</i> view over this bean map: each entry's {@code getValue} reads through to
598    * {@code BeanMap.get} and {@code setValue} writes through to {@code BeanMap.put}.  Unknown {@code fields}
599    * (keys not present in this bean map) are silently skipped.
600    *
601    * @param fields The fields to extract.
602    * @return
603    *    A new map with fields as key-value pairs.
604    *    <br>Note that modifying the values in this map will also modify the underlying bean.
605    */
606   @SuppressWarnings({
607      "java:S9391" // Hot path; avoids stream allocation.
608   })
609   public Map<String,Object> getProperties(String...fields) {
610      Map<String,Object> thisMap = this;
611      var entries = new ArrayList<Map.Entry<String,Object>>(fields.length);
612      for (var k : fields) {
613         if (thisMap.containsKey(k)) {
614            entries.add(new Map.Entry<>() {
615               @Override public String getKey() { return k; }
616               @Override public Object getValue() { return thisMap.get(k); }
617               @Override public Object setValue(Object v) { return thisMap.put(k, v); }
618            });
619         }
620      }
621      return new AbstractMap<>() {
622         @Override public Set<Map.Entry<String,Object>> entrySet() {
623            return new AbstractSet<>() {
624               @Override public Iterator<Map.Entry<String,Object>> iterator() { return entries.iterator(); }
625               @Override public int size() { return entries.size(); }
626            };
627         }
628      };
629   }
630
631   /**
632    * Returns the specified property on this bean map.
633    *
634    * <p>
635    * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using
636    * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods.
637    *
638    * <p>
639    * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method.
640    *
641    * @param propertyName The name of the property to look up.
642    * @return The bean property, or null if the bean has no such property.
643    */
644   public BeanMapEntry getProperty(String propertyName) {
645      var p = getPropertyMeta(propertyName);
646      if (p == null)
647         return null;
648      return new BeanMapEntry(this, p, propertyName);
649   }
650
651   /**
652    * Returns the metadata on the specified property.
653    *
654    * <p>
655    * Returns <jk>null</jk> if the property does not exist on the bean <em>or</em> if the property exists but is
656    * excluded by the current active view.  The latter allows the caller (e.g. {@link #put(String, Object)}) to
657    * route out-of-view input through the existing unknown/ignored-property path governed by
658    * {@link BeanConfigContext#isIgnoreUnknownBeanProperties()}.
659    *
660    * @param propertyName The name of the bean property.
661    * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist or is out of view.
662    */
663   public BeanPropertyMeta getPropertyMeta(String propertyName) {
664      var p = meta.getPropertyMeta(propertyName);
665      if (p != null && ! isInActiveView(p))
666         return null;
667      return p;
668   }
669
670   /**
671    * Same as {@link #get(Object)} except bypasses any {@code ObjectSwap} associated with the bean property or
672    * {@link BeanFilter} associated with the bean class.
673    *
674    * @param property The name of the property to get.
675    * @return The raw property value.
676    */
677   public Object getRaw(Object property) {
678      var pName = s(property);
679      var p = getPropertyMeta(pName);
680      if (p == null)
681         return null;
682      return p.getRaw(this, pName);
683   }
684
685   /**
686    * Returns the names of all properties associated with the bean.
687    *
688    * <p>
689    * The returned set is unmodifiable.
690    */
691   @Override /* Overridden from Map */
692   public Set<String> keySet() {
693      if (meta.getDynaProperty() == null)
694         return meta.getProperties().keySet();
695      Set<String> l = set();
696      meta.getProperties().forEach((k, v) -> {
697         if (neq(k, "*"))
698            l.add(k);
699      });
700      try {
701         l.addAll(meta.getDynaProperty().getDynaMap(bean).keySet());
702      } catch (Exception e) {
703         throw new BeanRuntimeException(e);
704      }
705      return l;
706   }
707
708   /**
709    * Convenience method for loading this map with the contents of the specified map.
710    *
711    * <p>
712    * Identical to {@link #putAll(Map)} except as a fluent-style method.
713    *
714    * @param entries The map containing the entries to add to this map.
715    * @return This object.
716    */
717   @SuppressWarnings({
718      "rawtypes" // Raw types necessary for generic type handling
719   })
720   public BeanMap<T> load(Map entries) {
721      putAll(entries);
722      return this;
723   }
724
725   /**
726    * Sets a property on the bean.
727    *
728    * <p>
729    * If there is a {@code ObjectSwap} associated with this bean property or bean property type class, then you must pass in a transformed value.
730    * For example, if the bean property type class is a {@link Date} and the bean property has the
731    * {@code DateFormat.ISO_INSTANT} setting associated with it through the
732    * {@code @Swap(value)} annotation, the value being passed in must be
733    * a String containing an ISO8601 date-time string value.
734    *
735    * <h5 class='section'>Example:</h5>
736    * <p class='bjava'>
737    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
738    *    Person <jv>person</jv> = <jk>new</jk> Person();
739    *
740    *    <jc>// Create a marshalling context and add the ISO8601 date-time swap</jc>
741    *    MarshallingContext <jv>marshallingContext</jv> = MarshallingContext.<jsm>create</jsm>().dateFormat(DateFormat.<jsf>ISO_INSTANT</jsf>).build();
742    *
743    *    <jc>// Wrap our bean in a bean map</jc>
744    *    BeanMap&lt;Person&gt; <jv>beanMap</jv> = <jv>marshallingContext</jv>.toBeanMap(<jv>person</jv>);
745    *
746    *    <jc>// Set the field</jc>
747    *    <jv>beanMap</jv>.put(<js>"birthDate"</js>, <js>"'1901-03-03T04:05:06-5000'"</js>);
748    * </p>
749    *
750    * @param property The name of the property to set.
751    * @param value The value to set the property to.  Can be <jk>null</jk> to set the property to <jk>null</jk>.
752    * @return
753    *    If the {@code beanMapPutReturnsOldValue} setting is <jk>true</jk> on the session's bean configuration,
754    *    then the old value of the property is returned.
755    *    Otherwise, this method always returns <jk>null</jk>.
756    * @throws
757    *    RuntimeException if any of the following occur.
758    *    <ul>
759    *       <li>BeanMapEntry does not exist on the underlying object.
760    *       <li>Security settings prevent access to the underlying object setter method.
761    *       <li>An exception occurred inside the setter method.
762    *    </ul>
763    */
764   @Override /* Overridden from Map */
765   public Object put(String property, Object value) {
766      var p = getPropertyMeta(property);
767      if (p == null) {
768         if (meta.getConfig().isIgnoreUnknownBeanProperties() || property.equals(typePropertyName))
769            return meta.onWriteProperty(bean, property, null);
770
771         p = getPropertyMeta("*");
772         if (p == null)
773            throw brex(meta.getClassInfo(), "Bean property '%s' not found.", property);
774      }
775      return p.set(this, property, value);
776   }
777
778   /**
779    * Given a string containing variables of the form <c>"{property}"</c>, replaces those variables with property
780    * values in this bean.
781    *
782    * @param s The string containing variables.
783    * @return A new string with variables replaced, or the same string if no variables were found.
784    */
785   public String resolveVars(String s) {
786      return formatNamed(s, this);
787   }
788
789   /**
790    * Returns a simple collection of properties for this bean map.
791    *
792    * @return A simple collection of properties for this bean map.
793    */
794   protected Collection<BeanPropertyMeta> getProperties() { return meta.getProperties().values(); }
795
796   void setBean(Object bean) { this.bean = (T)bean; }
797
798   /**
799    * Compares the specified object with this map for equality.
800    *
801    * <p>
802    * Returns <jk>true</jk> if the given object is also a map and the two maps represent the same
803    * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent the same mappings if
804    * <c>m1.entrySet().equals(m2.entrySet())</c>.
805    *
806    * <p>
807    * This implementation compares the entry sets of the two maps.
808    *
809    * @param o Object to be compared for equality with this map.
810    * @return <jk>true</jk> if the specified object is equal to this map.
811    */
812   @Override
813   public boolean equals(Object o) {
814      return o instanceof Map<?,?> o2 && eq(this, o2, (x, y) -> x.entrySet().equals(y.entrySet()));
815   }
816
817   /**
818    * Returns the hash code value for this map.
819    *
820    * <p>
821    * The hash code of a map is defined to be the sum of the hash codes of each entry in the map's
822    * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> implies that
823    * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and <c>m2</c>, as required
824    * by the general contract of {@link Object#hashCode()}.
825    *
826    * <p>
827    * This implementation computes the hash code from the entry set.
828    *
829    * @return The hash code value for this map.
830    */
831   @Override
832   public int hashCode() {
833      return entrySet().hashCode();
834   }
835}