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