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 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      // ------------------------------------------------------------ Constructors
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      // ------------------------------------------------------ Instance Variables
92  
93  
94      // NOTE - PropertyDescriptor instances are not Serializable, so the
95      // following variables must be declared as transient.  When a ContextBase
96      // instance is deserialized, the no-arguments constructor is called,
97      // and the initialize() method called there will repoopulate them.
98      // Therefore, no special restoration activity is required.
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     // ------------------------------------------------------------- Map Methods
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         // Case 1 -- no local properties
178         if (descriptors == null) {
179             return (super.containsValue(value));
180         }
181 
182         // Case 2 -- value found in the underlying Map
183         else if (super.containsValue(value)) {
184             return (true);
185         }
186 
187         // Case 3 -- check the values of our readable properties
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         // Case 1 -- no local properties
242         if (descriptors == null) {
243             return (super.get(key));
244         }
245 
246         // Case 2 -- this is a local property
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         // Case 3 -- retrieve value from our underlying Map
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         // Case 1 -- no local properties
276         if (descriptors == null) {
277             return (super.isEmpty());
278         }
279 
280         // Case 2 -- compare key count to property count
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         // Case 1 -- no local properties
320         if (descriptors == null) {
321             return (super.put(key, value));
322         }
323 
324         // Case 2 -- this is a local property
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         // Case 3 -- store or replace value in our underlying map
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         // Case 1 -- no local properties
382         if (descriptors == null) {
383             return (super.remove(key));
384         }
385 
386         // Case 2 -- this is a local property
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         // Case 3 -- remove from underlying Map
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     // --------------------------------------------------------- Private Methods
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         // Retrieve the set of property descriptors for this Context class
493         try {
494             pd = Introspector.getBeanInfo
495                 (getClass()).getPropertyDescriptors();
496         } catch (IntrospectionException e) {
497             pd = new PropertyDescriptor[0]; // Should never happen
498         }
499         eliminate("class"); // Because of "getClass()"
500         eliminate("empty"); // Because of "isEmpty()"
501 
502         // Initialize the underlying Map contents
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     // --------------------------------------------------------- Private Classes
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 }