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