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 }