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