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    }