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