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