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