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