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<Person> 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<Person> 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}