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("IllegalArgumentException occurred on call to class constructor ''{0}'' with argument types ''{1}''", c.getName(), JsonSerializer.DEFAULT_LAX.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'> 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.ignoreUnknownBeanProperties) 224 return null; 225 226 if (property.equals(beanTypePropertyName)) 227 return null; 228 229 throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property); 230 } 231 if (meta.beanFilter != null) 232 value = meta.beanFilter.writeProperty(this.bean, property, value); 233 return p.set(this, property, value); 234 } 235 236 /** 237 * Add a value to a collection or array property. 238 * 239 * <p> 240 * As a general rule, adding to arrays is not recommended since the array must be recreate each time this method is 241 * called. 242 * 243 * @param property Property name or child-element name (if {@link Xml#childName() @Xml.childName()} is specified). 244 * @param value The value to add to the collection or array. 245 */ 246 public void add(String property, Object value) { 247 BeanPropertyMeta p = meta.properties.get(property); 248 if (p == null) { 249 if (meta.ctx.ignoreUnknownBeanProperties) 250 return; 251 throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property); 252 } 253 p.add(this, property, value); 254 } 255 256 257 /** 258 * Gets a property on the bean. 259 * 260 * <p> 261 * If there is a {@link PojoSwap} associated with this bean property or bean property type class, then this method 262 * will return the transformed value. 263 * For example, if the bean property type class is a {@link Date} and the bean property has the 264 * {@link org.apache.juneau.transforms.DateSwap.ISO8601DT} swap associated with it through the 265 * {@link Swap#value() @Swap.value()} annotation, this method will return a String containing an 266 * ISO8601 date-time string value. 267 * 268 * <h5 class='section'>Example:</h5> 269 * <p class='bcode'> 270 * <jc>// Construct a bean with a 'birthDate' Date field</jc> 271 * Person p = <jk>new</jk> Person(); 272 * p.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6)); 273 * 274 * <jc>// Create a bean context and add the ISO8601 date-time swap</jc> 275 * BeanContext beanContext = <jk>new</jk> BeanContext().pojoSwaps(DateSwap.ISO8601DT.<jk>class</jk>); 276 * 277 * <jc>// Wrap our bean in a bean map</jc> 278 * BeanMap<Person> b = beanContext.forBean(p); 279 * 280 * <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc> 281 * String s = myBeanMap.get(<js>"birthDate"</js>); 282 * </p> 283 * 284 * @param property The name of the property to get. 285 * @throws 286 * RuntimeException if any of the following occur. 287 * <ol> 288 * <li>BeanMapEntry does not exist on the underlying object. 289 * <li>Security settings prevent access to the underlying object getter method. 290 * <li>An exception occurred inside the getter method. 291 * </ol> 292 */ 293 @Override /* Map */ 294 public Object get(Object property) { 295 String pName = asString(property); 296 BeanPropertyMeta p = getPropertyMeta(pName); 297 if (p == null) 298 return null; 299 if (meta.beanFilter != null) 300 return meta.beanFilter.readProperty(this.bean, pName, p.get(this, pName)); 301 return p.get(this, pName); 302 } 303 304 /** 305 * Same as {@link #get(Object)} except bypasses the POJO filter associated with the bean property or bean filter 306 * associated with the bean class. 307 * 308 * @param property The name of the property to get. 309 * @return The raw property value. 310 */ 311 public Object getRaw(Object property) { 312 String pName = asString(property); 313 BeanPropertyMeta p = getPropertyMeta(pName); 314 if (p == null) 315 return null; 316 return p.getRaw(this, pName); 317 } 318 319 /** 320 * Convenience method for setting multiple property values by passing in JSON text. 321 * 322 * <h5 class='section'>Example:</h5> 323 * <p class='bcode'> 324 * aPersonBean.load(<js>"{name:'John Smith',age:21}"</js>) 325 * </p> 326 * 327 * @param input The text that will get parsed into a map and then added to this map. 328 * @return This object (for method chaining). 329 * @throws ParseException If the input contains a syntax error or is malformed. 330 */ 331 public BeanMap<T> load(String input) throws ParseException { 332 putAll(new ObjectMap(input)); 333 return this; 334 } 335 336 /** 337 * Convenience method for setting multiple property values by passing in a reader. 338 * 339 * @param r The text that will get parsed into a map and then added to this map. 340 * @param p The parser to use to parse the text. 341 * @return This object (for method chaining). 342 * @throws ParseException If the input contains a syntax error or is malformed. 343 * @throws IOException Thrown by <code>Reader</code>. 344 */ 345 public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException { 346 putAll(new ObjectMap(r, p)); 347 return this; 348 } 349 350 /** 351 * Convenience method for loading this map with the contents of the specified map. 352 * 353 * <p> 354 * Identical to {@link #putAll(Map)} except as a fluent-style method. 355 * 356 * @param entries The map containing the entries to add to this map. 357 * @return This object (for method chaining). 358 */ 359 @SuppressWarnings({"unchecked","rawtypes"}) 360 public BeanMap<T> load(Map entries) { 361 putAll(entries); 362 return this; 363 } 364 365 /** 366 * Returns the names of all properties associated with the bean. 367 * 368 * <p> 369 * The returned set is unmodifiable. 370 */ 371 @Override /* Map */ 372 public Set<String> keySet() { 373 if (meta.dynaProperty == null) 374 return meta.properties.keySet(); 375 Set<String> l = new LinkedHashSet<>(); 376 for (String p : meta.properties.keySet()) 377 if (! "*".equals(p)) 378 l.add(p); 379 try { 380 l.addAll(meta.dynaProperty.getDynaMap(bean).keySet()); 381 } catch (Exception e) { 382 throw new BeanRuntimeException(e); 383 } 384 return l; 385 } 386 387 /** 388 * Returns the specified property on this bean map. 389 * 390 * <p> 391 * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using 392 * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods. 393 * 394 * <p> 395 * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method. 396 * 397 * @param propertyName The name of the property to look up. 398 * @return The bean property, or null if the bean has no such property. 399 */ 400 public BeanMapEntry getProperty(String propertyName) { 401 BeanPropertyMeta p = getPropertyMeta(propertyName); 402 if (p == null) 403 return null; 404 return new BeanMapEntry(this, p, propertyName); 405 } 406 407 /** 408 * Returns the metadata on the specified property. 409 * 410 * @param propertyName The name of the bean property. 411 * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist. 412 */ 413 public BeanPropertyMeta getPropertyMeta(String propertyName) { 414 BeanPropertyMeta bpMeta = meta.properties.get(propertyName); 415 if (bpMeta == null) 416 bpMeta = meta.dynaProperty; 417 return bpMeta; 418 } 419 420 /** 421 * Returns the {@link ClassMeta} of the wrapped bean. 422 * 423 * @return The class type of the wrapped bean. 424 */ 425 @Override /* Delegate */ 426 public ClassMeta<T> getClassMeta() { 427 return this.meta.getClassMeta(); 428 } 429 430 /** 431 * Invokes all the getters on this bean and return the values as a list of {@link BeanPropertyValue} objects. 432 * 433 * <p> 434 * This allows a snapshot of all values to be grabbed from a bean in one call. 435 * 436 * @param ignoreNulls 437 * Don't return properties whose values are null. 438 * @param prependVals 439 * Additional bean property values to prepended to this list. 440 * Any <jk>null</jk> values in this list will be ignored. 441 * @return The list of all bean property values. 442 */ 443 public List<BeanPropertyValue> getValues(final boolean ignoreNulls, BeanPropertyValue...prependVals) { 444 Collection<BeanPropertyMeta> properties = getProperties(); 445 int capacity = (ignoreNulls && properties.size() > 10) ? 10 : properties.size() + prependVals.length; 446 List<BeanPropertyValue> l = new ArrayList<>(capacity); 447 for (BeanPropertyValue v : prependVals) 448 if (v != null) 449 l.add(v); 450 for (BeanPropertyMeta bpm : properties) { 451 if (bpm.canRead()) { 452 try { 453 if (bpm.isDyna()) { 454 for (String pName : bpm.getDynaMap(bean).keySet()) { 455 Object val = bpm.get(this, pName); 456 if (val != null || ! ignoreNulls) 457 l.add(new BeanPropertyValue(bpm, pName, val, null)); 458 } 459 } else { 460 Object val = bpm.get(this, null); 461 if (val != null || ! ignoreNulls) 462 l.add(new BeanPropertyValue(bpm, bpm.getName(), val, null)); 463 } 464 } catch (Error e) { 465 // Errors should always be uncaught. 466 throw e; 467 } catch (Throwable t) { 468 l.add(new BeanPropertyValue(bpm, bpm.getName(), null, t)); 469 } 470 } 471 } 472 if (meta.sortProperties && meta.dynaProperty != null) 473 Collections.sort(l); 474 return l; 475 } 476 477 /** 478 * Given a string containing variables of the form <code>"{property}"</code>, replaces those variables with property 479 * values in this bean. 480 * 481 * @param s The string containing variables. 482 * @return A new string with variables replaced, or the same string if no variables were found. 483 */ 484 public String resolveVars(String s) { 485 return StringUtils.replaceVars(s, this); 486 } 487 488 /** 489 * Returns a simple collection of properties for this bean map. 490 * 491 * @return A simple collection of properties for this bean map. 492 */ 493 protected Collection<BeanPropertyMeta> getProperties() { 494 return meta.properties.values(); 495 } 496 497 /** 498 * Returns all the properties associated with the bean. 499 * 500 * @return A new set. 501 */ 502 @Override 503 public Set<Entry<String,Object>> entrySet() { 504 505 // If this bean has a dyna-property, then we need to construct the entire set before returning. 506 // Otherwise, we can create an iterator without a new data structure. 507 if (meta.dynaProperty != null) { 508 Set<Entry<String,Object>> s = new LinkedHashSet<>(); 509 for (BeanPropertyMeta pMeta : getProperties()) { 510 if (pMeta.isDyna()) { 511 try { 512 for (Map.Entry<String,Object> e : pMeta.getDynaMap(bean).entrySet()) 513 s.add(new BeanMapEntry(this, pMeta, e.getKey())); 514 } catch (Exception e) { 515 throw new BeanRuntimeException(e); 516 } 517 } else { 518 s.add(new BeanMapEntry(this, pMeta, pMeta.getName())); 519 } 520 } 521 return s; 522 } 523 524 // Construct our own anonymous set to implement this function. 525 Set<Entry<String,Object>> s = new AbstractSet<Entry<String,Object>>() { 526 527 // Get the list of properties from the meta object. 528 // Note that the HashMap.values() method caches results, so this collection 529 // will really only be constructed once per bean type since the underlying 530 // map never changes. 531 final Collection<BeanPropertyMeta> pSet = getProperties(); 532 533 @Override /* Set */ 534 public Iterator<java.util.Map.Entry<String, Object>> iterator() { 535 536 // Construct our own anonymous iterator that uses iterators against the meta.properties 537 // map to maintain position. This prevents us from having to construct any of our own 538 // collection objects. 539 return new Iterator<Entry<String,Object>>() { 540 541 final Iterator<BeanPropertyMeta> pIterator = pSet.iterator(); 542 543 @Override /* Iterator */ 544 public boolean hasNext() { 545 return pIterator.hasNext(); 546 } 547 548 @Override /* Iterator */ 549 public Map.Entry<String, Object> next() { 550 return new BeanMapEntry(BeanMap.this, pIterator.next(), null); 551 } 552 553 @Override /* Iterator */ 554 public void remove() { 555 throw new UnsupportedOperationException("Cannot remove item from iterator."); 556 } 557 }; 558 } 559 560 @Override /* Set */ 561 public int size() { 562 return pSet.size(); 563 } 564 }; 565 566 return s; 567 } 568 569 @SuppressWarnings("unchecked") 570 void setBean(Object bean) { 571 this.bean = (T)bean; 572 } 573}