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.commons.chain.impl; 018 019import java.beans.IntrospectionException; 020import java.beans.Introspector; 021import java.beans.PropertyDescriptor; 022import java.io.Serializable; 023import java.lang.reflect.Method; 024import java.util.AbstractCollection; 025import java.util.AbstractSet; 026import java.util.Collection; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.Map; 030import java.util.Objects; 031import java.util.Set; 032 033import org.apache.commons.chain.Context; 034 035/** 036 * Convenience base class for {@link Context} implementations. 037 * 038 * <p>In addition to the minimal functionality required by the {@link Context} 039 * interface, this class implements the recommended support for 040 * <em>Attribute-Property Transparency</em>. This is implemented by 041 * analyzing the available JavaBeans properties of this class (or its 042 * subclass), exposes them as key-value pairs in the {@code Map}, 043 * with the key being the name of the property itself.</p> 044 * 045 * <p><strong>IMPLEMENTATION NOTE</strong> - Because {@code empty} is a 046 * read-only property defined by the {@code Map} interface, it may not 047 * be utilized as an attribute key or property name.</p> 048 * 049 * @author Craig R. McClanahan 050 * @version $Revision$ $Date$ 051 */ 052public class ContextBase extends HashMap<String, Object> implements Context { 053 private static final long serialVersionUID = -2482145117370708259L; 054 055 // ------------------------------------------------------ Static Variables 056 057 /** 058 * Distinguished singleton value that is stored in the map for each 059 * key that is actually a property. This value is used to ensure that 060 * {@code equals()} comparisons will always fail. 061 */ 062 private static final Object SINGLETON = new Serializable() { 063 private static final long serialVersionUID = -6023767081282668587L; 064 065 @Override 066 public boolean equals(Object object) { 067 return false; 068 } 069 070 @Override 071 public int hashCode() { 072 return super.hashCode(); 073 } 074 }; 075 076 // ------------------------------------------------------ Instance Variables 077 078 // NOTE - PropertyDescriptor instances are not Serializable, so the 079 // following variables must be declared as transient. When a ContextBase 080 // instance is deserialized, the no-arguments constructor is called, 081 // and the initialize() method called there will repopulate them. 082 // Therefore, no special restoration activity is required. 083 084 /** 085 * The {@code PropertyDescriptor}s for all JavaBeans properties 086 * of this {@link Context} implementation class as an array. 087 */ 088 private final transient PropertyDescriptor[] pds = getPropertyDescriptors(); 089 090 /** 091 * The {@code PropertyDescriptor}s for all JavaBeans properties 092 * of this {@link Context} implementation class, keyed by property name. 093 * This collection is allocated only if there are any JavaBeans 094 * properties. 095 */ 096 private final transient Map<String, PropertyDescriptor> descriptors = getMapDescriptors(); 097 098 // ------------------------------------------------------------ Constructors 099 100 /** 101 * Default, no argument constructor. 102 */ 103 public ContextBase() { 104 } 105 106 /** 107 * Initialize the contents of this {@link Context} by copying the 108 * values from the specified {@code Map}. Any keys in {@code map} 109 * that correspond to local properties will cause the setter method for 110 * that property to be called. 111 * 112 * @param map Map whose key-value pairs are added 113 * 114 * @throws IllegalArgumentException if an exception is thrown 115 * writing a local property value. 116 * @throws UnsupportedOperationException if a local property does not 117 * have a write method. 118 */ 119 public ContextBase(Map<String, Object> map) { 120 super(map); 121 putAll(map); 122 } 123 124 // ------------------------------------------------------------- Map Methods 125 126 /** 127 * Override the default {@code Map} behavior to clear all keys and 128 * values except those corresponding to JavaBeans properties. 129 */ 130 @Override 131 public void clear() { 132 if (descriptors == null) { 133 super.clear(); 134 } else { 135 Iterator<String> keys = keySet().iterator(); 136 while (keys.hasNext()) { 137 String key = keys.next(); 138 if (!descriptors.containsKey(key)) { 139 keys.remove(); 140 } 141 } 142 } 143 } 144 145 /** 146 * Override the default {@code Map} behavior to return 147 * {@code true} if the specified value is present in either the 148 * underlying {@code Map} or one of the local property values. 149 * 150 * @param value the value look for in the context. 151 * 152 * @return {@code true} if found in this context otherwise 153 * {@code false}. 154 * 155 * @throws IllegalArgumentException if a property getter 156 * throws an exception 157 */ 158 @Override 159 public boolean containsValue(Object value) { 160 boolean b = super.containsValue(value); 161 162 // Case 1 -- no local properties 163 if (descriptors == null) { 164 return b; 165 } 166 167 // Case 2 -- value found in the underlying Map 168 if (b) { 169 return true; 170 } 171 172 // Case 3 -- check the values of our readable properties 173 for (PropertyDescriptor pd : pds) { 174 if (pd.getReadMethod() != null) { 175 Object prop = readProperty(pd); 176 if (value == null) { 177 if (prop == null) { 178 return true; 179 } 180 } else if (value.equals(prop)) { 181 return true; 182 } 183 } 184 } 185 return false; 186 } 187 188 /** 189 * Override the default {@code Map} behavior to return a 190 * {@code Set} that meets the specified default behavior except 191 * for attempts to remove the key for a property of the {@link Context} 192 * implementation class, which will throw 193 * {@code UnsupportedOperationException}. 194 * 195 * @return Set of entries in the Context. 196 */ 197 @Override 198 public Set<Map.Entry<String, Object>> entrySet() { 199 return new EntrySetImpl(); 200 } 201 202 /** 203 * Override the default {@code Map} behavior to return the value 204 * of a local property if the specified key matches a local property name. 205 * 206 * <p><strong>IMPLEMENTATION NOTE</strong> - If the specified 207 * {@code key} identifies a write-only property, {@code null} 208 * will arbitrarily be returned, in order to avoid difficulties implementing 209 * the contracts of the {@code Map} interface.</p> 210 * 211 * @param key Key of the value to be returned 212 * 213 * @return The value for the specified key. 214 * 215 * @throws IllegalArgumentException if an exception is thrown 216 * reading this local property value. 217 * @throws UnsupportedOperationException if this local property does not 218 * have a read method. 219 */ 220 @Override 221 public Object get(Object key) { 222 // Case 1 -- no local properties 223 if (descriptors == null) { 224 return super.get(key); 225 } 226 227 // Case 2 -- this is a local property 228 if (key != null) { 229 final PropertyDescriptor descriptor = descriptors.get(key); 230 if (descriptor != null) { 231 if (descriptor.getReadMethod() != null) { 232 return readProperty(descriptor); 233 } else { 234 return null; 235 } 236 } 237 } 238 239 // Case 3 -- retrieve value from our underlying Map 240 return super.get(key); 241 } 242 243 /** 244 * Override the default {@code Map} behavior to return 245 * {@code true} if the underlying {@code Map} only contains 246 * key-value pairs for local properties (if any). 247 * 248 * @return {@code true} if this Context is empty, otherwise 249 * {@code false}. 250 */ 251 @Override 252 public boolean isEmpty() { 253 // Case 1 -- no local properties 254 if (descriptors == null) { 255 return super.isEmpty(); 256 } 257 258 // Case 2 -- compare key count to property count 259 return super.size() <= descriptors.size(); 260 } 261 262 /** 263 * Override the default {@code Map} behavior to return a 264 * {@code Set} that meets the specified default behavior except 265 * for attempts to remove the key for a property of the {@link Context} 266 * implementation class, which will throw 267 * {@code UnsupportedOperationException}. 268 * 269 * @return The set of keys for objects in this Context. 270 */ 271 public Set<String> keySet() { 272 return super.keySet(); 273 } 274 275 /** 276 * Override the default {@code Map} behavior to set the value of a 277 * local property if the specified key matches a local property name. 278 * 279 * @param key Key of the value to be stored or replaced 280 * @param value New value to be stored 281 * 282 * @return The value added to the Context. 283 * 284 * @throws IllegalArgumentException if an exception is thrown 285 * reading or writing this local property value. 286 * @throws UnsupportedOperationException if this local property does not 287 * have both a read method and a write method 288 */ 289 @Override 290 public Object put(String key, Object value) { 291 // Case 1 -- no local properties 292 if (descriptors == null) { 293 return super.put(key, value); 294 } 295 296 // Case 2 -- this is a local property 297 if (key != null) { 298 final PropertyDescriptor descriptor = descriptors.get(key); 299 if (descriptor != null) { 300 Object previous = null; 301 if (descriptor.getReadMethod() != null) { 302 previous = readProperty(descriptor); 303 } 304 writeProperty(descriptor, value); 305 return previous; 306 } 307 } 308 309 // Case 3 -- store or replace value in our underlying map 310 return super.put(key, value); 311 } 312 313 /** 314 * Override the default {@code Map} behavior to call the 315 * {@code put()} method individually for each key-value pair 316 * in the specified {@code Map}. 317 * 318 * @param map {@code Map} containing key-value pairs to store 319 * (or replace) 320 * 321 * @throws IllegalArgumentException if an exception is thrown 322 * reading or writing a local property value. 323 * @throws UnsupportedOperationException if a local property does not 324 * have both a read method and a write method 325 */ 326 @Override 327 public void putAll(Map<? extends String, ? extends Object> map) { 328 map.forEach(this::put); 329 } 330 331 /** 332 * Override the default {@code Map} behavior to throw 333 * {@code UnsupportedOperationException} on any attempt to 334 * remove a key that is the name of a local property. 335 * 336 * @param key Key to be removed 337 * 338 * @return The value removed from the Context. 339 * 340 * @throws UnsupportedOperationException if the specified 341 * {@code key} matches the name of a local property 342 */ 343 @Override 344 public Object remove(Object key) { 345 // Case 1 -- no local properties 346 if (descriptors == null) { 347 return super.remove(key); 348 } 349 350 // Case 2 -- this is a local property 351 if (key != null) { 352 PropertyDescriptor descriptor = descriptors.get(key); 353 if (descriptor != null) { 354 throw new UnsupportedOperationException("Local property '" + key + "' cannot be removed"); 355 } 356 } 357 358 // Case 3 -- remove from underlying Map 359 return super.remove(key); 360 } 361 362 /** 363 * Override the default {@code Map} behavior to return a 364 * {@code Collection} that meets the specified default behavior except 365 * for attempts to remove the key for a property of the {@link Context} 366 * implementation class, which will throw 367 * {@code UnsupportedOperationException}. 368 * 369 * @return The collection of values in this Context. 370 */ 371 @Override 372 public Collection<Object> values() { 373 return new ValuesImpl(); 374 } 375 376 // --------------------------------------------------------- Private Methods 377 378 /** 379 * Return an {@code Iterator} over the set of {@code Map.Entry} 380 * objects representing our key-value pairs. 381 * 382 * @return an {@code Iterator} over the set of {@code Map.Entry} objects 383 */ 384 private Iterator<Map.Entry<String, Object>> entriesIterator() { 385 return new EntrySetIterator(); 386 } 387 388 /** 389 * Return a {@code Map.Entry} for the specified key value, if it 390 * is present; otherwise, return {@code null}. 391 * 392 * @param key Attribute key or property name 393 * 394 * @return a {@code Map.Entry} for the specified key value, if it 395 * is present; otherwise, return {@code null} 396 */ 397 private Map.Entry<String, Object> entry(Object key) { 398 if (containsKey(key)) { 399 return new MapEntryImpl(key.toString(), get(key)); 400 } else { 401 return null; 402 } 403 } 404 405 /** 406 * Get and return the value for the specified property. 407 * 408 * @param descriptor {@code PropertyDescriptor} for the 409 * specified property 410 * 411 * @return the value of the specified property 412 * 413 * @throws IllegalArgumentException if an exception is thrown 414 * reading this local property value. 415 * @throws UnsupportedOperationException if this local property does not 416 * have a read method. 417 */ 418 private Object readProperty(PropertyDescriptor descriptor) { 419 try { 420 Method method = descriptor.getReadMethod(); 421 if (method == null) { 422 throw new UnsupportedOperationException("Property '" 423 + descriptor.getName() + "' is not readable"); 424 } 425 return method.invoke(this); 426 } catch (Exception e) { 427 throw new UnsupportedOperationException("Exception reading property '" 428 + descriptor.getName() + "': " + e.getMessage()); 429 } 430 } 431 432 /** 433 * Remove the specified key-value pair, if it exists, and return 434 * {@code true}. If this pair does not exist, return {@code false}. 435 * 436 * @param entry Key-value pair to be removed 437 * 438 * @return if the specified key-value pair is removed, return 439 * {@code true}, otherwise {@code false} 440 * 441 * @throws UnsupportedOperationException if the specified key 442 * identifies a property instead of an attribute. 443 */ 444 private boolean remove(Map.Entry<?, ?> entry) { 445 Map.Entry<String, Object> actual = entry(entry.getKey()); 446 if (actual == null) { 447 return false; 448 } else if (entry.equals(actual)) { 449 remove(entry.getKey()); 450 return true; 451 } else { 452 return false; 453 } 454 } 455 456 /** 457 * Return an {@code Iterator} over the set of values in this 458 * {@code Map}. 459 * 460 * @return an {@code Iterator} over the set of values in this 461 * {@code Map} 462 */ 463 private Iterator<Object> valuesIterator() { 464 return new ValuesIterator(); 465 } 466 467 /** 468 * Set the value for the specified property. 469 * 470 * @param descriptor {@code PropertyDescriptor} for the 471 * specified property 472 * @param value The new value for this property (must be of the 473 * correct type) 474 * 475 * @throws IllegalArgumentException if an exception is thrown 476 * writing this local property value. 477 * @throws UnsupportedOperationException if this local property does not 478 * have a write method. 479 */ 480 private void writeProperty(PropertyDescriptor descriptor, Object value) { 481 try { 482 Method method = descriptor.getWriteMethod(); 483 if (method == null) { 484 throw new UnsupportedOperationException("Property '" + descriptor.getName() 485 + "' is not writeable"); 486 } 487 method.invoke(this, value); 488 } catch (Exception e) { 489 throw new UnsupportedOperationException("Exception writing property '" 490 + descriptor.getName() + "': " + e.getMessage()); 491 } 492 } 493 494 /** 495 * Returns descriptors for all properties of the bean. 496 * 497 * @return descriptors for all properties of the bean 498 * or an empty array if an problem occurs 499 */ 500 private PropertyDescriptor[] getPropertyDescriptors() { 501 // Retrieve the set of property descriptors for this Context class 502 try { 503 return Introspector.getBeanInfo(getClass()).getPropertyDescriptors(); 504 } catch (IntrospectionException e) { 505 return new PropertyDescriptor[0]; // Should never happen 506 } 507 } 508 509 /** 510 * The {@code PropertyDescriptor}s for all JavaBeans properties 511 * of this {@link Context} implementation class, keyed by property 512 * name. This collection is allocated only if there are any JavaBeans 513 * properties. 514 * 515 * @return {@code PropertyDescriptor}s for all JavaBeans properties 516 * as an collection or {@code null} if there are no JavaBeans 517 * properties 518 */ 519 private Map<String, PropertyDescriptor> getMapDescriptors() { 520 Map<String, PropertyDescriptor> ret = new HashMap<>(); 521 522 // Initialize the underlying Map contents 523 for (PropertyDescriptor pd : pds) { 524 String name = pd.getName(); 525 526 // Add descriptor (ignoring getClass() and isEmpty()) 527 if (!("class".equals(name) || "empty".equals(name))) { 528 ret.put(name, pd); 529 super.put(name, SINGLETON); 530 } 531 } 532 533 return ret.isEmpty() ? null : ret; 534 } 535 536 // --------------------------------------------------------- Private Classes 537 538 /** 539 * Private implementation of {@code Set} that implements the 540 * semantics required for the value returned by {@code entrySet()}. 541 */ 542 private final class EntrySetImpl extends AbstractSet<Map.Entry<String, Object>> { 543 544 @Override 545 public void clear() { 546 ContextBase.this.clear(); 547 } 548 549 @Override 550 public boolean contains(Object obj) { 551 if (!(obj instanceof Map.Entry)) { 552 return false; 553 } 554 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj; 555 Map.Entry<String, Object> actual = ContextBase.this.entry(entry.getKey()); 556 if (actual != null) { 557 return actual.equals(entry); 558 } else { 559 return false; 560 } 561 } 562 563 @Override 564 public boolean isEmpty() { 565 return ContextBase.this.isEmpty(); 566 } 567 568 @Override 569 public Iterator<Map.Entry<String, Object>> iterator() { 570 return ContextBase.this.entriesIterator(); 571 } 572 573 @Override 574 public boolean remove(Object obj) { 575 if (obj instanceof Map.Entry) { 576 return ContextBase.this.remove((Map.Entry<?, ?>) obj); 577 } else { 578 return false; 579 } 580 } 581 582 @Override 583 public int size() { 584 return ContextBase.this.size(); 585 } 586 } 587 588 /** 589 * Private implementation of {@code Iterator} for the 590 * {@code Set} returned by {@code entrySet()}. 591 */ 592 private final class EntrySetIterator implements Iterator<Map.Entry<String, Object>> { 593 594 private Map.Entry<String, Object> entry = null; 595 private Iterator<String> keys = ContextBase.this.keySet().iterator(); 596 597 @Override 598 public boolean hasNext() { 599 return keys.hasNext(); 600 } 601 602 @Override 603 public Map.Entry<String, Object> next() { 604 entry = ContextBase.this.entry(keys.next()); 605 return entry; 606 } 607 608 @Override 609 public void remove() { 610 ContextBase.this.remove(entry); 611 } 612 } 613 614 /** 615 * Private implementation of {@code Map.Entry} for each item in 616 * {@code EntrySetImpl}. 617 */ 618 private class MapEntryImpl implements Map.Entry<String, Object> { 619 620 private String key; 621 private Object value; 622 623 MapEntryImpl(String key, Object value) { 624 this.key = key; 625 this.value = value; 626 } 627 628 @Override 629 public boolean equals(Object obj) { 630 if (this == obj) { 631 return true; 632 } 633 if (obj == null || getClass() != obj.getClass()) { 634 return false; 635 } 636 MapEntryImpl other = (MapEntryImpl) obj; 637 if (!getEnclosingInstance().equals(other.getEnclosingInstance())) { 638 return false; 639 } 640 return Objects.equals(key, other.key) && Objects.equals(value, other.value); 641 } 642 643 @Override 644 public String getKey() { 645 return this.key; 646 } 647 648 @Override 649 public Object getValue() { 650 return this.value; 651 } 652 653 @Override 654 public int hashCode() { 655 return Objects.hashCode(key) ^ Objects.hashCode(value); 656 } 657 658 @Override 659 public Object setValue(Object value) { 660 Object previous = this.value; 661 ContextBase.this.put(this.key, value); 662 this.value = value; 663 return previous; 664 } 665 666 @Override 667 public String toString() { 668 return getKey() + "=" + getValue(); 669 } 670 671 private ContextBase getEnclosingInstance() { 672 return ContextBase.this; 673 } 674 } 675 676 /** 677 * Private implementation of {@code Collection} that implements the 678 * semantics required for the value returned by {@code values()}. 679 */ 680 private final class ValuesImpl extends AbstractCollection<Object> { 681 682 @Override 683 public void clear() { 684 ContextBase.this.clear(); 685 } 686 687 @Override 688 public boolean contains(Object obj) { 689 if (!(obj instanceof Map.Entry)) { 690 return false; 691 } 692 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj; 693 return ContextBase.this.containsValue(entry.getValue()); 694 } 695 696 @Override 697 public boolean isEmpty() { 698 return ContextBase.this.isEmpty(); 699 } 700 701 @Override 702 public Iterator<Object> iterator() { 703 return ContextBase.this.valuesIterator(); 704 } 705 706 @Override 707 public boolean remove(Object obj) { 708 if (obj instanceof Map.Entry) { 709 return ContextBase.this.remove((Map.Entry<?, ?>) obj); 710 } else { 711 return false; 712 } 713 } 714 715 @Override 716 public int size() { 717 return ContextBase.this.size(); 718 } 719 } 720 721 /** 722 * Private implementation of {@code Iterator} for the 723 * {@code Collection} returned by {@code values()}. 724 */ 725 private final class ValuesIterator implements Iterator<Object> { 726 727 private Map.Entry<String, Object> entry = null; 728 private Iterator<String> keys = ContextBase.this.keySet().iterator(); 729 730 @Override 731 public boolean hasNext() { 732 return keys.hasNext(); 733 } 734 735 @Override 736 public Object next() { 737 entry = ContextBase.this.entry(keys.next()); 738 return entry.getValue(); 739 } 740 741 @Override 742 public void remove() { 743 ContextBase.this.remove(entry); 744 } 745 } 746}