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