View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.chain.impl;
18  
19  
20  import java.beans.IntrospectionException;
21  import java.beans.Introspector;
22  import java.beans.PropertyDescriptor;
23  import java.lang.reflect.Method;
24  import java.util.AbstractCollection;
25  import java.util.AbstractSet;
26  import java.util.Collection;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  import java.io.Serializable;
32  import org.apache.commons.chain.Context;
33  
34  
35  /**
36   * <p>Convenience base class for {@link Context} implementations.</p>
37   *
38   * <p>In addition to the minimal functionality required by the {@link Context}
39   * interface, this class implements the recommended support for
40   * <em>Attribute-Property Transparency</em>. This is implemented by
41   * analyzing the available JavaBeans properties of this class (or its
42   * subclass), exposes them as key-value pairs in the <code>Map</code>,
43   * with the key being the name of the property itself.</p>
44   *
45   * <p><strong>IMPLEMENTATION NOTE</strong> - Because <code>empty</code> is a
46   * read-only property defined by the <code>Map</code> interface, it may not
47   * be utilized as an attribute key or property name.</p>
48   *
49   * @author Craig R. McClanahan
50   * @version $Revision: 499247 $ $Date: 2007-01-24 04:09:44 +0000 (Wed, 24 Jan 2007) $
51   */
52  
53  public class ContextBase extends HashMap implements Context {
54  
55  
56      // ------------------------------------------------------------ Constructors
57  
58  
59      /**
60       * Default, no argument constructor.
61       */
62      public ContextBase() {
63  
64          super();
65          initialize();
66  
67      }
68  
69  
70      /**
71       * <p>Initialize the contents of this {@link Context} by copying the
72       * values from the specified <code>Map</code>.  Any keys in <code>map</code>
73       * that correspond to local properties will cause the setter method for
74       * that property to be called.</p>
75       *
76       * @param map Map whose key-value pairs are added
77       *
78       * @exception IllegalArgumentException if an exception is thrown
79       *  writing a local property value
80       * @exception UnsupportedOperationException if a local property does not
81       *  have a write method.
82       */
83      public ContextBase(Map map) {
84  
85          super(map);
86          initialize();
87          putAll(map);
88  
89      }
90  
91  
92      // ------------------------------------------------------ Instance Variables
93  
94  
95      // NOTE - PropertyDescriptor instances are not Serializable, so the
96      // following variables must be declared as transient.  When a ContextBase
97      // instance is deserialized, the no-arguments constructor is called,
98      // and the initialize() method called there will repoopulate them.
99      // 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 }