1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.commons.chain.impl;
17
18
19 import java.beans.IntrospectionException;
20 import java.beans.Introspector;
21 import java.beans.PropertyDescriptor;
22 import java.lang.reflect.Method;
23 import java.util.AbstractCollection;
24 import java.util.AbstractSet;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.Map;
29 import java.util.Set;
30 import java.io.Serializable;
31 import org.apache.commons.chain.Context;
32
33
34 /***
35 * <p>Convenience base class for {@link Context} implementations.</p>
36 *
37 * <p>In addition to the minimal functionality required by the {@link Context}
38 * interface, this class implements the recommended support for
39 * <em>Attribute-Property Transparency</p>. This is implemented by
40 * analyzing the available JavaBeans properties of this class (or its
41 * subclass), exposes them as key-value pairs in the <code>Map</code>,
42 * with the key being the name of the property itself.</p>
43 *
44 * <p><strong>IMPLEMENTATION NOTE</strong> - Because <code>empty</code> is a
45 * read-only property defined by the <code>Map</code> interface, it may not
46 * be utilized as an attribute key or property name.</p>
47 *
48 * @author Craig R. McClanahan
49 * @version $Revision: 412150 $ $Date: 2006-06-06 16:31:50 +0100 (Tue, 06 Jun 2006) $
50 */
51
52 public class ContextBase extends HashMap implements Context {
53
54
55
56
57
58 /***
59 * Default, no argument constructor.
60 */
61 public ContextBase() {
62
63 super();
64 initialize();
65
66 }
67
68
69 /***
70 * <p>Initialize the contents of this {@link Context} by copying the
71 * values from the specified <code>Map</code>. Any keys in <code>map</code>
72 * that correspond to local properties will cause the setter method for
73 * that property to be called.</p>
74 *
75 * @param map Map whose key-value pairs are added
76 *
77 * @exception IllegalArgumentException if an exception is thrown
78 * writing a local property value
79 * @exception UnsupportedOperationException if a local property does not
80 * have a write method.
81 */
82 public ContextBase(Map map) {
83
84 super(map);
85 initialize();
86 putAll(map);
87
88 }
89
90
91
92
93
94
95
96
97
98
99
100 /***
101 * <p>The <code>PropertyDescriptor</code>s for all JavaBeans properties
102 * of this {@link Context} implementation class, keyed by property name.
103 * This collection is allocated only if there are any JavaBeans
104 * properties.</p>
105 */
106 private transient Map descriptors = null;
107
108
109 /***
110 * <p>The same <code>PropertyDescriptor</code>s as an array.</p>
111 */
112 private transient PropertyDescriptor[] pd = null;
113
114
115 /***
116 * <p>Distinguished singleton value that is stored in the map for each
117 * key that is actually a property. This value is used to ensure that
118 * <code>equals()</code> comparisons will always fail.</p>
119 */
120 private static Object singleton;
121
122 static {
123
124 singleton = new Serializable() {
125 public boolean equals(Object object) {
126 return (false);
127 }
128 };
129
130 }
131
132
133 /***
134 * <p>Zero-length array of parameter values for calling property getters.
135 * </p>
136 */
137 private static Object[] zeroParams = new Object[0];
138
139
140
141
142
143 /***
144 * <p>Override the default <code>Map</code> behavior to clear all keys and
145 * values except those corresponding to JavaBeans properties.</p>
146 */
147 public void clear() {
148
149 if (descriptors == null) {
150 super.clear();
151 } else {
152 Iterator keys = keySet().iterator();
153 while (keys.hasNext()) {
154 Object key = keys.next();
155 if (!descriptors.containsKey(key)) {
156 keys.remove();
157 }
158 }
159 }
160
161 }
162
163
164 /***
165 * <p>Override the default <code>Map</code> behavior to return
166 * <code>true</code> if the specified value is present in either the
167 * underlying <code>Map</code> or one of the local property values.</p>
168 *
169 * @param value the value look for in the context.
170 * @return <code>true</code> if found in this context otherwise
171 * <code>false</code>.
172 * @exception IllegalArgumentException if a property getter
173 * throws an exception
174 */
175 public boolean containsValue(Object value) {
176
177
178 if (descriptors == null) {
179 return (super.containsValue(value));
180 }
181
182
183 else if (super.containsValue(value)) {
184 return (true);
185 }
186
187
188 for (int i = 0; i < pd.length; i++) {
189 if (pd[i].getReadMethod() != null) {
190 Object prop = readProperty(pd[i]);
191 if (value == null) {
192 if (prop == null) {
193 return (true);
194 }
195 } else if (value.equals(prop)) {
196 return (true);
197 }
198 }
199 }
200 return (false);
201
202 }
203
204
205 /***
206 * <p>Override the default <code>Map</code> behavior to return a
207 * <code>Set</code> that meets the specified default behavior except
208 * for attempts to remove the key for a property of the {@link Context}
209 * implementation class, which will throw
210 * <code>UnsupportedOperationException</code>.</p>
211 *
212 * @return Set of entries in the Context.
213 */
214 public Set entrySet() {
215
216 return (new EntrySetImpl());
217
218 }
219
220
221 /***
222 * <p>Override the default <code>Map</code> behavior to return the value
223 * of a local property if the specified key matches a local property name.
224 * </p>
225 *
226 * <p><strong>IMPLEMENTATION NOTE</strong> - If the specified
227 * <code>key</code> identifies a write-only property, <code>null</code>
228 * will arbitrarily be returned, in order to avoid difficulties implementing
229 * the contracts of the <code>Map</code> interface.</p>
230 *
231 * @param key Key of the value to be returned
232 * @return The value for the specified key.
233 *
234 * @exception IllegalArgumentException if an exception is thrown
235 * reading this local property value
236 * @exception UnsupportedOperationException if this local property does not
237 * have a read method.
238 */
239 public Object get(Object key) {
240
241
242 if (descriptors == null) {
243 return (super.get(key));
244 }
245
246
247 if (key != null) {
248 PropertyDescriptor descriptor =
249 (PropertyDescriptor) descriptors.get(key);
250 if (descriptor != null) {
251 if (descriptor.getReadMethod() != null) {
252 return (readProperty(descriptor));
253 } else {
254 return (null);
255 }
256 }
257 }
258
259
260 return (super.get(key));
261
262 }
263
264
265 /***
266 * <p>Override the default <code>Map</code> behavior to return
267 * <code>true</code> if the underlying <code>Map</code> only contains
268 * key-value pairs for local properties (if any).</p>
269 *
270 * @return <code>true</code> if this Context is empty, otherwise
271 * <code>false</code>.
272 */
273 public boolean isEmpty() {
274
275
276 if (descriptors == null) {
277 return (super.isEmpty());
278 }
279
280
281 return (super.size() <= descriptors.size());
282
283 }
284
285
286 /***
287 * <p>Override the default <code>Map</code> behavior to return a
288 * <code>Set</code> that meets the specified default behavior except
289 * for attempts to remove the key for a property of the {@link Context}
290 * implementation class, which will throw
291 * <code>UnsupportedOperationException</code>.</p>
292 *
293 * @return The set of keys for objects in this Context.
294 */
295 public Set keySet() {
296
297
298 return (super.keySet());
299
300 }
301
302
303 /***
304 * <p>Override the default <code>Map</code> behavior to set the value
305 * of a local property if the specified key matches a local property name.
306 * </p>
307 *
308 * @param key Key of the value to be stored or replaced
309 * @param value New value to be stored
310 * @return The value added to the Context.
311 *
312 * @exception IllegalArgumentException if an exception is thrown
313 * reading or wrting this local property value
314 * @exception UnsupportedOperationException if this local property does not
315 * have both a read method and a write method
316 */
317 public Object put(Object key, Object value) {
318
319
320 if (descriptors == null) {
321 return (super.put(key, value));
322 }
323
324
325 if (key != null) {
326 PropertyDescriptor descriptor =
327 (PropertyDescriptor) descriptors.get(key);
328 if (descriptor != null) {
329 Object previous = null;
330 if (descriptor.getReadMethod() != null) {
331 previous = readProperty(descriptor);
332 }
333 writeProperty(descriptor, value);
334 return (previous);
335 }
336 }
337
338
339 return (super.put(key, value));
340
341 }
342
343
344 /***
345 * <p>Override the default <code>Map</code> behavior to call the
346 * <code>put()</code> method individually for each key-value pair
347 * in the specified <code>Map</code>.</p>
348 *
349 * @param map <code>Map</code> containing key-value pairs to store
350 * (or replace)
351 *
352 * @exception IllegalArgumentException if an exception is thrown
353 * reading or wrting a local property value
354 * @exception UnsupportedOperationException if a local property does not
355 * have both a read method and a write method
356 */
357 public void putAll(Map map) {
358
359 Iterator pairs = map.entrySet().iterator();
360 while (pairs.hasNext()) {
361 Map.Entry pair = (Map.Entry) pairs.next();
362 put(pair.getKey(), pair.getValue());
363 }
364
365 }
366
367
368 /***
369 * <p>Override the default <code>Map</code> behavior to throw
370 * <code>UnsupportedOperationException</code> on any attempt to
371 * remove a key that is the name of a local property.</p>
372 *
373 * @param key Key to be removed
374 * @return The value removed from the Context.
375 *
376 * @exception UnsupportedOperationException if the specified
377 * <code>key</code> matches the name of a local property
378 */
379 public Object remove(Object key) {
380
381
382 if (descriptors == null) {
383 return (super.remove(key));
384 }
385
386
387 if (key != null) {
388 PropertyDescriptor descriptor =
389 (PropertyDescriptor) descriptors.get(key);
390 if (descriptor != null) {
391 throw new UnsupportedOperationException
392 ("Local property '" + key + "' cannot be removed");
393 }
394 }
395
396
397 return (super.remove(key));
398
399 }
400
401
402 /***
403 * <p>Override the default <code>Map</code> behavior to return a
404 * <code>Collection</code> that meets the specified default behavior except
405 * for attempts to remove the key for a property of the {@link Context}
406 * implementation class, which will throw
407 * <code>UnsupportedOperationException</code>.</p>
408 *
409 * @return The collection of values in this Context.
410 */
411 public Collection values() {
412
413 return (new ValuesImpl());
414
415 }
416
417
418
419
420
421 /***
422 * <p>Eliminate the specified property descriptor from the list of
423 * property descriptors in <code>pd</code>.</p>
424 *
425 * @param name Name of the property to eliminate
426 *
427 * @exception IllegalArgumentException if the specified property name
428 * is not present
429 */
430 private void eliminate(String name) {
431
432 int j = -1;
433 for (int i = 0; i < pd.length; i++) {
434 if (name.equals(pd[i].getName())) {
435 j = i;
436 break;
437 }
438 }
439 if (j < 0) {
440 throw new IllegalArgumentException("Property '" + name
441 + "' is not present");
442 }
443 PropertyDescriptor[] results = new PropertyDescriptor[pd.length - 1];
444 System.arraycopy(pd, 0, results, 0, j);
445 System.arraycopy(pd, j + 1, results, j, pd.length - (j + 1));
446 pd = results;
447
448 }
449
450
451 /***
452 * <p>Return an <code>Iterator</code> over the set of <code>Map.Entry</code>
453 * objects representing our key-value pairs.</p>
454 */
455 private Iterator entriesIterator() {
456
457 return (new EntrySetIterator());
458
459 }
460
461
462 /***
463 * <p>Return a <code>Map.Entry</code> for the specified key value, if it
464 * is present; otherwise, return <code>null</code>.</p>
465 *
466 * @param key Attribute key or property name
467 */
468 private Map.Entry entry(Object key) {
469
470 if (containsKey(key)) {
471 return (new MapEntryImpl(key, get(key)));
472 } else {
473 return (null);
474 }
475
476 }
477
478
479 /***
480 * <p>Customize the contents of our underlying <code>Map</code> so that
481 * it contains keys corresponding to all of the JavaBeans properties of
482 * the {@link Context} implementation class.</p>
483 *
484 *
485 * @exception IllegalArgumentException if an exception is thrown
486 * writing this local property value
487 * @exception UnsupportedOperationException if this local property does not
488 * have a write method.
489 */
490 private void initialize() {
491
492
493 try {
494 pd = Introspector.getBeanInfo
495 (getClass()).getPropertyDescriptors();
496 } catch (IntrospectionException e) {
497 pd = new PropertyDescriptor[0];
498 }
499 eliminate("class");
500 eliminate("empty");
501
502
503 if (pd.length > 0) {
504 descriptors = new HashMap();
505 for (int i = 0; i < pd.length; i++) {
506 descriptors.put(pd[i].getName(), pd[i]);
507 super.put(pd[i].getName(), singleton);
508 }
509 }
510
511 }
512
513
514 /***
515 * <p>Get and return the value for the specified property.</p>
516 *
517 * @param descriptor <code>PropertyDescriptor</code> for the
518 * specified property
519 *
520 * @exception IllegalArgumentException if an exception is thrown
521 * reading this local property value
522 * @exception UnsupportedOperationException if this local property does not
523 * have a read method.
524 */
525 private Object readProperty(PropertyDescriptor descriptor) {
526
527 try {
528 Method method = descriptor.getReadMethod();
529 if (method == null) {
530 throw new UnsupportedOperationException
531 ("Property '" + descriptor.getName()
532 + "' is not readable");
533 }
534 return (method.invoke(this, zeroParams));
535 } catch (Exception e) {
536 throw new UnsupportedOperationException
537 ("Exception reading property '" + descriptor.getName()
538 + "': " + e.getMessage());
539 }
540
541 }
542
543
544 /***
545 * <p>Remove the specified key-value pair, if it exists, and return
546 * <code>true</code>. If this pair does not exist, return
547 * <code>false</code>.</p>
548 *
549 * @param entry Key-value pair to be removed
550 *
551 * @exception UnsupportedOperationException if the specified key
552 * identifies a property instead of an attribute
553 */
554 private boolean remove(Map.Entry entry) {
555
556 Map.Entry actual = entry(entry.getKey());
557 if (actual == null) {
558 return (false);
559 } else if (!entry.equals(actual)) {
560 return (false);
561 } else {
562 remove(entry.getKey());
563 return (true);
564 }
565
566 }
567
568
569 /***
570 * <p>Return an <code>Iterator</code> over the set of values in this
571 * <code>Map</code>.</p>
572 */
573 private Iterator valuesIterator() {
574
575 return (new ValuesIterator());
576
577 }
578
579
580 /***
581 * <p>Set the value for the specified property.</p>
582 *
583 * @param descriptor <code>PropertyDescriptor</code> for the
584 * specified property
585 * @param value The new value for this property (must be of the
586 * correct type)
587 *
588 * @exception IllegalArgumentException if an exception is thrown
589 * writing this local property value
590 * @exception UnsupportedOperationException if this local property does not
591 * have a write method.
592 */
593 private void writeProperty(PropertyDescriptor descriptor, Object value) {
594
595 try {
596 Method method = descriptor.getWriteMethod();
597 if (method == null) {
598 throw new UnsupportedOperationException
599 ("Property '" + descriptor.getName()
600 + "' is not writeable");
601 }
602 method.invoke(this, new Object[] {value});
603 } catch (Exception e) {
604 throw new UnsupportedOperationException
605 ("Exception writing property '" + descriptor.getName()
606 + "': " + e.getMessage());
607 }
608
609 }
610
611
612
613
614
615 /***
616 * <p>Private implementation of <code>Set</code> that implements the
617 * semantics required for the value returned by <code>entrySet()</code>.</p>
618 */
619 private class EntrySetImpl extends AbstractSet {
620
621 public void clear() {
622 ContextBase.this.clear();
623 }
624
625 public boolean contains(Object obj) {
626 if (!(obj instanceof Map.Entry)) {
627 return (false);
628 }
629 Map.Entry entry = (Map.Entry) obj;
630 Entry actual = ContextBase.this.entry(entry.getKey());
631 if (actual != null) {
632 return (actual.equals(entry));
633 } else {
634 return (false);
635 }
636 }
637
638 public boolean isEmpty() {
639 return (ContextBase.this.isEmpty());
640 }
641
642 public Iterator iterator() {
643 return (ContextBase.this.entriesIterator());
644 }
645
646 public boolean remove(Object obj) {
647 if (obj instanceof Map.Entry) {
648 return (ContextBase.this.remove((Map.Entry) obj));
649 } else {
650 return (false);
651 }
652 }
653
654 public int size() {
655 return (ContextBase.this.size());
656 }
657
658 }
659
660
661 /***
662 * <p>Private implementation of <code>Iterator</code> for the
663 * <code>Set</code> returned by <code>entrySet()</code>.</p>
664 */
665 private class EntrySetIterator implements Iterator {
666
667 private Map.Entry entry = null;
668 private Iterator keys = ContextBase.this.keySet().iterator();
669
670 public boolean hasNext() {
671 return (keys.hasNext());
672 }
673
674 public Object next() {
675 entry = ContextBase.this.entry(keys.next());
676 return (entry);
677 }
678
679 public void remove() {
680 ContextBase.this.remove(entry);
681 }
682
683 }
684
685
686 /***
687 * <p>Private implementation of <code>Map.Entry</code> for each item in
688 * <code>EntrySetImpl</code>.</p>
689 */
690 private class MapEntryImpl implements Map.Entry {
691
692 MapEntryImpl(Object key, Object value) {
693 this.key = key;
694 this.value = value;
695 }
696
697 private Object key;
698 private Object value;
699
700 public boolean equals(Object obj) {
701 if (obj == null) {
702 return (false);
703 } else if (!(obj instanceof Map.Entry)) {
704 return (false);
705 }
706 Map.Entry entry = (Map.Entry) obj;
707 if (key == null) {
708 return (entry.getKey() == null);
709 }
710 if (key.equals(entry.getKey())) {
711 if (value == null) {
712 return (entry.getValue() == null);
713 } else {
714 return (value.equals(entry.getValue()));
715 }
716 } else {
717 return (false);
718 }
719 }
720
721 public Object getKey() {
722 return (this.key);
723 }
724
725 public Object getValue() {
726 return (this.value);
727 }
728
729 public int hashCode() {
730 return (((key == null) ? 0 : key.hashCode())
731 ^ ((value == null) ? 0 : value.hashCode()));
732 }
733
734 public Object setValue(Object value) {
735 Object previous = this.value;
736 ContextBase.this.put(this.key, value);
737 this.value = value;
738 return (previous);
739 }
740
741 public String toString() {
742 return getKey() + "=" + getValue();
743 }
744 }
745
746
747 /***
748 * <p>Private implementation of <code>Collection</code> that implements the
749 * semantics required for the value returned by <code>values()</code>.</p>
750 */
751 private class ValuesImpl extends AbstractCollection {
752
753 public void clear() {
754 ContextBase.this.clear();
755 }
756
757 public boolean contains(Object obj) {
758 if (!(obj instanceof Map.Entry)) {
759 return (false);
760 }
761 Map.Entry entry = (Map.Entry) obj;
762 return (ContextBase.this.containsValue(entry.getValue()));
763 }
764
765 public boolean isEmpty() {
766 return (ContextBase.this.isEmpty());
767 }
768
769 public Iterator iterator() {
770 return (ContextBase.this.valuesIterator());
771 }
772
773 public boolean remove(Object obj) {
774 if (obj instanceof Map.Entry) {
775 return (ContextBase.this.remove((Map.Entry) obj));
776 } else {
777 return (false);
778 }
779 }
780
781 public int size() {
782 return (ContextBase.this.size());
783 }
784
785 }
786
787
788 /***
789 * <p>Private implementation of <code>Iterator</code> for the
790 * <code>Collection</code> returned by <code>values()</code>.</p>
791 */
792 private class ValuesIterator implements Iterator {
793
794 private Map.Entry entry = null;
795 private Iterator keys = ContextBase.this.keySet().iterator();
796
797 public boolean hasNext() {
798 return (keys.hasNext());
799 }
800
801 public Object next() {
802 entry = ContextBase.this.entry(keys.next());
803 return (entry.getValue());
804 }
805
806 public void remove() {
807 ContextBase.this.remove(entry);
808 }
809
810 }
811
812
813 }