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