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