View Javadoc

1   /*
2    * Copyright 1999-2004 The Apache Software Foundation
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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      // ------------------------------------------------------------ Constructors
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      // ------------------------------------------------------ Instance Variables
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     // ------------------------------------------------------------- Map Methods
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         // Case 1 -- no local properties
168         if (descriptors == null) {
169             return (super.containsValue(value));
170         }
171 
172         // Case 2 -- value found in the underlying Map
173         else if (super.containsValue(value)) {
174             return (true);
175         }
176 
177         // Case 3 -- check the values of our readable properties
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         // Case 1 -- no local properties
229         if (descriptors == null) {
230             return (super.get(key));
231         }
232 
233         // Case 2 -- this is a local property
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         // Case 3 -- retrieve value from our underlying Map
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         // Case 1 -- no local properties
260         if (descriptors == null) {
261             return (super.isEmpty());
262         }
263 
264         // Case 2 -- compare key count to property count
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         // Case 1 -- no local properties
301         if (descriptors == null) {
302             return (super.put(key, value));
303         }
304 
305         // Case 2 -- this is a local property
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         // Case 3 -- store or replace value in our underlying map
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         // Case 1 -- no local properties
362         if (descriptors == null) {
363             return (super.remove(key));
364         }
365 
366         // Case 2 -- this is a local property
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         // Case 3 -- remove from underlying Map
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     // --------------------------------------------------------- Private Methods
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         // Retrieve the set of property descriptors for this Context class
471         try {
472             pd = Introspector.getBeanInfo
473                 (getClass()).getPropertyDescriptors();
474         } catch (IntrospectionException e) {
475             pd = new PropertyDescriptor[0]; // Should never happen
476         }
477         eliminate("class"); // Because of "getClass()"
478         eliminate("empty"); // Because of "isEmpty()"
479 
480         // Initialize the underlying Map contents
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     // --------------------------------------------------------- Private Classes
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 }