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.commons.bean; 018 019import static org.apache.juneau.commons.utils.ClassUtils.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023import static org.apache.juneau.commons.utils.ThrowableUtils.*; 024 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.commons.reflect.*; 029 030/** 031 * Java bean wrapper class. 032 * 033 * <h5 class='topic'>Description</h5> 034 * 035 * A wrapper that wraps Java bean instances inside of a {@link Map} interface that allows properties on the wrapped 036 * object can be accessed using the {@link Map#get(Object) get()} and {@link Map#put(Object,Object) put()} methods. 037 * 038 * <p> 039 * Create instances through the bean API. For the bean-modeling-only path use {@link BeanMap#of(Object)} or 040 * {@link BeanMap#of(Object, BeanMeta)}; the marshalling layer's {@code MarshallingSession#toBeanMap(Object)} 041 * additionally wires a {@link BeanSession} into the resulting map for session-aware behavior. 042 * 043 * <h5 class='topic'>Bean property order</h5> 044 * 045 * The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()} 046 * methods is as follows: 047 * <ul class='spaced-list'> 048 * <li> 049 * If the {@link BeanType @BeanType} annotation specifies an explicit property list via 050 * {@link BeanType#properties() properties()} (or its synonym {@link BeanType#p() p()}), 051 * the order matches the annotation. 052 * <li> 053 * Otherwise, if {@link BeanType#unsorted() @BeanType(unsorted=true)} is set on the class, or 054 * {@link BeanConfigContext#isUnsortedProperties()} is enabled on the session, properties are returned 055 * in their natural reflection order. 056 * <li> 057 * Otherwise, properties are returned in alphabetical order. 058 * </ul> 059 * 060 * <h5 class='topic'>POJO swaps</h5> 061 * 062 * If {@code ObjectSwap} transforms are defined on the class types of the properties of this bean or the bean properties 063 * themselves, the {@link #get(Object)} and {@link #put(String, Object)} methods will automatically transform the 064 * property value to and from the serialized form. 065 * 066 * <h5 class='topic'>Thread safety</h5> 067 * 068 * Instances are not thread-safe. 069 * This type is mutable (wrapped bean reference, property caches, and optional session pointer) and does not perform 070 * internal synchronization. 071 * 072 * 073 * @param <T> Specifies the type of object that this map encapsulates. 074 */ 075@SuppressWarnings({ 076 "java:S3776", // Cognitive complexity acceptable for bean property iteration and filtering 077 "unchecked" // of() casts bean.getClass() to Class<T>, setBean() casts to T, and getProperty() casts the value to T2; all are tied to the wrapped bean type 078}) 079public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T> { 080 081 /** 082 * Convenience method for wrapping a bean inside a {@link BeanMap}. 083 * 084 * @param <T> The bean type. 085 * @param bean The bean being wrapped. Must not be <jk>null</jk> (its runtime class is used to build the metadata). 086 * @return A new {@link BeanMap} instance wrapping the bean. 087 */ 088 public static <T> BeanMap<T> of(T bean) { 089 return new BeanMap<>(bean, BeanMeta.of((Class<T>) bean.getClass())); 090 } 091 092 /** 093 * Convenience method for wrapping a bean inside a {@link BeanMap} using a pre-built {@link BeanMeta}. 094 * 095 * <p> 096 * This is the bean-modeling entry point — it builds a {@link BeanMap} without going through a 097 * marshalling session, paired with a {@link BeanMeta} typically produced by 098 * {@link BeanMeta#of(Class, BeanConfigContext)}. No marshalling session is attached. 099 * 100 * @param <T> The bean type. 101 * @param bean The bean being wrapped. Can be <jk>null</jk> for a read-only bean whose properties are supplied later and instantiated via {@link #getBean()}. 102 * @param meta The bean metadata. Must not be <jk>null</jk>. 103 * @return A new {@link BeanMap} instance wrapping the bean. 104 */ 105 public static <T> BeanMap<T> of(T bean, BeanMeta<T> meta) { 106 return new BeanMap<>(bean, meta); 107 } 108 109 /** The wrapped object. */ 110 protected T bean; 111 112 /** Temporary holding cache for beans with read-only properties. Normally null. */ 113 protected Map<String,Object> propertyCache; 114 115 private Set<String> presentProperties; // Names of required properties seen so far. null when the bean has no required properties. 116 117 /** Temporary holding cache for bean properties of array types when the add() method is being used. */ 118 protected Map<String,List<?>> arrayPropertyCache; 119 120 /** The BeanMeta associated with the class of the object. */ 121 protected BeanMeta<T> meta; 122 private BeanSession session; 123 124 private final String typePropertyName; 125 126 /** 127 * Constructor. 128 * 129 * <p> 130 * Bean-modeling-only constructor. Does not carry a {@link BeanSession} reference. 131 * The marshalling layer wires the session in via {@link #setBeanSession(BeanSession)} 132 * immediately after construction. 133 * 134 * @param bean The bean to wrap inside this map. Can be <jk>null</jk> for a read-only bean whose properties are supplied later and instantiated via {@link #getBean()}. 135 * @param meta The metadata associated with the bean class. Must not be <jk>null</jk>. 136 */ 137 public BeanMap(T bean, BeanMeta<T> meta) { 138 this.bean = bean; 139 this.meta = meta; 140 if (ine(meta.getConstructorArgs())) 141 propertyCache = new TreeMap<>(); 142 this.typePropertyName = meta.getTypePropertyName(); 143 if (meta.hasRequiredProperties()) 144 presentProperties = new HashSet<>(); 145 } 146 147 /** 148 * Records that a property was written from input. Only tracked when the bean has required properties. 149 * 150 * @param name The property name. 151 */ 152 void recordPresent(String name) { 153 if (nn(presentProperties)) 154 presentProperties.add(name); 155 } 156 157 /** 158 * Returns the {@code @BeanProp(required=true)} properties that have not been written to this map. 159 * 160 * <p> 161 * A property counts as written once {@link #put(String, Object)} (or a collection {@code add}) has been called for 162 * it, even with a <jk>null</jk> value. Parsers call this once per bean, before the bean is constructed. 163 * 164 * <h5 class='section'>Example:</h5> 165 * <p class='bjava'> 166 * <jc>// MyBean has @BeanProp(required=true) on 'name' and 'zip'.</jc> 167 * BeanMap<MyBean> <jv>m</jv> = <jv>session</jv>.newBeanMap(MyBean.<jk>class</jk>); 168 * <jv>m</jv>.put(<js>"name"</js>, <jk>null</jk>); 169 * 170 * <jc>// [zip]</jc> 171 * List<String> <jv>missing</jv> = <jv>m</jv>.getMissingRequiredProperties(); 172 * </p> 173 * 174 * @return The missing required property names, in property order. Empty (never <jk>null</jk>) if none. 175 */ 176 public List<String> getMissingRequiredProperties() { 177 if (presentProperties == null) 178 return List.of(); 179 return meta.getRequiredPropertyNames().stream().filter(x -> ! presentProperties.contains(x)).toList(); 180 } 181 182 /** 183 * Wires this bean map to a {@link BeanSession}. 184 * 185 * <p> 186 * Called by marshalling-side construction paths (for example, 187 * {@code MarshallingSession#toBeanMap(Object)}) to wire in the session used by session-aware 188 * operations (type conversion, child collection construction, and parser/serializer-backed 189 * conversions) after bean-modeling construction. 190 * 191 * @param value The bean session that produced this bean map. Typically a marshalling-session implementation. Can be <jk>null</jk> to detach the session. 192 */ 193 public void setBeanSession(BeanSession value) { 194 session = value; 195 } 196 197 /** 198 * Add a value to a collection or array property. 199 * 200 * <p> 201 * As a general rule, adding to arrays is not recommended since the array must be recreate each time this method is 202 * called. 203 * 204 * @param property Property name or child-element name (if {@code @Xml(childName)} is specified). 205 * @param value The value to add to the collection or array. Can be <jk>null</jk>. 206 */ 207 public void add(String property, Object value) { 208 var p = getPropertyMeta(property); 209 if (p == null) { 210 if (meta.getConfig().isIgnoreUnknownBeanProperties()) 211 return; 212 throw brex(meta.getClassInfo(), "Bean property '%s' not found.", property); 213 } 214 p.add(this, property, value); 215 } 216 217 @Override /* Overridden from Map */ 218 public boolean containsKey(Object property) { 219 // JUNEAU-248: Match the behavior of keySet() - only check properties map, not hiddenProperties 220 var key = emptyIfNull(property); 221 if (meta.getProperties().containsKey(key) && neq(key, "*")) 222 return true; 223 if (nn(meta.getDynaProperty())) { 224 try { 225 return meta.getDynaProperty().getDynaMap(bean).containsKey(key); 226 } catch (Exception e) { 227 throw brex(e); 228 } 229 } 230 return false; 231 } 232 233 /** 234 * Returns all the properties associated with the bean. 235 * 236 * @return A new set. 237 */ 238 @Override 239 public Set<Entry<String,Object>> entrySet() { 240 241 // If this bean has a dyna-property, then we need to construct the entire set before returning. 242 // Otherwise, we can create an iterator without a new data structure. 243 if (nn(meta.getDynaProperty())) { 244 Set<Entry<String,Object>> s = set(); 245 forEachProperty(x -> true, x -> { 246 if (x.isDyna()) { 247 try { 248 x.getDynaMap(bean).entrySet().forEach(y -> s.add(new BeanMapEntry(this, x, y.getKey()))); 249 } catch (Exception e) { 250 throw brex(e); 251 } 252 } else { 253 s.add(new BeanMapEntry(this, x, x.getName())); 254 } 255 }); 256 return s; 257 } 258 259 // Construct our own anonymous set to implement this function. 260 return new AbstractSet<>() { 261 262 // Get the list of properties from the meta object. 263 // Note that the HashMap.values() method caches results, so this collection 264 // will really only be constructed once per bean type since the underlying 265 // map never changes. 266 final Collection<BeanPropertyMeta> pSet = getProperties(); 267 268 @Override /* Overridden from Set */ 269 public Iterator<Map.Entry<String,Object>> iterator() { 270 271 // Construct our own anonymous iterator that uses iterators against the meta.getProperties() 272 // map to maintain position. This prevents us from having to construct any of our own 273 // collection objects. 274 return new Iterator<>() { 275 276 final Iterator<BeanPropertyMeta> pIterator = pSet.iterator(); 277 278 @Override /* Overridden from Iterator */ 279 public boolean hasNext() { 280 return pIterator.hasNext(); 281 } 282 283 @Override /* Overridden from Iterator */ 284 public Map.Entry<String,Object> next() { 285 return new BeanMapEntry(BeanMap.this, pIterator.next(), null); 286 } 287 288 @Override /* Overridden from Iterator */ 289 public void remove() { 290 throw uoex("Cannot remove item from iterator."); 291 } 292 }; 293 } 294 295 @Override /* Overridden from Set */ 296 public int size() { 297 return pSet.size(); 298 } 299 }; 300 } 301 302 /** 303 * Performs an action on each property in this bean map. 304 * 305 * <p> 306 * If a {@link BeanSession} is attached and has an active view, only properties that are visible under 307 * that view (as determined by {@link BeanSession#isPropertyInActiveView(BeanPropertyMeta)}) are visited. 308 * 309 * @param filter The filter to apply to properties. 310 * @param action The action. 311 * @return This object. 312 */ 313 public BeanMap<T> forEachProperty(Predicate<BeanPropertyMeta> filter, Consumer<BeanPropertyMeta> action) { 314 for (var p : meta.getPropertyArray()) 315 if (filter.test(p) && isInActiveView(p)) 316 action.accept(p); 317 return this; 318 } 319 320 /** 321 * Returns <jk>true</jk> if the property is visible under the active view on this bean map's session. 322 * 323 * <p> 324 * When no session is attached, or no active view is set, all properties are visible. 325 * 326 * @param pMeta The property to test. 327 * @return <jk>true</jk> if the property is in-view. 328 */ 329 protected boolean isInActiveView(BeanPropertyMeta pMeta) { 330 return session == null || session.isPropertyInActiveView(pMeta); 331 } 332 333 /** 334 * Invokes all the getters on this bean and consumes the results. 335 * 336 * @param valueFilter Filter to apply to value before applying action. 337 * @param action The action to perform. 338 * @return The list of all bean property values. 339 */ 340 public BeanMap<T> forEachValue(Predicate<Object> valueFilter, BeanPropertyConsumer action) { 341 342 // Normal bean. 343 if (meta.getDynaProperty() == null) { 344 forEachProperty(BeanPropertyMeta::canRead, bpm -> { 345 try { 346 var val = bpm.get(this, null); 347 if (valueFilter.test(val)) 348 action.apply(bpm, bpm.getName(), val, null); 349 } catch (Exception t) { 350 action.apply(bpm, bpm.getName(), null, t); 351 } 352 }); 353 354 // Bean with dyna properties. 355 } else { 356 Map<String,BeanPropertyValue> actions = (meta.isUnsortedProperties() ? map() : sortedMap()); 357 358 forEachProperty(x -> ! x.isDyna() && x.canRead(), bpm -> { 359 try { 360 actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), bpm.get(this, null), null)); 361 } catch (Exception t) { 362 actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), null, t)); 363 } 364 }); 365 366 forEachProperty(x -> x.isDyna() && x.canRead(), bpm -> { 367 Map<String,Object> dynaMap; 368 try { 369 dynaMap = bpm.getDynaMap(bean); 370 } catch (Exception e) { 371 actions.put(bpm.getName(), new BeanPropertyValue(bpm, bpm.getName(), null, e)); 372 return; 373 } 374 if (nn(dynaMap)) { 375 dynaMap.forEach((k, v) -> { 376 try { 377 var val = bpm.get(this, k); 378 actions.put(k, new BeanPropertyValue(bpm, k, val, null)); 379 } catch (Exception e) { 380 actions.put(k, new BeanPropertyValue(bpm, k, null, e)); 381 } 382 }); 383 } 384 }); 385 386 actions.forEach((k, v) -> { 387 if (v.getThrown() != null || valueFilter.test(v.getValue())) 388 action.apply(v.getMeta(), v.getName(), v.getValue(), v.getThrown()); 389 }); 390 } 391 392 return this; 393 } 394 395 /** 396 * Gets a property on the bean. 397 * 398 * <p> 399 * If an {@code ObjectSwap}-style transform is associated with this bean property or bean property type class, then this method 400 * will return the transformed value. 401 * For example, if the bean property type class is a {@link Date} and the bean property has the 402 * {@code DateFormat.ISO_INSTANT} setting associated with it through the 403 * {@code @Swap(value)} annotation, this method will return a String containing an 404 * ISO8601 date-time string value. 405 * 406 * <h5 class='section'>Example:</h5> 407 * <p class='bjava'> 408 * <jc>// Construct a bean with a 'birthDate' Date field</jc> 409 * Person <jv>person</jv> = <jk>new</jk> Person(); 410 * <jv>person</jv>.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6)); 411 * 412 * <jc>// Create a marshalling context and add the ISO8601 date-time swap</jc> 413 * MarshallingContext <jv>marshallingContext</jv> = MarshallingContext.<jsm>create</jsm>().dateFormat(DateFormat.<jsf>ISO_INSTANT</jsf>).build(); 414 * 415 * <jc>// Wrap our bean in a bean map</jc> 416 * BeanMap<Person> <jv>beanMap</jv> = <jv>marshallingContext</jv>.toBeanMap(<jv>person</jv>); 417 * 418 * <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc> 419 * String <jv>birthDate</jv> = <jv>beanMap</jv>.get(<js>"birthDate"</js>); 420 * </p> 421 * 422 * @param property The name of the property to get. 423 * @return The property value. 424 * @throws RuntimeException if any of the following occur. 425 * <ol> 426 * <li>BeanMapEntry does not exist on the underlying object. 427 * <li>Security settings prevent access to the underlying object getter method. 428 * <li>An exception occurred inside the getter method. 429 * </ol> 430 */ 431 @Override /* Overridden from Map */ 432 public Object get(Object property) { 433 var pName = s(property); 434 var p = getPropertyMeta(pName); 435 if (p == null) 436 return meta.onReadProperty(this.bean, pName, null); 437 return p.get(this, pName); 438 } 439 440 /** 441 * Same as {@link #get(Object)} but casts the value to the specific type. 442 * 443 * @param <T2> The type to cast to. 444 * @param property The name of the property to get. 445 * @param c The type to cast to. 446 * @return The property value. 447 * @throws RuntimeException if any of the following occur. 448 * <ol> 449 * <li>BeanMapEntry does not exist on the underlying object. 450 * <li>Security settings prevent access to the underlying object getter method. 451 * <li>An exception occurred inside the getter method. 452 * </ol> 453 * @throws ClassCastException if property is not the specified type. 454 */ 455 public <T2> T2 get(String property, Class<T2> c) { 456 var pName = s(property); 457 var p = getPropertyMeta(pName); 458 if (p == null) 459 return (T2)meta.onReadProperty(this.bean, pName, null); 460 return (T2)p.get(this, pName); 461 } 462 463 /** 464 * Returns the wrapped bean object. 465 * 466 * <p> 467 * Triggers bean creation if bean has read-only properties set through a constructor defined by the 468 * {@link BeanCtor @BeanCtor} annotation. 469 * 470 * <p> 471 * The post-creation Optional<X> initialization step (which seeds null {@link Optional} properties with 472 * {@link BeanInfo#getOptionalDefault()}) is skipped for properties built via the bean-modeling-only path 473 * ({@link BeanMeta#of(Class, BeanConfigContext)}) because per-property type metadata is unavailable; 474 * those properties are left untouched and any {@link Optional}-typed field stays at its constructor-assigned 475 * value. 476 * 477 * @return The inner bean object. 478 */ 479 public T getBean() { 480 T b = getBean(true); 481 482 // If we have any arrays that need to be constructed, do it now. 483 if (nn(arrayPropertyCache)) { 484 arrayPropertyCache.forEach((k, v) -> { 485 try { 486 getPropertyMeta(k).setArray(b, v); 487 } catch (Exception e1) { 488 throw toRex(e1); 489 } 490 }); 491 arrayPropertyCache = null; 492 } 493 494 // Initialize any null Optional<X> fields. Skip properties whose ClassMeta is unavailable 495 // (bean-modeling-only path — Optional handling is a marshalling concern that requires type metadata). 496 meta.getProperties().forEach((k,v) -> { 497 var cm = v.getBeanInfo(); 498 if (nn(cm) && cm.isOptional() && v.get(this, k) == null) 499 v.set(this, k, cm.getOptionalDefault()); 500 }); 501 502 // Do the same for hidden fields. 503 meta.getHiddenProperties().forEach((k, v) -> { 504 var cm = v.getBeanInfo(); 505 if (nn(cm) && cm.isOptional() && v.get(this, k) == null) 506 v.set(this, k, cm.getOptionalDefault()); 507 }); 508 509 return b; 510 } 511 512 /** 513 * Returns the wrapped bean object. 514 * 515 * <p> 516 * If <c>create</c> is <jk>false</jk>, then this method may return <jk>null</jk> if the bean has read-only 517 * properties set through a constructor defined by the {@link BeanCtor @BeanCtor} annotation. 518 * 519 * <p> 520 * This method does NOT always return the bean in it's final state. 521 * Array properties temporary stored as ArrayLists are not finalized until the {@link #getBean()} method is called. 522 * 523 * @param create If bean hasn't been instantiated yet, then instantiate it. 524 * @return The inner bean object. 525 */ 526 public T getBean(boolean create) { 527 /** If this is a read-only bean, then we need to create it. */ 528 if (bean == null && create && ine(meta.getConstructorArgs())) { 529 var props = meta.getConstructorArgs(); 530 var c = meta.getConstructor(); 531 var args = new Object[props.size()]; 532 for (var i = 0; i < props.size(); i++) { 533 var propName = props.get(i); 534 var rawVal = propertyCache.remove(propName); 535 // Convert value to expected property type if it's not already the correct type. 536 // Uses the same logic as MarshallingSession.convertToMemberType's early-return check: 537 // collections/maps with specific element/value types always need conversion. 538 if (rawVal != null) { 539 var pm = getPropertyMeta(propName); 540 if (pm != null) { 541 var cm = pm.getBeanInfo(); 542 var needsConversion = !cm.inner().isInstance(rawVal) 543 || (cm.isCollection() && !cm.getElementType().isObject()) 544 || (cm.isMap() && !cm.getValueType().is(Object.class)); 545 if (needsConversion) 546 rawVal = session.convertToType(rawVal, cm); 547 } 548 } 549 args[i] = rawVal; 550 } 551 try { 552 bean = c.<T>newInstance(args); 553 propertyCache.forEach(this::put); 554 propertyCache = null; 555 } catch (IllegalArgumentException e) { 556 throw brex(e, meta.getBeanInfo().inner(), "IllegalArgumentException occurred on call to class constructor '%s' with argument types '%s'", c.getNameSimple(), 557 Arrays.toString(getClasses(args))); 558 } catch (Exception e) { 559 throw brex(e); 560 } 561 } 562 return bean; 563 } 564 565 /** 566 * Returns the bean session that created this bean map. 567 * 568 * <p> 569 * The returned value is the bean-modeling SPI seam. Marshalling-side callers needing the concrete 570 * marshalling-session implementation can cast — every session wired in via 571 * {@link #setBeanSession(BeanSession)} on the marshalling-side path is currently a 572 * {@code MarshallingSession}. 573 * 574 * @return The bean session that created this bean map. 575 */ 576 public final BeanSession getBeanSession() { return session; } 577 578 /** 579 * Returns the {@link BeanInfo} of the wrapped bean. 580 * 581 * @return The class type of the wrapped bean. 582 */ 583 @Override /* Overridden from Delegate */ 584 public BeanInfo<T> getBeanInfo() { return this.meta.getBeanInfo(); } 585 586 /** 587 * Returns the metadata associated with this bean map. 588 * 589 * @return The metadata associated with this bean map. 590 */ 591 public BeanMeta<T> getMeta() { return meta; } 592 593 /** 594 * Extracts the specified field values from this bean and returns it as a simple Map. 595 * 596 * <p> 597 * The returned map is a <i>live</i> view over this bean map: each entry's {@code getValue} reads through to 598 * {@code BeanMap.get} and {@code setValue} writes through to {@code BeanMap.put}. Unknown {@code fields} 599 * (keys not present in this bean map) are silently skipped. 600 * 601 * @param fields The fields to extract. 602 * @return 603 * A new map with fields as key-value pairs. 604 * <br>Note that modifying the values in this map will also modify the underlying bean. 605 */ 606 @SuppressWarnings({ 607 "java:S9391" // Hot path; avoids stream allocation. 608 }) 609 public Map<String,Object> getProperties(String...fields) { 610 Map<String,Object> thisMap = this; 611 var entries = new ArrayList<Map.Entry<String,Object>>(fields.length); 612 for (var k : fields) { 613 if (thisMap.containsKey(k)) { 614 entries.add(new Map.Entry<>() { 615 @Override public String getKey() { return k; } 616 @Override public Object getValue() { return thisMap.get(k); } 617 @Override public Object setValue(Object v) { return thisMap.put(k, v); } 618 }); 619 } 620 } 621 return new AbstractMap<>() { 622 @Override public Set<Map.Entry<String,Object>> entrySet() { 623 return new AbstractSet<>() { 624 @Override public Iterator<Map.Entry<String,Object>> iterator() { return entries.iterator(); } 625 @Override public int size() { return entries.size(); } 626 }; 627 } 628 }; 629 } 630 631 /** 632 * Returns the specified property on this bean map. 633 * 634 * <p> 635 * Allows you to get and set an individual property on a bean without having a handle to the bean itself by using 636 * the {@link BeanMapEntry#getValue()} and {@link BeanMapEntry#setValue(Object)} methods. 637 * 638 * <p> 639 * This method can also be used to get metadata on a property by calling the {@link BeanMapEntry#getMeta()} method. 640 * 641 * @param propertyName The name of the property to look up. 642 * @return The bean property, or null if the bean has no such property. 643 */ 644 public BeanMapEntry getProperty(String propertyName) { 645 var p = getPropertyMeta(propertyName); 646 if (p == null) 647 return null; 648 return new BeanMapEntry(this, p, propertyName); 649 } 650 651 /** 652 * Returns the metadata on the specified property. 653 * 654 * <p> 655 * Returns <jk>null</jk> if the property does not exist on the bean <em>or</em> if the property exists but is 656 * excluded by the current active view. The latter allows the caller (e.g. {@link #put(String, Object)}) to 657 * route out-of-view input through the existing unknown/ignored-property path governed by 658 * {@link BeanConfigContext#isIgnoreUnknownBeanProperties()}. 659 * 660 * @param propertyName The name of the bean property. 661 * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist or is out of view. 662 */ 663 public BeanPropertyMeta getPropertyMeta(String propertyName) { 664 var p = meta.getPropertyMeta(propertyName); 665 if (p != null && ! isInActiveView(p)) 666 return null; 667 return p; 668 } 669 670 /** 671 * Same as {@link #get(Object)} except bypasses any {@code ObjectSwap} associated with the bean property or 672 * {@link BeanFilter} associated with the bean class. 673 * 674 * @param property The name of the property to get. 675 * @return The raw property value. 676 */ 677 public Object getRaw(Object property) { 678 var pName = s(property); 679 var p = getPropertyMeta(pName); 680 if (p == null) 681 return null; 682 return p.getRaw(this, pName); 683 } 684 685 /** 686 * Returns the names of all properties associated with the bean. 687 * 688 * <p> 689 * The returned set is unmodifiable. 690 */ 691 @Override /* Overridden from Map */ 692 public Set<String> keySet() { 693 if (meta.getDynaProperty() == null) 694 return meta.getProperties().keySet(); 695 Set<String> l = set(); 696 meta.getProperties().forEach((k, v) -> { 697 if (neq(k, "*")) 698 l.add(k); 699 }); 700 try { 701 l.addAll(meta.getDynaProperty().getDynaMap(bean).keySet()); 702 } catch (Exception e) { 703 throw new BeanRuntimeException(e); 704 } 705 return l; 706 } 707 708 /** 709 * Convenience method for loading this map with the contents of the specified map. 710 * 711 * <p> 712 * Identical to {@link #putAll(Map)} except as a fluent-style method. 713 * 714 * @param entries The map containing the entries to add to this map. 715 * @return This object. 716 */ 717 @SuppressWarnings({ 718 "rawtypes" // Raw types necessary for generic type handling 719 }) 720 public BeanMap<T> load(Map entries) { 721 putAll(entries); 722 return this; 723 } 724 725 /** 726 * Sets a property on the bean. 727 * 728 * <p> 729 * If there is a {@code ObjectSwap} associated with this bean property or bean property type class, then you must pass in a transformed value. 730 * For example, if the bean property type class is a {@link Date} and the bean property has the 731 * {@code DateFormat.ISO_INSTANT} setting associated with it through the 732 * {@code @Swap(value)} annotation, the value being passed in must be 733 * a String containing an ISO8601 date-time string value. 734 * 735 * <h5 class='section'>Example:</h5> 736 * <p class='bjava'> 737 * <jc>// Construct a bean with a 'birthDate' Date field</jc> 738 * Person <jv>person</jv> = <jk>new</jk> Person(); 739 * 740 * <jc>// Create a marshalling context and add the ISO8601 date-time swap</jc> 741 * MarshallingContext <jv>marshallingContext</jv> = MarshallingContext.<jsm>create</jsm>().dateFormat(DateFormat.<jsf>ISO_INSTANT</jsf>).build(); 742 * 743 * <jc>// Wrap our bean in a bean map</jc> 744 * BeanMap<Person> <jv>beanMap</jv> = <jv>marshallingContext</jv>.toBeanMap(<jv>person</jv>); 745 * 746 * <jc>// Set the field</jc> 747 * <jv>beanMap</jv>.put(<js>"birthDate"</js>, <js>"'1901-03-03T04:05:06-5000'"</js>); 748 * </p> 749 * 750 * @param property The name of the property to set. 751 * @param value The value to set the property to. Can be <jk>null</jk> to set the property to <jk>null</jk>. 752 * @return 753 * If the {@code beanMapPutReturnsOldValue} setting is <jk>true</jk> on the session's bean configuration, 754 * then the old value of the property is returned. 755 * Otherwise, this method always returns <jk>null</jk>. 756 * @throws 757 * RuntimeException if any of the following occur. 758 * <ul> 759 * <li>BeanMapEntry does not exist on the underlying object. 760 * <li>Security settings prevent access to the underlying object setter method. 761 * <li>An exception occurred inside the setter method. 762 * </ul> 763 */ 764 @Override /* Overridden from Map */ 765 public Object put(String property, Object value) { 766 var p = getPropertyMeta(property); 767 if (p == null) { 768 if (meta.getConfig().isIgnoreUnknownBeanProperties() || property.equals(typePropertyName)) 769 return meta.onWriteProperty(bean, property, null); 770 771 p = getPropertyMeta("*"); 772 if (p == null) 773 throw brex(meta.getClassInfo(), "Bean property '%s' not found.", property); 774 } 775 return p.set(this, property, value); 776 } 777 778 /** 779 * Given a string containing variables of the form <c>"{property}"</c>, replaces those variables with property 780 * values in this bean. 781 * 782 * @param s The string containing variables. 783 * @return A new string with variables replaced, or the same string if no variables were found. 784 */ 785 public String resolveVars(String s) { 786 return formatNamed(s, this); 787 } 788 789 /** 790 * Returns a simple collection of properties for this bean map. 791 * 792 * @return A simple collection of properties for this bean map. 793 */ 794 protected Collection<BeanPropertyMeta> getProperties() { return meta.getProperties().values(); } 795 796 void setBean(Object bean) { this.bean = (T)bean; } 797 798 /** 799 * Compares the specified object with this map for equality. 800 * 801 * <p> 802 * Returns <jk>true</jk> if the given object is also a map and the two maps represent the same 803 * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent the same mappings if 804 * <c>m1.entrySet().equals(m2.entrySet())</c>. 805 * 806 * <p> 807 * This implementation compares the entry sets of the two maps. 808 * 809 * @param o Object to be compared for equality with this map. 810 * @return <jk>true</jk> if the specified object is equal to this map. 811 */ 812 @Override 813 public boolean equals(Object o) { 814 return o instanceof Map<?,?> o2 && eq(this, o2, (x, y) -> x.entrySet().equals(y.entrySet())); 815 } 816 817 /** 818 * Returns the hash code value for this map. 819 * 820 * <p> 821 * The hash code of a map is defined to be the sum of the hash codes of each entry in the map's 822 * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> implies that 823 * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and <c>m2</c>, as required 824 * by the general contract of {@link Object#hashCode()}. 825 * 826 * <p> 827 * This implementation computes the hash code from the entry set. 828 * 829 * @return The hash code value for this map. 830 */ 831 @Override 832 public int hashCode() { 833 return entrySet().hashCode(); 834 } 835}