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