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("IllegalArgumentException occurred on call to class constructor ''{0}'' with argument types ''{1}''", c.getName(), JsonSerializer.DEFAULT_LAX.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'>
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.ignoreUnknownBeanProperties)
224            return null;
225
226         if (property.equals(beanTypePropertyName))
227            return null;
228
229         throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property);
230      }
231      if (meta.beanFilter != null)
232         value = meta.beanFilter.writeProperty(this.bean, property, value);
233      return p.set(this, property, value);
234   }
235
236   /**
237    * Add a value to a collection or array property.
238    * 
239    * <p>
240    * As a general rule, adding to arrays is not recommended since the array must be recreate each time this method is
241    * called.
242    * 
243    * @param property Property name or child-element name (if {@link Xml#childName() @Xml.childName()} is specified).
244    * @param value The value to add to the collection or array.
245    */
246   public void add(String property, Object value) {
247      BeanPropertyMeta p = meta.properties.get(property);
248      if (p == null) {
249         if (meta.ctx.ignoreUnknownBeanProperties)
250            return;
251         throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property);
252      }
253      p.add(this, property, value);
254   }
255
256
257   /**
258    * Gets a property on the bean.
259    * 
260    * <p>
261    * If there is a {@link PojoSwap} associated with this bean property or bean property type class, then this method
262    * will return the transformed value.
263    * For example, if the bean property type class is a {@link Date} and the bean property has the
264    * {@link org.apache.juneau.transforms.DateSwap.ISO8601DT} swap associated with it through the
265    * {@link Swap#value() @Swap.value()} annotation, this method will return a String containing an
266    * ISO8601 date-time string value.
267    * 
268    * <h5 class='section'>Example:</h5>
269    * <p class='bcode'>
270    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
271    *    Person p = <jk>new</jk> Person();
272    *    p.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6));
273    * 
274    *    <jc>// Create a bean context and add the ISO8601 date-time swap</jc>
275    *    BeanContext beanContext = <jk>new</jk> BeanContext().pojoSwaps(DateSwap.ISO8601DT.<jk>class</jk>);
276    * 
277    *    <jc>// Wrap our bean in a bean map</jc>
278    *    BeanMap&lt;Person&gt; b = beanContext.forBean(p);
279    * 
280    *    <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc>
281    *    String s = myBeanMap.get(<js>"birthDate"</js>);
282    * </p>
283    * 
284    * @param property The name of the property to get.
285    * @throws
286    *    RuntimeException if any of the following occur.
287    *    <ol>
288    *       <li>BeanMapEntry does not exist on the underlying object.
289    *       <li>Security settings prevent access to the underlying object getter method.
290    *       <li>An exception occurred inside the getter method.
291    *    </ol>
292    */
293   @Override /* Map */
294   public Object get(Object property) {
295      String pName = asString(property);
296      BeanPropertyMeta p = getPropertyMeta(pName);
297      if (p == null)
298         return null;
299      if (meta.beanFilter != null)
300         return meta.beanFilter.readProperty(this.bean, pName, p.get(this, pName));
301      return p.get(this, pName);
302   }
303
304   /**
305    * Same as {@link #get(Object)} except bypasses the POJO filter associated with the bean property or bean filter
306    * associated with the bean class.
307    * 
308    * @param property The name of the property to get.
309    * @return The raw property value.
310    */
311   public Object getRaw(Object property) {
312      String pName = asString(property);
313      BeanPropertyMeta p = getPropertyMeta(pName);
314      if (p == null)
315         return null;
316      return p.getRaw(this, pName);
317   }
318
319   /**
320    * Convenience method for setting multiple property values by passing in JSON text.
321    * 
322    * <h5 class='section'>Example:</h5>
323    * <p class='bcode'>
324    *    aPersonBean.load(<js>"{name:'John Smith',age:21}"</js>)
325    * </p>
326    * 
327    * @param input The text that will get parsed into a map and then added to this map.
328    * @return This object (for method chaining).
329    * @throws ParseException If the input contains a syntax error or is malformed.
330    */
331   public BeanMap<T> load(String input) throws ParseException {
332      putAll(new ObjectMap(input));
333      return this;
334   }
335
336   /**
337    * Convenience method for setting multiple property values by passing in a reader.
338    * 
339    * @param r The text that will get parsed into a map and then added to this map.
340    * @param p The parser to use to parse the text.
341    * @return This object (for method chaining).
342    * @throws ParseException If the input contains a syntax error or is malformed.
343    * @throws IOException Thrown by <code>Reader</code>.
344    */
345   public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException {
346      putAll(new ObjectMap(r, p));
347      return this;
348   }
349
350   /**
351    * Convenience method for loading this map with the contents of the specified map.
352    * 
353    * <p>
354    * Identical to {@link #putAll(Map)} except as a fluent-style method.
355    * 
356    * @param entries The map containing the entries to add to this map.
357    * @return This object (for method chaining).
358    */
359   @SuppressWarnings({"unchecked","rawtypes"})
360   public BeanMap<T> load(Map entries) {
361      putAll(entries);
362      return this;
363   }
364
365   /**
366    * Returns the names of all properties associated with the bean.
367    * 
368    * <p>
369    * The returned set is unmodifiable.
370    */
371   @Override /* Map */
372   public Set<String> keySet() {
373      if (meta.dynaProperty == null)
374         return meta.properties.keySet();
375      Set<String> l = new LinkedHashSet<>();
376      for (String p : meta.properties.keySet())
377         if (! "*".equals(p))
378            l.add(p);
379      try {
380         l.addAll(meta.dynaProperty.getDynaMap(bean).keySet());
381      } catch (Exception e) {
382         throw new BeanRuntimeException(e);
383      }
384      return l;
385   }
386
387   /**
388    * Returns the specified property on this bean map.
389    * 
390    * <p>
391    * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using
392    * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods.
393    * 
394    * <p>
395    * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method.
396    * 
397    * @param propertyName The name of the property to look up.
398    * @return The bean property, or null if the bean has no such property.
399    */
400   public BeanMapEntry getProperty(String propertyName) {
401      BeanPropertyMeta p = getPropertyMeta(propertyName);
402      if (p == null)
403         return null;
404      return new BeanMapEntry(this, p, propertyName);
405   }
406
407   /**
408    * Returns the metadata on the specified property.
409    * 
410    * @param propertyName The name of the bean property.
411    * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist.
412    */
413   public BeanPropertyMeta getPropertyMeta(String propertyName) {
414      BeanPropertyMeta bpMeta = meta.properties.get(propertyName);
415      if (bpMeta == null)
416         bpMeta = meta.dynaProperty;
417      return bpMeta;
418   }
419
420   /**
421    * Returns the {@link ClassMeta} of the wrapped bean.
422    * 
423    * @return The class type of the wrapped bean.
424    */
425   @Override /* Delegate */
426   public ClassMeta<T> getClassMeta() {
427      return this.meta.getClassMeta();
428   }
429
430   /**
431    * Invokes all the getters on this bean and return the values as a list of {@link BeanPropertyValue} objects.
432    * 
433    * <p>
434    * This allows a snapshot of all values to be grabbed from a bean in one call.
435    * 
436    * @param ignoreNulls
437    *    Don't return properties whose values are null.
438    * @param prependVals
439    *    Additional bean property values to prepended to this list.
440    *    Any <jk>null</jk> values in this list will be ignored.
441    * @return The list of all bean property values.
442    */
443   public List<BeanPropertyValue> getValues(final boolean ignoreNulls, BeanPropertyValue...prependVals) {
444      Collection<BeanPropertyMeta> properties = getProperties();
445      int capacity = (ignoreNulls && properties.size() > 10) ? 10 : properties.size() + prependVals.length;
446      List<BeanPropertyValue> l = new ArrayList<>(capacity);
447      for (BeanPropertyValue v : prependVals)
448         if (v != null)
449            l.add(v);
450      for (BeanPropertyMeta bpm : properties) {
451         if (bpm.canRead()) {
452            try {
453               if (bpm.isDyna()) {
454                  for (String pName : bpm.getDynaMap(bean).keySet()) {
455                     Object val = bpm.get(this, pName);
456                     if (val != null || ! ignoreNulls)
457                        l.add(new BeanPropertyValue(bpm, pName, val, null));
458                  }
459               } else {
460                  Object val = bpm.get(this, null);
461                  if (val != null || ! ignoreNulls)
462                     l.add(new BeanPropertyValue(bpm, bpm.getName(), val, null));
463               }
464            } catch (Error e) {
465               // Errors should always be uncaught.
466               throw e;
467            } catch (Throwable t) {
468               l.add(new BeanPropertyValue(bpm, bpm.getName(), null, t));
469            }
470         }
471      }
472      if (meta.sortProperties && meta.dynaProperty != null)
473         Collections.sort(l);
474      return l;
475   }
476
477   /**
478    * Given a string containing variables of the form <code>"{property}"</code>, replaces those variables with property
479    * values in this bean.
480    * 
481    * @param s The string containing variables.
482    * @return A new string with variables replaced, or the same string if no variables were found.
483    */
484   public String resolveVars(String s) {
485      return StringUtils.replaceVars(s, this);
486   }
487
488   /**
489    * Returns a simple collection of properties for this bean map.
490    * 
491    * @return A simple collection of properties for this bean map.
492    */
493   protected Collection<BeanPropertyMeta> getProperties() {
494      return meta.properties.values();
495   }
496
497   /**
498    * Returns all the properties associated with the bean.
499    * 
500    * @return A new set.
501    */
502   @Override
503   public Set<Entry<String,Object>> entrySet() {
504
505      // If this bean has a dyna-property, then we need to construct the entire set before returning.
506      // Otherwise, we can create an iterator without a new data structure.
507      if (meta.dynaProperty != null) {
508         Set<Entry<String,Object>> s = new LinkedHashSet<>();
509         for (BeanPropertyMeta pMeta : getProperties()) {
510            if (pMeta.isDyna()) {
511               try {
512                  for (Map.Entry<String,Object> e : pMeta.getDynaMap(bean).entrySet())
513                     s.add(new BeanMapEntry(this, pMeta, e.getKey()));
514               } catch (Exception e) {
515                  throw new BeanRuntimeException(e);
516               }
517            } else {
518               s.add(new BeanMapEntry(this, pMeta, pMeta.getName()));
519            }
520         }
521         return s;
522      }
523
524      // Construct our own anonymous set to implement this function.
525      Set<Entry<String,Object>> s = new AbstractSet<Entry<String,Object>>() {
526
527         // Get the list of properties from the meta object.
528         // Note that the HashMap.values() method caches results, so this collection
529         // will really only be constructed once per bean type since the underlying
530         // map never changes.
531         final Collection<BeanPropertyMeta> pSet = getProperties();
532
533         @Override /* Set */
534         public Iterator<java.util.Map.Entry<String, Object>> iterator() {
535
536            // Construct our own anonymous iterator that uses iterators against the meta.properties
537            // map to maintain position.  This prevents us from having to construct any of our own
538            // collection objects.
539            return new Iterator<Entry<String,Object>>() {
540
541               final Iterator<BeanPropertyMeta> pIterator = pSet.iterator();
542
543               @Override /* Iterator */
544               public boolean hasNext() {
545                  return pIterator.hasNext();
546               }
547
548               @Override /* Iterator */
549               public Map.Entry<String, Object> next() {
550                  return new BeanMapEntry(BeanMap.this, pIterator.next(), null);
551               }
552
553               @Override /* Iterator */
554               public void remove() {
555                  throw new UnsupportedOperationException("Cannot remove item from iterator.");
556               }
557            };
558         }
559
560         @Override /* Set */
561         public int size() {
562            return pSet.size();
563         }
564      };
565
566      return s;
567   }
568
569   @SuppressWarnings("unchecked")
570   void setBean(Object bean) {
571      this.bean = (T)bean;
572   }
573}