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