001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau;
018
019import static org.apache.juneau.common.utils.ThrowableUtils.*;
020import static org.apache.juneau.common.utils.Utils.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.function.*;
025
026import org.apache.juneau.annotation.*;
027import org.apache.juneau.collections.*;
028import org.apache.juneau.common.utils.*;
029import org.apache.juneau.internal.*;
030import org.apache.juneau.json.*;
031import org.apache.juneau.parser.*;
032import org.apache.juneau.reflect.*;
033import org.apache.juneau.swap.*;
034
035/**
036 * Java bean wrapper class.
037 *
038 * <h5 class='topic'>Description</h5>
039 *
040 * A wrapper that wraps Java bean instances inside of a {@link Map} interface that allows properties on the wrapped
041 * object can be accessed using the {@link Map#get(Object) get()} and {@link Map#put(Object,Object) put()} methods.
042 *
043 * <p>
044 * Use the {@link BeanContext} class to create instances of this class.
045 *
046 * <h5 class='topic'>Bean property order</h5>
047 *
048 * The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()}
049 * methods are as follows:
050 * <ul class='spaced-list'>
051 *    <li>
052 *       If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties
053 *       in the annotation.
054 *    <li>
055 *       If {@link Bean @Bean} annotation is not specified on the class, then the order is the same as that returned
056 *       by the {@link java.beans.BeanInfo} class (i.e. ordered by definition in the class).
057 * </ul>
058 *
059 * <h5 class='topic'>POJO swaps</h5>
060 *
061 * If {@link ObjectSwap ObjectSwaps} are defined on the class types of the properties of this bean or the bean properties
062 * themselves, the {@link #get(Object)} and {@link #put(String, Object)} methods will automatically transform the
063 * property value to and from the serialized form.
064 *
065 * <h5 class='section'>See Also:</h5><ul>
066
067 * </ul>
068 *
069 * @param <T> Specifies the type of object that this map encapsulates.
070 */
071public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T> {
072
073   /** The wrapped object. */
074   protected T bean;
075
076   /** Temporary holding cache for beans with read-only properties.  Normally null. */
077   protected Map<String,Object> propertyCache;
078
079   /** Temporary holding cache for bean properties of array types when the add() method is being used. */
080   protected Map<String,List<?>> arrayPropertyCache;
081
082   /** The BeanMeta associated with the class of the object. */
083   protected BeanMeta<T> meta;
084
085   private final BeanSession session;
086   private final String typePropertyName;
087
088   /**
089    * Convenience method for wrapping a bean inside a {@link BeanMap}.
090    *
091    * @param <T> The bean type.
092    * @param bean The bean being wrapped.
093    * @return A new {@link BeanMap} instance wrapping the bean.
094    */
095   public static <T> BeanMap<T> of(T bean) {
096      return BeanContext.DEFAULT_SESSION.toBeanMap(bean);
097   }
098
099   /**
100    * Instance of this class are instantiated through the BeanContext class.
101    *
102    * @param session The bean session object that created this bean map.
103    * @param bean The bean to wrap inside this map.
104    * @param meta The metadata associated with the bean class.
105    */
106   protected BeanMap(BeanSession session, T bean, BeanMeta<T> meta) {
107      this.session = session;
108      this.bean = bean;
109      this.meta = meta;
110      if (meta.constructorArgs.length > 0)
111         propertyCache = new TreeMap<>();
112      this.typePropertyName = session.getBeanTypePropertyName(meta.classMeta);
113   }
114
115   /**
116    * Returns the metadata associated with this bean map.
117    *
118    * @return The metadata associated with this bean map.
119    */
120   public BeanMeta<T> getMeta() {
121      return meta;
122   }
123
124   /**
125    * Returns the bean session that created this bean map.
126    *
127    * @return The bean session that created this bean map.
128    */
129   public final BeanSession getBeanSession() {
130      return session;
131   }
132
133   /**
134    * Returns the wrapped bean object.
135    *
136    * <p>
137    * Triggers bean creation if bean has read-only properties set through a constructor defined by the
138    * {@link Beanc @Beanc} annotation.
139    *
140    * @return The inner bean object.
141    */
142   public T getBean() {
143      T b = getBean(true);
144
145      // If we have any arrays that need to be constructed, do it now.
146      if (arrayPropertyCache != null) {
147         arrayPropertyCache.forEach((k,v) -> {
148            try {
149                getPropertyMeta(k).setArray(b, v);
150            } catch (Exception e1) {
151               throw asRuntimeException(e1);
152            }
153         });
154         arrayPropertyCache = null;
155      }
156
157      // Initialize any null Optional<X> fields.
158      for (BeanPropertyMeta pMeta : this.meta.propertyArray) {
159         ClassMeta<?> cm = pMeta.getClassMeta();
160         if (cm.isOptional() && pMeta.get(this, pMeta.getName()) == null)
161            pMeta.set(this, pMeta.getName(), cm.getOptionalDefault());
162      }
163      // Do the same for hidden fields.
164      this.meta.hiddenProperties.forEach((k,v) -> {
165         ClassMeta<?> cm = v.getClassMeta();
166         if (cm.isOptional() && v.get(this, v.getName()) == null)
167            v.set(this, v.getName(), cm.getOptionalDefault());
168      });
169
170      return b;
171   }
172
173   /**
174    * Returns the wrapped bean object.
175    *
176    * <p>
177    * If <c>create</c> is <jk>false</jk>, then this method may return <jk>null</jk> if the bean has read-only
178    * properties set through a constructor defined by the {@link Beanc @Beanc} annotation.
179    *
180    * <p>
181    * This method does NOT always return the bean in it's final state.
182    * Array properties temporary stored as ArrayLists are not finalized until the {@link #getBean()} method is called.
183    *
184    * @param create If bean hasn't been instantiated yet, then instantiate it.
185    * @return The inner bean object.
186    */
187   public T getBean(boolean create) {
188      /** If this is a read-only bean, then we need to create it. */
189      if (bean == null && create && meta.constructorArgs.length > 0) {
190         String[] props = meta.constructorArgs;
191         ConstructorInfo c = meta.constructor;
192         Object[] args = new Object[props.length];
193         for (int i = 0; i < props.length; i++)
194            args[i] = propertyCache.remove(props[i]);
195         try {
196            bean = c.<T>invoke(args);
197            propertyCache.forEach((k,v) -> put(k, v));
198            propertyCache = null;
199         } catch (IllegalArgumentException e) {
200            throw new BeanRuntimeException(e, meta.classMeta.innerClass, "IllegalArgumentException occurred on call to class constructor ''{0}'' with argument types ''{1}''", c.getSimpleName(), Json5Serializer.DEFAULT.toString(ClassUtils.getClasses(args)));
201         } catch (Exception e) {
202            throw new BeanRuntimeException(e);
203         }
204      }
205      return bean;
206   }
207
208   /**
209    * Sets a property on the bean.
210    *
211    * <p>
212    * If there is a {@link ObjectSwap} associated with this bean property or bean property type class, then you must pass
213    * in a transformed value.
214    * For example, if the bean property type class is a {@link Date} and the bean property has the
215    * {@link org.apache.juneau.swaps.TemporalDateSwap.IsoInstant} swap associated with it through the
216    * {@link Swap#value() @Swap(value)} annotation, the value being passed in must be
217    * a String containing an ISO8601 date-time string value.
218    *
219    * <h5 class='section'>Example:</h5>
220    * <p class='bjava'>
221    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
222    *    Person <jv>person</jv> = <jk>new</jk> Person();
223    *
224    *    <jc>// Create a bean context and add the ISO8601 date-time swap</jc>
225    *    BeanContext <jv>beanContext</jv> = BeanContext.<jsm>create</jsm>().swaps(DateSwap.ISO8601DT.<jk>class</jk>).build();
226    *
227    *    <jc>// Wrap our bean in a bean map</jc>
228    *    BeanMap&lt;Person&gt; <jv>beanMap</jv> = <jv>beanContext</jv>.toBeanMap(<jv>person</jv>);
229    *
230    *    <jc>// Set the field</jc>
231    *    <jv>beanMap</jv>.put(<js>"birthDate"</js>, <js>"'1901-03-03T04:05:06-5000'"</js>);
232    * </p>
233    *
234    * @param property The name of the property to set.
235    * @param value The value to set the property to.
236    * @return
237    *    If the bean context setting {@code beanMapPutReturnsOldValue} is <jk>true</jk>, then the old value of the
238    *    property is returned.
239    *    Otherwise, this method always returns <jk>null</jk>.
240    * @throws
241    *    RuntimeException if any of the following occur.
242    *    <ul>
243    *       <li>BeanMapEntry does not exist on the underlying object.
244    *       <li>Security settings prevent access to the underlying object setter method.
245    *       <li>An exception occurred inside the setter method.
246    *    </ul>
247    */
248   @Override /* Map */
249   public Object put(String property, Object value) {
250      BeanPropertyMeta p = getPropertyMeta(property);
251      if (p == null) {
252         if (meta.ctx.isIgnoreUnknownBeanProperties() || 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 org.apache.juneau.xml.annotation.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 ObjectSwap} 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.swaps.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='bjava'>
302    *    <jc>// Construct a bean with a 'birthDate' Date field</jc>
303    *    Person <jv>person</jv> = <jk>new</jk> Person();
304    *    <jv>person</jv>.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 <jv>beanContext</jv> = BeanContext.<jsm>create</jsm>().swaps(DateSwap.ISO8601DT.<jk>class</jk>).build();
308    *
309    *    <jc>// Wrap our bean in a bean map</jc>
310    *    BeanMap&lt;Person&gt; <jv>beanMap</jv> = <jv>beanContext</jv>.toBeanMap(<jv>person</jv>);
311    *
312    *    <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc>
313    *    String <jv>birthDate</jv> = <jv>beanMap</jv>.get(<js>"birthDate"</js>);
314    * </p>
315    *
316    * @param property The name of the property to get.
317    * @return The property value.
318    * @throws 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 = s(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)} but casts the value to the specific type.
336    *
337    * @param <T2> The type to cast to.
338    * @param property The name of the property to get.
339    * @param c The type to cast to.
340    * @return The property value.
341    * @throws RuntimeException if any of the following occur.
342    *    <ol>
343    *       <li>BeanMapEntry does not exist on the underlying object.
344    *       <li>Security settings prevent access to the underlying object getter method.
345    *       <li>An exception occurred inside the getter method.
346    *    </ol>
347    * @throws ClassCastException if property is not the specified type.
348    */
349   @SuppressWarnings("unchecked")
350   public <T2> T2 get(String property, Class<T2> c) {
351      String pName = s(property);
352      BeanPropertyMeta p = getPropertyMeta(pName);
353      if (p == null)
354         return (T2)meta.onReadProperty(this.bean, pName, null);
355      return (T2)p.get(this, pName);
356   }
357
358   /**
359    * Same as {@link #get(Object)} except bypasses the POJO filter associated with the bean property or bean filter
360    * associated with the bean class.
361    *
362    * @param property The name of the property to get.
363    * @return The raw property value.
364    */
365   public Object getRaw(Object property) {
366      String pName = s(property);
367      BeanPropertyMeta p = getPropertyMeta(pName);
368      if (p == null)
369         return null;
370      return p.getRaw(this, pName);
371   }
372
373   /**
374    * Convenience method for setting multiple property values by passing in JSON text.
375    *
376    * <h5 class='section'>Example:</h5>
377    * <p class='bjava'>
378    *    <jv>beanMap</jv>.load(<js>"{name:'John Smith',age:21}"</js>)
379    * </p>
380    *
381    * @param input The text that will get parsed into a map and then added to this map.
382    * @return This object.
383    * @throws ParseException Malformed input encountered.
384    */
385   public BeanMap<T> load(String input) throws ParseException {
386      putAll(JsonMap.ofJson(input));
387      return this;
388   }
389
390   /**
391    * Convenience method for setting multiple property values by passing in a reader.
392    *
393    * @param r The text that will get parsed into a map and then added to this map.
394    * @param p The parser to use to parse the text.
395    * @return This object.
396    * @throws ParseException Malformed input encountered.
397    * @throws IOException Thrown by <c>Reader</c>.
398    */
399   public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException {
400      putAll(JsonMap.ofText(r, p));
401      return this;
402   }
403
404   /**
405    * Convenience method for loading this map with the contents of the specified map.
406    *
407    * <p>
408    * Identical to {@link #putAll(Map)} except as a fluent-style method.
409    *
410    * @param entries The map containing the entries to add to this map.
411    * @return This object.
412    */
413   @SuppressWarnings({"unchecked","rawtypes"})
414   public BeanMap<T> load(Map entries) {
415      putAll(entries);
416      return this;
417   }
418
419   /**
420    * Returns the names of all properties associated with the bean.
421    *
422    * <p>
423    * The returned set is unmodifiable.
424    */
425   @Override /* Map */
426   public Set<String> keySet() {
427      if (meta.dynaProperty == null)
428         return meta.properties.keySet();
429      Set<String> l = set();
430      meta.properties.forEach((k,v) -> {
431         if (! "*".equals(k))
432            l.add(k);
433      });
434      try {
435         l.addAll(meta.dynaProperty.getDynaMap(bean).keySet());
436      } catch (Exception e) {
437         throw new BeanRuntimeException(e);
438      }
439      return l;
440   }
441
442   /**
443    * Returns the specified property on this bean map.
444    *
445    * <p>
446    * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using
447    * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods.
448    *
449    * <p>
450    * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method.
451    *
452    * @param propertyName The name of the property to look up.
453    * @return The bean property, or null if the bean has no such property.
454    */
455   public BeanMapEntry getProperty(String propertyName) {
456      BeanPropertyMeta p = getPropertyMeta(propertyName);
457      if (p == null)
458         return null;
459      return new BeanMapEntry(this, p, propertyName);
460   }
461
462   /**
463    * Returns the metadata on the specified property.
464    *
465    * @param propertyName The name of the bean property.
466    * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist.
467    */
468   public BeanPropertyMeta getPropertyMeta(String propertyName) {
469      return meta.getPropertyMeta(propertyName);
470   }
471
472   /**
473    * Returns the {@link ClassMeta} of the wrapped bean.
474    *
475    * @return The class type of the wrapped bean.
476    */
477   @Override /* Delegate */
478   public ClassMeta<T> getClassMeta() {
479      return this.meta.getClassMeta();
480   }
481
482   /**
483    * Extracts the specified field values from this bean and returns it as a simple Map.
484    *
485    * @param fields The fields to extract.
486    * @return
487    *    A new map with fields as key-value pairs.
488    *    <br>Note that modifying the values in this map will also modify the underlying bean.
489    */
490   public Map<String,Object> getProperties(String...fields) {
491      return new FilteredMap<>(null, this, fields);
492   }
493
494   /**
495    * Invokes all the getters on this bean and consumes the results.
496    *
497    * @param valueFilter Filter to apply to value before applying action.
498    * @param action The action to perform.
499    * @return The list of all bean property values.
500    */
501   public BeanMap<T> forEachValue(Predicate<Object> valueFilter, BeanPropertyConsumer action) {
502
503      // Normal bean.
504      if (meta.dynaProperty == null) {
505         forEachProperty(BeanPropertyMeta::canRead, bpm -> {
506            try {
507               Object val = bpm.get(this, null);
508               if (valueFilter.test(val))
509                  action.apply(bpm, bpm.getName(), val, null);
510            } catch (Error e) {
511               // Errors should always be uncaught.
512               throw e;
513            } catch (Throwable t) {
514               action.apply(bpm, bpm.getName(), null, t);
515            }
516         });
517
518      // Bean with dyna properties.
519      } else {
520         Map<String,BeanPropertyValue> actions = (meta.sortProperties ? CollectionUtils.sortedMap() : map());
521
522         forEachProperty(x -> ! x.isDyna(), bpm -> {
523            try {
524               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), bpm.get(this, null), null));
525            } catch (Error e) {
526               // Errors should always be uncaught.
527               throw e;
528            } catch (Throwable t) {
529               actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), null, t));
530            }
531         });
532
533         forEachProperty(BeanPropertyMeta::isDyna, bpm -> {
534            try {
535               // TODO - This is kind of inefficient.
536               Map<String,Object> dynaMap = bpm.getDynaMap(bean);
537               if (dynaMap != null) {
538                  dynaMap.forEach((k,v) -> {
539                     Object val = bpm.get(this, k);
540                     actions.put(k, new BeanPropertyValue(bpm, k, val, null));
541                  });
542               }
543            } catch (Exception e) {
544               e.printStackTrace();
545            }
546         });
547
548         actions.forEach((k,v) -> {
549            if (valueFilter.test(v.getValue()))
550               action.apply(v.getMeta(), v.getName(), v.getValue(), v.getThrown());
551         });
552      }
553
554      return this;
555   }
556
557   /**
558    * Given a string containing variables of the form <c>"{property}"</c>, replaces those variables with property
559    * values in this bean.
560    *
561    * @param s The string containing variables.
562    * @return A new string with variables replaced, or the same string if no variables were found.
563    */
564   public String resolveVars(String s) {
565      return StringUtils.replaceVars(s, this);
566   }
567
568   /**
569    * Returns a simple collection of properties for this bean map.
570    *
571    * @return A simple collection of properties for this bean map.
572    */
573   protected Collection<BeanPropertyMeta> getProperties() {
574      return alist(meta.propertyArray);
575   }
576
577   /**
578    * Performs an action on each property in this bean map.
579    *
580    * @param filter The filter to apply to properties.
581    * @param action The action.
582    * @return This object.
583    */
584   public BeanMap<T> forEachProperty(Predicate<BeanPropertyMeta> filter, Consumer<BeanPropertyMeta> action) {
585      for (BeanPropertyMeta bpm : meta.propertyArray)
586         if (filter.test(bpm))
587            action.accept(bpm);
588      return this;
589   }
590
591   /**
592    * Returns all the properties associated with the bean.
593    *
594    * @return A new set.
595    */
596   @Override
597   public Set<Entry<String,Object>> entrySet() {
598
599      // If this bean has a dyna-property, then we need to construct the entire set before returning.
600      // Otherwise, we can create an iterator without a new data structure.
601      if (meta.dynaProperty != null) {
602         Set<Entry<String,Object>> s = set();
603         forEachProperty(x -> true, x -> {
604            if (x.isDyna()) {
605               try {
606                  x.getDynaMap(bean).entrySet().forEach(y -> s.add(new BeanMapEntry(this, x, y.getKey())));
607               } catch (Exception e) {
608                  throw new BeanRuntimeException(e);
609               }
610            } else {
611               s.add(new BeanMapEntry(this, x, x.getName()));
612            }
613         });
614         return s;
615      }
616
617      // Construct our own anonymous set to implement this function.
618      Set<Entry<String,Object>> s = new AbstractSet<>() {
619
620         // Get the list of properties from the meta object.
621         // Note that the HashMap.values() method caches results, so this collection
622         // will really only be constructed once per bean type since the underlying
623         // map never changes.
624         final Collection<BeanPropertyMeta> pSet = getProperties();
625
626         @Override /* Set */
627         public Iterator<java.util.Map.Entry<String, Object>> iterator() {
628
629            // Construct our own anonymous iterator that uses iterators against the meta.properties
630            // map to maintain position.  This prevents us from having to construct any of our own
631            // collection objects.
632            return new Iterator<>() {
633
634               final Iterator<BeanPropertyMeta> pIterator = pSet.iterator();
635
636               @Override /* Iterator */
637               public boolean hasNext() {
638                  return pIterator.hasNext();
639               }
640
641               @Override /* Iterator */
642               public Map.Entry<String, Object> next() {
643                  return new BeanMapEntry(BeanMap.this, pIterator.next(), null);
644               }
645
646               @Override /* Iterator */
647               public void remove() {
648                  throw new UnsupportedOperationException("Cannot remove item from iterator.");
649               }
650            };
651         }
652
653         @Override /* Set */
654         public int size() {
655            return pSet.size();
656         }
657      };
658
659      return s;
660   }
661
662   @SuppressWarnings("unchecked")
663   void setBean(Object bean) {
664      this.bean = (T)bean;
665   }
666}