001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau; 014 015import static org.apache.juneau.internal.StringUtils.*; 016 017import java.io.*; 018import java.util.*; 019 020import org.apache.juneau.annotation.*; 021import org.apache.juneau.internal.*; 022import org.apache.juneau.json.*; 023import org.apache.juneau.parser.*; 024import org.apache.juneau.reflect.*; 025import org.apache.juneau.transform.*; 026import org.apache.juneau.xml.annotation.*; 027 028/** 029 * Java bean wrapper class. 030 * 031 * <h5 class='topic'>Description</h5> 032 * 033 * A wrapper that wraps Java bean instances inside of a {@link Map} interface that allows properties on the wrapped 034 * object can be accessed using the {@link Map#get(Object) get()} and {@link Map#put(Object,Object) put()} methods. 035 * 036 * <p> 037 * Use the {@link BeanContext} class to create instances of this class. 038 * 039 * <h5 class='topic'>Bean property order</h5> 040 * 041 * The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()} 042 * methods are as follows: 043 * <ul class='spaced-list'> 044 * <li> 045 * If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties 046 * in the annotation. 047 * <li> 048 * If {@link Bean @Bean} annotation is not specified on the class, then the order is the same as that returned 049 * by the {@link java.beans.BeanInfo} class (i.e. ordered by definition in the class). 050 * </ul> 051 * 052 * <p> 053 * <br>The order can also be overridden through the use of a {@link BeanFilter}. 054 * 055 * <h5 class='topic'>POJO swaps</h5> 056 * 057 * If {@link PojoSwap PojoSwaps} are defined on the class types of the properties of this bean or the bean properties 058 * themselves, the {@link #get(Object)} and {@link #put(String, Object)} methods will automatically transform the 059 * property value to and from the serialized form. 060 * 061 * @param <T> Specifies the type of object that this map encapsulates. 062 */ 063public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T> { 064 065 /** The wrapped object. */ 066 protected T bean; 067 068 /** Temporary holding cache for beans with read-only properties. Normally null. */ 069 protected Map<String,Object> propertyCache; 070 071 /** Temporary holding cache for bean properties of array types when the add() method is being used. */ 072 protected Map<String,List<?>> arrayPropertyCache; 073 074 /** The BeanMeta associated with the class of the object. */ 075 protected BeanMeta<T> meta; 076 077 private final BeanSession session; 078 private final String beanTypePropertyName; 079 080 /** 081 * Convenience method for wrapping a bean inside a {@link BeanMap}. 082 * 083 * @param <T> The bean type. 084 * @param bean The bean being wrapped. 085 * @return A new {@link BeanMap} instance wrapping the bean. 086 */ 087 public static <T> BeanMap<T> create(T bean) { 088 return BeanContext.DEFAULT.createBeanSession().toBeanMap(bean); 089 } 090 091 /** 092 * Instance of this class are instantiated through the BeanContext class. 093 * 094 * @param session The bean session object that created this bean map. 095 * @param bean The bean to wrap inside this map. 096 * @param meta The metadata associated with the bean class. 097 */ 098 protected BeanMap(BeanSession session, T bean, BeanMeta<T> meta) { 099 this.session = session; 100 this.bean = bean; 101 this.meta = meta; 102 if (meta.constructorArgs.length > 0) 103 propertyCache = new TreeMap<>(); 104 this.beanTypePropertyName = session.getBeanTypePropertyName(meta.classMeta); 105 } 106 107 /** 108 * Returns the metadata associated with this bean map. 109 * 110 * @return The metadata associated with this bean map. 111 */ 112 public BeanMeta<T> getMeta() { 113 return meta; 114 } 115 116 /** 117 * Returns the bean session that created this bean map. 118 * 119 * @return The bean session that created this bean map. 120 */ 121 public final BeanSession getBeanSession() { 122 return session; 123 } 124 125 /** 126 * Returns the wrapped bean object. 127 * 128 * <p> 129 * Triggers bean creation if bean has read-only properties set through a constructor defined by the 130 * {@link BeanConstructor @BeanConstructor} annotation. 131 * 132 * @return The inner bean object. 133 */ 134 public T getBean() { 135 T b = getBean(true); 136 137 // If we have any arrays that need to be constructed, do it now. 138 if (arrayPropertyCache != null) { 139 for (Map.Entry<String,List<?>> e : arrayPropertyCache.entrySet()) { 140 String key = e.getKey(); 141 List<?> value = e.getValue(); 142 BeanPropertyMeta bpm = getPropertyMeta(key); 143 try { 144 bpm.setArray(b, value); 145 } catch (Exception e1) { 146 throw new RuntimeException(e1); 147 } 148 } 149 arrayPropertyCache = null; 150 } 151 return b; 152 } 153 154 /** 155 * Returns the wrapped bean object. 156 * 157 * <p> 158 * If <c>create</c> is <jk>false</jk>, then this method may return <jk>null</jk> if the bean has read-only 159 * properties set through a constructor defined by the {@link BeanConstructor @BeanConstructor} annotation. 160 * 161 * <p> 162 * This method does NOT always return the bean in it's final state. 163 * Array properties temporary stored as ArrayLists are not finalized until the {@link #getBean()} method is called. 164 * 165 * @param create If bean hasn't been instantiated yet, then instantiate it. 166 * @return The inner bean object. 167 */ 168 public T getBean(boolean create) { 169 /** If this is a read-only bean, then we need to create it. */ 170 if (bean == null && create && meta.constructorArgs.length > 0) { 171 String[] props = meta.constructorArgs; 172 ConstructorInfo c = meta.constructor; 173 Object[] args = new Object[props.length]; 174 for (int i = 0; i < props.length; i++) 175 args[i] = propertyCache.remove(props[i]); 176 try { 177 bean = c.<T>invoke(args); 178 for (Map.Entry<String,Object> e : propertyCache.entrySet()) 179 put(e.getKey(), e.getValue()); 180 propertyCache = null; 181 } catch (IllegalArgumentException e) { 182 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))); 183 } catch (Exception e) { 184 throw new BeanRuntimeException(e); 185 } 186 } 187 return bean; 188 } 189 190 /** 191 * Sets a property on the bean. 192 * 193 * <p> 194 * If there is a {@link PojoSwap} associated with this bean property or bean property type class, then you must pass 195 * in a transformed value. 196 * For example, if the bean property type class is a {@link Date} and the bean property has the 197 * {@link org.apache.juneau.transforms.TemporalDateSwap.IsoInstant} swap associated with it through the 198 * {@link Swap#value() @Swap(value)} annotation, the value being passed in must be 199 * a String containing an ISO8601 date-time string value. 200 * 201 * <h5 class='section'>Example:</h5> 202 * <p class='bcode w800'> 203 * <jc>// Construct a bean with a 'birthDate' Date field</jc> 204 * Person p = <jk>new</jk> Person(); 205 * 206 * <jc>// Create a bean context and add the ISO8601 date-time swap</jc> 207 * BeanContext beanContext = <jk>new</jk> BeanContext().pojoSwaps(DateSwap.ISO8601DT.<jk>class</jk>); 208 * 209 * <jc>// Wrap our bean in a bean map</jc> 210 * BeanMap<Person> b = beanContext.forBean(p); 211 * 212 * <jc>// Set the field</jc> 213 * myBeanMap.put(<js>"birthDate"</js>, <js>"'1901-03-03T04:05:06-5000'"</js>); 214 * </p> 215 * 216 * @param property The name of the property to set. 217 * @param value The value to set the property to. 218 * @return 219 * If the bean context setting {@code beanMapPutReturnsOldValue} is <jk>true</jk>, then the old value of the 220 * property is returned. 221 * Otherwise, this method always returns <jk>null</jk>. 222 * @throws 223 * RuntimeException if any of the following occur. 224 * <ul> 225 * <li>BeanMapEntry does not exist on the underlying object. 226 * <li>Security settings prevent access to the underlying object setter method. 227 * <li>An exception occurred inside the setter method. 228 * </ul> 229 */ 230 @Override /* Map */ 231 public Object put(String property, Object value) { 232 BeanPropertyMeta p = meta.properties.get(property); 233 if (p == null) { 234 if (meta.ctx.isIgnoreUnknownBeanProperties()) 235 return null; 236 237 if (property.equals(beanTypePropertyName)) 238 return null; 239 240 p = meta.properties.get("*"); 241 if (p == null) 242 throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property); 243 } 244 if (meta.beanFilter != null) 245 value = meta.beanFilter.writeProperty(this.bean, property, value); 246 return p.set(this, property, value); 247 } 248 249 /** 250 * Add a value to a collection or array property. 251 * 252 * <p> 253 * As a general rule, adding to arrays is not recommended since the array must be recreate each time this method is 254 * called. 255 * 256 * @param property Property name or child-element name (if {@link Xml#childName() @Xml(childName)} is specified). 257 * @param value The value to add to the collection or array. 258 */ 259 public void add(String property, Object value) { 260 BeanPropertyMeta p = meta.properties.get(property); 261 if (p == null) { 262 if (meta.ctx.isIgnoreUnknownBeanProperties()) 263 return; 264 throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property); 265 } 266 p.add(this, property, value); 267 } 268 269 270 /** 271 * Gets a property on the bean. 272 * 273 * <p> 274 * If there is a {@link PojoSwap} associated with this bean property or bean property type class, then this method 275 * will return the transformed value. 276 * For example, if the bean property type class is a {@link Date} and the bean property has the 277 * {@link org.apache.juneau.transforms.TemporalDateSwap.IsoInstant} swap associated with it through the 278 * {@link Swap#value() @Swap(value)} annotation, this method will return a String containing an 279 * ISO8601 date-time string value. 280 * 281 * <h5 class='section'>Example:</h5> 282 * <p class='bcode w800'> 283 * <jc>// Construct a bean with a 'birthDate' Date field</jc> 284 * Person p = <jk>new</jk> Person(); 285 * p.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6)); 286 * 287 * <jc>// Create a bean context and add the ISO8601 date-time swap</jc> 288 * BeanContext beanContext = <jk>new</jk> BeanContext().pojoSwaps(DateSwap.ISO8601DT.<jk>class</jk>); 289 * 290 * <jc>// Wrap our bean in a bean map</jc> 291 * BeanMap<Person> b = beanContext.forBean(p); 292 * 293 * <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc> 294 * String s = myBeanMap.get(<js>"birthDate"</js>); 295 * </p> 296 * 297 * @param property The name of the property to get. 298 * @throws 299 * RuntimeException if any of the following occur. 300 * <ol> 301 * <li>BeanMapEntry does not exist on the underlying object. 302 * <li>Security settings prevent access to the underlying object getter method. 303 * <li>An exception occurred inside the getter method. 304 * </ol> 305 */ 306 @Override /* Map */ 307 public Object get(Object property) { 308 String pName = stringify(property); 309 BeanPropertyMeta p = getPropertyMeta(pName); 310 if (p == null) 311 return null; 312 if (meta.beanFilter != null) 313 return meta.beanFilter.readProperty(this.bean, pName, p.get(this, pName)); 314 return p.get(this, pName); 315 } 316 317 /** 318 * Same as {@link #get(Object)} except bypasses the POJO filter associated with the bean property or bean filter 319 * associated with the bean class. 320 * 321 * @param property The name of the property to get. 322 * @return The raw property value. 323 */ 324 public Object getRaw(Object property) { 325 String pName = stringify(property); 326 BeanPropertyMeta p = getPropertyMeta(pName); 327 if (p == null) 328 return null; 329 return p.getRaw(this, pName); 330 } 331 332 /** 333 * Convenience method for setting multiple property values by passing in JSON text. 334 * 335 * <h5 class='section'>Example:</h5> 336 * <p class='bcode w800'> 337 * aPersonBean.load(<js>"{name:'John Smith',age:21}"</js>) 338 * </p> 339 * 340 * @param input The text that will get parsed into a map and then added to this map. 341 * @return This object (for method chaining). 342 * @throws ParseException Malformed input encountered. 343 */ 344 public BeanMap<T> load(String input) throws ParseException { 345 putAll(new ObjectMap(input)); 346 return this; 347 } 348 349 /** 350 * Convenience method for setting multiple property values by passing in a reader. 351 * 352 * @param r The text that will get parsed into a map and then added to this map. 353 * @param p The parser to use to parse the text. 354 * @return This object (for method chaining). 355 * @throws ParseException Malformed input encountered. 356 * @throws IOException Thrown by <c>Reader</c>. 357 */ 358 public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException { 359 putAll(new ObjectMap(r, p)); 360 return this; 361 } 362 363 /** 364 * Convenience method for loading this map with the contents of the specified map. 365 * 366 * <p> 367 * Identical to {@link #putAll(Map)} except as a fluent-style method. 368 * 369 * @param entries The map containing the entries to add to this map. 370 * @return This object (for method chaining). 371 */ 372 @SuppressWarnings({"unchecked","rawtypes"}) 373 public BeanMap<T> load(Map entries) { 374 putAll(entries); 375 return this; 376 } 377 378 /** 379 * Returns the names of all properties associated with the bean. 380 * 381 * <p> 382 * The returned set is unmodifiable. 383 */ 384 @Override /* Map */ 385 public Set<String> keySet() { 386 if (meta.dynaProperty == null) 387 return meta.properties.keySet(); 388 Set<String> l = new LinkedHashSet<>(); 389 for (String p : meta.properties.keySet()) 390 if (! "*".equals(p)) 391 l.add(p); 392 try { 393 l.addAll(meta.dynaProperty.getDynaMap(bean).keySet()); 394 } catch (Exception e) { 395 throw new BeanRuntimeException(e); 396 } 397 return l; 398 } 399 400 /** 401 * Returns the specified property on this bean map. 402 * 403 * <p> 404 * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using 405 * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods. 406 * 407 * <p> 408 * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method. 409 * 410 * @param propertyName The name of the property to look up. 411 * @return The bean property, or null if the bean has no such property. 412 */ 413 public BeanMapEntry getProperty(String propertyName) { 414 BeanPropertyMeta p = getPropertyMeta(propertyName); 415 if (p == null) 416 return null; 417 return new BeanMapEntry(this, p, propertyName); 418 } 419 420 /** 421 * Returns the metadata on the specified property. 422 * 423 * @param propertyName The name of the bean property. 424 * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist. 425 */ 426 public BeanPropertyMeta getPropertyMeta(String propertyName) { 427 BeanPropertyMeta bpMeta = meta.properties.get(propertyName); 428 if (bpMeta == null) 429 bpMeta = meta.dynaProperty; 430 return bpMeta; 431 } 432 433 /** 434 * Returns the {@link ClassMeta} of the wrapped bean. 435 * 436 * @return The class type of the wrapped bean. 437 */ 438 @Override /* Delegate */ 439 public ClassMeta<T> getClassMeta() { 440 return this.meta.getClassMeta(); 441 } 442 443 /** 444 * Invokes all the getters on this bean and return the values as a list of {@link BeanPropertyValue} objects. 445 * 446 * <p> 447 * This allows a snapshot of all values to be grabbed from a bean in one call. 448 * 449 * @param ignoreNulls 450 * Don't return properties whose values are null. 451 * @param prependVals 452 * Additional bean property values to prepended to this list. 453 * Any <jk>null</jk> values in this list will be ignored. 454 * @return The list of all bean property values. 455 */ 456 public List<BeanPropertyValue> getValues(final boolean ignoreNulls, BeanPropertyValue...prependVals) { 457 Collection<BeanPropertyMeta> properties = getProperties(); 458 int capacity = (ignoreNulls && properties.size() > 10) ? 10 : properties.size() + prependVals.length; 459 List<BeanPropertyValue> l = new ArrayList<>(capacity); 460 for (BeanPropertyValue v : prependVals) 461 if (v != null) 462 l.add(v); 463 for (BeanPropertyMeta bpm : properties) { 464 if (bpm.canRead()) { 465 try { 466 if (bpm.isDyna()) { 467 Map<String,Object> dynaMap = bpm.getDynaMap(bean); 468 if (dynaMap != null) { 469 for (String pName : bpm.getDynaMap(bean).keySet()) { 470 Object val = bpm.get(this, pName); 471 if (val != null || ! ignoreNulls) 472 l.add(new BeanPropertyValue(bpm, pName, val, null)); 473 } 474 } 475 } else { 476 Object val = bpm.get(this, null); 477 if (val != null || ! ignoreNulls) 478 l.add(new BeanPropertyValue(bpm, bpm.getName(), val, null)); 479 } 480 } catch (Error e) { 481 // Errors should always be uncaught. 482 throw e; 483 } catch (Throwable t) { 484 l.add(new BeanPropertyValue(bpm, bpm.getName(), null, t)); 485 } 486 } 487 } 488 if (meta.sortProperties && meta.dynaProperty != null) 489 Collections.sort(l); 490 return l; 491 } 492 493 /** 494 * Given a string containing variables of the form <c>"{property}"</c>, replaces those variables with property 495 * values in this bean. 496 * 497 * @param s The string containing variables. 498 * @return A new string with variables replaced, or the same string if no variables were found. 499 */ 500 public String resolveVars(String s) { 501 return StringUtils.replaceVars(s, this); 502 } 503 504 /** 505 * Returns a simple collection of properties for this bean map. 506 * 507 * @return A simple collection of properties for this bean map. 508 */ 509 protected Collection<BeanPropertyMeta> getProperties() { 510 return meta.properties.values(); 511 } 512 513 /** 514 * Returns all the properties associated with the bean. 515 * 516 * @return A new set. 517 */ 518 @Override 519 public Set<Entry<String,Object>> entrySet() { 520 521 // If this bean has a dyna-property, then we need to construct the entire set before returning. 522 // Otherwise, we can create an iterator without a new data structure. 523 if (meta.dynaProperty != null) { 524 Set<Entry<String,Object>> s = new LinkedHashSet<>(); 525 for (BeanPropertyMeta pMeta : getProperties()) { 526 if (pMeta.isDyna()) { 527 try { 528 for (Map.Entry<String,Object> e : pMeta.getDynaMap(bean).entrySet()) 529 s.add(new BeanMapEntry(this, pMeta, e.getKey())); 530 } catch (Exception e) { 531 throw new BeanRuntimeException(e); 532 } 533 } else { 534 s.add(new BeanMapEntry(this, pMeta, pMeta.getName())); 535 } 536 } 537 return s; 538 } 539 540 // Construct our own anonymous set to implement this function. 541 Set<Entry<String,Object>> s = new AbstractSet<Entry<String,Object>>() { 542 543 // Get the list of properties from the meta object. 544 // Note that the HashMap.values() method caches results, so this collection 545 // will really only be constructed once per bean type since the underlying 546 // map never changes. 547 final Collection<BeanPropertyMeta> pSet = getProperties(); 548 549 @Override /* Set */ 550 public Iterator<java.util.Map.Entry<String, Object>> iterator() { 551 552 // Construct our own anonymous iterator that uses iterators against the meta.properties 553 // map to maintain position. This prevents us from having to construct any of our own 554 // collection objects. 555 return new Iterator<Entry<String,Object>>() { 556 557 final Iterator<BeanPropertyMeta> pIterator = pSet.iterator(); 558 559 @Override /* Iterator */ 560 public boolean hasNext() { 561 return pIterator.hasNext(); 562 } 563 564 @Override /* Iterator */ 565 public Map.Entry<String, Object> next() { 566 return new BeanMapEntry(BeanMap.this, pIterator.next(), null); 567 } 568 569 @Override /* Iterator */ 570 public void remove() { 571 throw new UnsupportedOperationException("Cannot remove item from iterator."); 572 } 573 }; 574 } 575 576 @Override /* Set */ 577 public int size() { 578 return pSet.size(); 579 } 580 }; 581 582 return s; 583 } 584 585 @SuppressWarnings("unchecked") 586 void setBean(Object bean) { 587 this.bean = (T)bean; 588 } 589}