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.commons.utils.ClassUtils.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
024
025import java.io.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.juneau.annotation.*;
030import org.apache.juneau.collections.*;
031import org.apache.juneau.commons.reflect.*;
032import org.apache.juneau.internal.*;
033import org.apache.juneau.json.*;
034import org.apache.juneau.parser.*;
035import org.apache.juneau.swap.*;
036
037/**
038 * Java bean wrapper class.
039 *
040 * <h5 class='topic'>Description</h5>
041 *
042 * A wrapper that wraps Java bean instances inside of a {@link Map} interface that allows properties on the wrapped
043 * object can be accessed using the {@link Map#get(Object) get()} and {@link Map#put(Object,Object) put()} methods.
044 *
045 * <p>
046 * Use the {@link BeanContext} class to create instances of this class.
047 *
048 * <h5 class='topic'>Bean property order</h5>
049 *
050 * The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()}
051 * methods are as follows:
052 * <ul class='spaced-list'>
053 *    <li>
054 *       If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties
055 *       in the annotation.
056 *    <li>
057 *       If {@link Bean @Bean} annotation is not specified on the class, then the order is the same as that returned
058 *       by the {@link java.beans.BeanInfo} class (i.e. ordered by definition in the class).
059 * </ul>
060 *
061 * <h5 class='topic'>POJO swaps</h5>
062 *
063 * If {@link ObjectSwap ObjectSwaps} are defined on the class types of the properties of this bean or the bean properties
064 * themselves, the {@link #get(Object)} and {@link #put(String, Object)} methods will automatically transform the
065 * property value to and from the serialized form.
066 *
067 *
068 * @param <T> Specifies the type of object that this map encapsulates.
069 */
070public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T> {
071
072   /**
073    * Convenience method for wrapping a bean inside a {@link BeanMap}.
074    *
075    * @param <T> The bean type.
076    * @param bean The bean being wrapped.
077    * @return A new {@link BeanMap} instance wrapping the bean.
078    */
079   public static <T> BeanMap<T> of(T bean) {
080      return BeanContext.DEFAULT_SESSION.toBeanMap(bean);
081   }
082
083   /** The wrapped object. */
084   protected T bean;
085
086   /** Temporary holding cache for beans with read-only properties.  Normally null. */
087   protected Map<String,Object> propertyCache;
088
089   /** Temporary holding cache for bean properties of array types when the add() method is being used. */
090   protected Map<String,List<?>> arrayPropertyCache;
091
092   /** The BeanMeta associated with the class of the object. */
093   protected BeanMeta<T> meta;
094   private final BeanSession session;
095
096   private final String typePropertyName;
097
098   /**
099    * Instance of this class are instantiated through the BeanContext class.
100    *
101    * @param session The bean session object that created this bean map.
102    * @param bean The bean to wrap inside this map.
103    * @param meta The metadata associated with the bean class.
104    */
105   protected BeanMap(BeanSession session, T bean, BeanMeta<T> meta) {
106      this.session = session;
107      this.bean = bean;
108      this.meta = meta;
109      if (ne(meta.getConstructorArgs()))
110         propertyCache = new TreeMap<>();
111      this.typePropertyName = session.getBeanTypePropertyName(meta.getClassMeta());
112   }
113
114   /**
115    * Add a value to a collection or array property.
116    *
117    * <p>
118    * As a general rule, adding to arrays is not recommended since the array must be recreate each time this method is
119    * called.
120    *
121    * @param property Property name or child-element name (if {@link org.apache.juneau.xml.annotation.Xml#childName() @Xml(childName)} is specified).
122    * @param value The value to add to the collection or array.
123    */
124   public void add(String property, Object value) {
125      var p = getPropertyMeta(property);
126      if (p == null) {
127         if (meta.getBeanContext().isIgnoreUnknownBeanProperties())
128            return;
129         throw bex(meta.getClassMeta(), "Bean property ''{0}'' not found.", property);
130      }
131      p.add(this, property, value);
132   }
133
134   @Override /* Overridden from Map */
135   public boolean containsKey(Object property) {
136      // JUNEAU-248: Match the behavior of keySet() - only check properties map, not hiddenProperties
137      var key = emptyIfNull(property);
138      if (meta.getProperties().containsKey(key) && ! "*".equals(key))
139         return true;
140      if (nn(meta.getDynaProperty())) {
141         try {
142            return meta.getDynaProperty().getDynaMap(bean).containsKey(key);
143         } catch (Exception e) {
144            throw bex(e);
145         }
146      }
147      return false;
148   }
149
150   /**
151    * Returns all the properties associated with the bean.
152    *
153    * @return A new set.
154    */
155   @Override
156   public Set<Entry<String,Object>> entrySet() {
157
158      // If this bean has a dyna-property, then we need to construct the entire set before returning.
159      // Otherwise, we can create an iterator without a new data structure.
160      if (nn(meta.getDynaProperty())) {
161         Set<Entry<String,Object>> s = set();
162         forEachProperty(x -> true, x -> {
163            if (x.isDyna()) {
164               try {
165                  x.getDynaMap(bean).entrySet().forEach(y -> s.add(new BeanMapEntry(this, x, y.getKey())));
166               } catch (Exception e) {
167                  throw bex(e);
168               }
169            } else {
170               s.add(new BeanMapEntry(this, x, x.getName()));
171            }
172         });
173         return s;
174      }
175
176      // Construct our own anonymous set to implement this function.
177      var s = new AbstractSet<Entry<String,Object>>() {
178
179         // Get the list of properties from the meta object.
180         // Note that the HashMap.values() method caches results, so this collection
181         // will really only be constructed once per bean type since the underlying
182         // map never changes.
183         final Collection<BeanPropertyMeta> pSet = getProperties();
184
185         @Override /* Overridden from Set */
186         public Iterator<java.util.Map.Entry<String,Object>> iterator() {
187
188            // Construct our own anonymous iterator that uses iterators against the meta.getProperties()
189            // map to maintain position.  This prevents us from having to construct any of our own
190            // collection objects.
191            return new Iterator<>() {
192
193               final Iterator<BeanPropertyMeta> pIterator = pSet.iterator();
194
195               @Override /* Overridden from Iterator */
196               public boolean hasNext() {
197                  return pIterator.hasNext();
198               }
199
200               @Override /* Overridden from Iterator */
201               public Map.Entry<String,Object> next() {
202                  return new BeanMapEntry(BeanMap.this, pIterator.next(), null);
203               }
204
205               @Override /* Overridden from Iterator */
206               public void remove() {
207                  throw unsupportedOp("Cannot remove item from iterator.");
208               }
209            };
210         }
211
212         @Override /* Overridden from Set */
213         public int size() {
214            return pSet.size();
215         }
216      };
217
218      return s;
219   }
220
221   /**
222    * Performs an action on each property in this bean map.
223    *
224    * @param filter The filter to apply to properties.
225    * @param action The action.
226    * @return This object.
227    */
228   public BeanMap<T> forEachProperty(Predicate<BeanPropertyMeta> filter, Consumer<BeanPropertyMeta> action) {
229      meta.getProperties().values().stream().filter(filter).forEach(action);
230//    for (var bpm : meta.propertyArray)
231//       if (filter.test(bpm))
232//          action.accept(bpm);
233      return this;
234   }
235
236   /**
237    * Invokes all the getters on this bean and consumes the results.
238    *
239    * @param valueFilter Filter to apply to value before applying action.
240    * @param action The action to perform.
241    * @return The list of all bean property values.
242    */
243   public BeanMap<T> forEachValue(Predicate<Object> valueFilter, BeanPropertyConsumer action) {
244
245      // Normal bean.
246      if (meta.getDynaProperty() == null) {
247         forEachProperty(BeanPropertyMeta::canRead, bpm -> {
248            try {
249               var val = bpm.get(this, null);
250               if (valueFilter.test(val))
251                  action.apply(bpm, bpm.getName(), val, null);
252            } catch (Error e) {
253               // Errors should always be uncaught.
254               throw e;
255            } catch (Throwable t) {
256               action.apply(bpm, bpm.getName(), null, t);
257            }
258         });
259
260         // Bean with dyna properties.
261      } else {
262         Map<String,BeanPropertyValue> actions = (meta.isSortProperties() ? sortedMap() : map());
263
264         forEachProperty(x -> ! x.isDyna(), bpm -> {
265            try {
266               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), bpm.get(this, null), null));
267            } catch (Error e) {
268               // Errors should always be uncaught.
269               throw e;
270            } catch (Throwable t) {
271               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), null, t));
272            }
273         });
274
275         forEachProperty(BeanPropertyMeta::isDyna, bpm -> {
276            try {
277               // TODO - This is kind of inefficient.
278               Map<String,Object> dynaMap = bpm.getDynaMap(bean);
279               if (nn(dynaMap)) {
280                  dynaMap.forEach((k, v) -> {
281                     var val = bpm.get(this, k);
282                     actions.put(k, new BeanPropertyValue(bpm, k, val, null));
283                  });
284               }
285            } catch (Exception e) {
286               e.printStackTrace();
287            }
288         });
289
290         actions.forEach((k, v) -> {
291            if (valueFilter.test(v.getValue()))
292               action.apply(v.getMeta(), v.getName(), v.getValue(), v.getThrown());
293         });
294      }
295
296      return this;
297   }
298
299   /**
300    * Gets a property on the bean.
301    *
302    * <p>
303    * If there is a {@link ObjectSwap} associated with this bean property or bean property type class, then this method
304    * will return the transformed value.
305    * For example, if the bean property type class is a {@link Date} and the bean property has the
306    * {@link org.apache.juneau.swaps.TemporalDateSwap.IsoInstant} swap associated with it through the
307    * {@link Swap#value() @Swap(value)} annotation, this method will return a String containing an
308    * ISO8601 date-time string value.
309    *
310    * <h5 class='section'>Example:</h5>
311    * <p class='bjava'>
312    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
313    *    Person <jv>person</jv> = <jk>new</jk> Person();
314    *    <jv>person</jv>.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6));
315    *
316    *    <jc>// Create a bean context and add the ISO8601 date-time swap</jc>
317    *    BeanContext <jv>beanContext</jv> = BeanContext.<jsm>create</jsm>().swaps(DateSwap.ISO8601DT.<jk>class</jk>).build();
318    *
319    *    <jc>// Wrap our bean in a bean map</jc>
320    *    BeanMap&lt;Person&gt; <jv>beanMap</jv> = <jv>beanContext</jv>.toBeanMap(<jv>person</jv>);
321    *
322    *    <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc>
323    *    String <jv>birthDate</jv> = <jv>beanMap</jv>.get(<js>"birthDate"</js>);
324    * </p>
325    *
326    * @param property The name of the property to get.
327    * @return The property value.
328    * @throws RuntimeException if any of the following occur.
329    *    <ol>
330    *       <li>BeanMapEntry does not exist on the underlying object.
331    *       <li>Security settings prevent access to the underlying object getter method.
332    *       <li>An exception occurred inside the getter method.
333    *    </ol>
334    */
335   @Override /* Overridden from Map */
336   public Object get(Object property) {
337      var pName = s(property);
338      var p = getPropertyMeta(pName);
339      if (p == null)
340         return meta.onReadProperty(this.bean, pName, null);
341      return p.get(this, pName);
342   }
343
344   /**
345    * Same as {@link #get(Object)} but casts the value to the specific type.
346    *
347    * @param <T2> The type to cast to.
348    * @param property The name of the property to get.
349    * @param c The type to cast to.
350    * @return The property value.
351    * @throws RuntimeException if any of the following occur.
352    *    <ol>
353    *       <li>BeanMapEntry does not exist on the underlying object.
354    *       <li>Security settings prevent access to the underlying object getter method.
355    *       <li>An exception occurred inside the getter method.
356    *    </ol>
357    * @throws ClassCastException if property is not the specified type.
358    */
359   @SuppressWarnings("unchecked")
360   public <T2> T2 get(String property, Class<T2> c) {
361      var pName = s(property);
362      var p = getPropertyMeta(pName);
363      if (p == null)
364         return (T2)meta.onReadProperty(this.bean, pName, null);
365      return (T2)p.get(this, pName);
366   }
367
368   /**
369    * Returns the wrapped bean object.
370    *
371    * <p>
372    * Triggers bean creation if bean has read-only properties set through a constructor defined by the
373    * {@link Beanc @Beanc} annotation.
374    *
375    * @return The inner bean object.
376    */
377   public T getBean() {
378      T b = getBean(true);
379
380      // If we have any arrays that need to be constructed, do it now.
381      if (nn(arrayPropertyCache)) {
382         arrayPropertyCache.forEach((k, v) -> {
383            try {
384               getPropertyMeta(k).setArray(b, v);
385            } catch (Exception e1) {
386               throw toRex(e1);
387            }
388         });
389         arrayPropertyCache = null;
390      }
391
392      // Initialize any null Optional<X> fields.
393      meta.getProperties().forEach((k,v) -> {
394         var cm = v.getClassMeta();
395         if (cm.isOptional() && v.get(this, k) == null)
396            v.set(this, k, cm.getOptionalDefault());
397      });
398
399      // Do the same for hidden fields.
400      meta.getHiddenProperties().forEach((k, v) -> {
401         var cm = v.getClassMeta();
402         if (cm.isOptional() && v.get(this, k) == null)
403            v.set(this, k, cm.getOptionalDefault());
404      });
405
406      return b;
407   }
408
409   /**
410    * Returns the wrapped bean object.
411    *
412    * <p>
413    * If <c>create</c> is <jk>false</jk>, then this method may return <jk>null</jk> if the bean has read-only
414    * properties set through a constructor defined by the {@link Beanc @Beanc} annotation.
415    *
416    * <p>
417    * This method does NOT always return the bean in it's final state.
418    * Array properties temporary stored as ArrayLists are not finalized until the {@link #getBean()} method is called.
419    *
420    * @param create If bean hasn't been instantiated yet, then instantiate it.
421    * @return The inner bean object.
422    */
423   public T getBean(boolean create) {
424      /** If this is a read-only bean, then we need to create it. */
425      if (bean == null && create && ne(meta.getConstructorArgs())) {
426         var props = meta.getConstructorArgs();
427         var c = meta.getConstructor();
428         var args = new Object[props.size()];
429         for (var i = 0; i < props.size(); i++)
430            args[i] = propertyCache.remove(props.get(i));
431         try {
432            bean = c.<T>newInstance(args);
433            propertyCache.forEach((k, v) -> put(k, v));
434            propertyCache = null;
435         } catch (IllegalArgumentException e) {
436            throw bex(e, meta.getClassMeta().inner(), "IllegalArgumentException occurred on call to class constructor ''{0}'' with argument types ''{1}''", c.getSimpleName(),
437               Json5Serializer.DEFAULT.toString(getClasses(args)));
438         } catch (Exception e) {
439            throw bex(e);
440         }
441      }
442      return bean;
443   }
444
445   /**
446    * Returns the bean session that created this bean map.
447    *
448    * @return The bean session that created this bean map.
449    */
450   public final BeanSession getBeanSession() { return session; }
451
452   /**
453    * Returns the {@link ClassMeta} of the wrapped bean.
454    *
455    * @return The class type of the wrapped bean.
456    */
457   @Override /* Overridden from Delegate */
458   public ClassMeta<T> getClassMeta() { return this.meta.getClassMeta(); }
459
460   /**
461    * Returns the metadata associated with this bean map.
462    *
463    * @return The metadata associated with this bean map.
464    */
465   public BeanMeta<T> getMeta() { return meta; }
466
467   /**
468    * Extracts the specified field values from this bean and returns it as a simple Map.
469    *
470    * @param fields The fields to extract.
471    * @return
472    *    A new map with fields as key-value pairs.
473    *    <br>Note that modifying the values in this map will also modify the underlying bean.
474    */
475   public Map<String,Object> getProperties(String...fields) {
476      return new FilteredKeyMap<>(null, this, fields);
477   }
478
479   /**
480    * Returns the specified property on this bean map.
481    *
482    * <p>
483    * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using
484    * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods.
485    *
486    * <p>
487    * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method.
488    *
489    * @param propertyName The name of the property to look up.
490    * @return The bean property, or null if the bean has no such property.
491    */
492   public BeanMapEntry getProperty(String propertyName) {
493      var p = getPropertyMeta(propertyName);
494      if (p == null)
495         return null;
496      return new BeanMapEntry(this, p, propertyName);
497   }
498
499   /**
500    * Returns the metadata on the specified property.
501    *
502    * @param propertyName The name of the bean property.
503    * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist.
504    */
505   public BeanPropertyMeta getPropertyMeta(String propertyName) {
506      return meta.getPropertyMeta(propertyName);
507   }
508
509   /**
510    * Same as {@link #get(Object)} except bypasses the POJO filter associated with the bean property or bean filter
511    * associated with the bean class.
512    *
513    * @param property The name of the property to get.
514    * @return The raw property value.
515    */
516   public Object getRaw(Object property) {
517      var pName = s(property);
518      var p = getPropertyMeta(pName);
519      if (p == null)
520         return null;
521      return p.getRaw(this, pName);
522   }
523
524   /**
525    * Returns the names of all properties associated with the bean.
526    *
527    * <p>
528    * The returned set is unmodifiable.
529    */
530   @Override /* Overridden from Map */
531   public Set<String> keySet() {
532      if (meta.getDynaProperty() == null)
533         return meta.getProperties().keySet();
534      Set<String> l = set();
535      meta.getProperties().forEach((k, v) -> {
536         if (! "*".equals(k))
537            l.add(k);
538      });
539      try {
540         l.addAll(meta.getDynaProperty().getDynaMap(bean).keySet());
541      } catch (Exception e) {
542         throw new BeanRuntimeException(e);
543      }
544      return l;
545   }
546
547   /**
548    * Convenience method for loading this map with the contents of the specified map.
549    *
550    * <p>
551    * Identical to {@link #putAll(Map)} except as a fluent-style method.
552    *
553    * @param entries The map containing the entries to add to this map.
554    * @return This object.
555    */
556   @SuppressWarnings({ "unchecked", "rawtypes" })
557   public BeanMap<T> load(Map entries) {
558      putAll(entries);
559      return this;
560   }
561
562   /**
563    * Convenience method for setting multiple property values by passing in a reader.
564    *
565    * @param r The text that will get parsed into a map and then added to this map.
566    * @param p The parser to use to parse the text.
567    * @return This object.
568    * @throws ParseException Malformed input encountered.
569    * @throws IOException Thrown by <c>Reader</c>.
570    */
571   public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException {
572      putAll(JsonMap.ofText(r, p));
573      return this;
574   }
575
576   /**
577    * Convenience method for setting multiple property values by passing in JSON text.
578    *
579    * <h5 class='section'>Example:</h5>
580    * <p class='bjava'>
581    *    <jv>beanMap</jv>.load(<js>"{name:'John Smith',age:21}"</js>)
582    * </p>
583    *
584    * @param input The text that will get parsed into a map and then added to this map.
585    * @return This object.
586    * @throws ParseException Malformed input encountered.
587    */
588   public BeanMap<T> load(String input) throws ParseException {
589      putAll(JsonMap.ofJson(input));
590      return this;
591   }
592
593   /**
594    * Sets a property on the bean.
595    *
596    * <p>
597    * If there is a {@link ObjectSwap} associated with this bean property or bean property type class, then you must pass
598    * in a transformed value.
599    * For example, if the bean property type class is a {@link Date} and the bean property has the
600    * {@link org.apache.juneau.swaps.TemporalDateSwap.IsoInstant} swap associated with it through the
601    * {@link Swap#value() @Swap(value)} annotation, the value being passed in must be
602    * a String containing an ISO8601 date-time string value.
603    *
604    * <h5 class='section'>Example:</h5>
605    * <p class='bjava'>
606    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
607    *    Person <jv>person</jv> = <jk>new</jk> Person();
608    *
609    *    <jc>// Create a bean context and add the ISO8601 date-time swap</jc>
610    *    BeanContext <jv>beanContext</jv> = BeanContext.<jsm>create</jsm>().swaps(DateSwap.ISO8601DT.<jk>class</jk>).build();
611    *
612    *    <jc>// Wrap our bean in a bean map</jc>
613    *    BeanMap&lt;Person&gt; <jv>beanMap</jv> = <jv>beanContext</jv>.toBeanMap(<jv>person</jv>);
614    *
615    *    <jc>// Set the field</jc>
616    *    <jv>beanMap</jv>.put(<js>"birthDate"</js>, <js>"'1901-03-03T04:05:06-5000'"</js>);
617    * </p>
618    *
619    * @param property The name of the property to set.
620    * @param value The value to set the property to.
621    * @return
622    *    If the bean context setting {@code beanMapPutReturnsOldValue} is <jk>true</jk>, then the old value of the
623    *    property is returned.
624    *    Otherwise, this method always returns <jk>null</jk>.
625    * @throws
626    *    RuntimeException if any of the following occur.
627    *    <ul>
628    *       <li>BeanMapEntry does not exist on the underlying object.
629    *       <li>Security settings prevent access to the underlying object setter method.
630    *       <li>An exception occurred inside the setter method.
631    *    </ul>
632    */
633   @Override /* Overridden from Map */
634   public Object put(String property, Object value) {
635      var p = getPropertyMeta(property);
636      if (p == null) {
637         if (meta.getBeanContext().isIgnoreUnknownBeanProperties() || property.equals(typePropertyName))
638            return meta.onWriteProperty(bean, property, null);
639
640         p = getPropertyMeta("*");
641         if (p == null)
642            throw bex(meta.getClassMeta(), "Bean property ''{0}'' not found.", property);
643      }
644      return p.set(this, property, value);
645   }
646
647   /**
648    * Given a string containing variables of the form <c>"{property}"</c>, replaces those variables with property
649    * values in this bean.
650    *
651    * @param s The string containing variables.
652    * @return A new string with variables replaced, or the same string if no variables were found.
653    */
654   public String resolveVars(String s) {
655      return formatNamed(s, this);
656   }
657
658   /**
659    * Returns a simple collection of properties for this bean map.
660    *
661    * @return A simple collection of properties for this bean map.
662    */
663   protected Collection<BeanPropertyMeta> getProperties() { return meta.getProperties().values(); }
664
665   @SuppressWarnings("unchecked")
666   void setBean(Object bean) { this.bean = (T)bean; }
667}