001: /*
002: * Copyright 1999-2004 The Apache Software Foundation
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.apache.commons.chain.impl;
017:
018: import java.beans.IntrospectionException;
019: import java.beans.Introspector;
020: import java.beans.PropertyDescriptor;
021: import java.lang.reflect.Method;
022: import java.util.AbstractCollection;
023: import java.util.AbstractSet;
024: import java.util.Collection;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.Map;
028: import java.util.Set;
029: import java.io.Serializable;
030: import org.apache.commons.chain.Context;
031:
032: /**
033: * <p>Convenience base class for {@link Context} implementations.</p>
034: *
035: * <p>In addition to the minimal functionality required by the {@link Context}
036: * interface, this class implements the recommended support for
037: * <em>Attribute-Property Transparency</p>. This is implemented by
038: * analyzing the available JavaBeans properties of this class (or its
039: * subclass), exposes them as key-value pairs in the <code>Map</code>,
040: * with the key being the name of the property itself.</p>
041: *
042: * <p><strong>IMPLEMENTATION NOTE</strong> - Because <code>empty</code> is a
043: * read-only property defined by the <code>Map</code> interface, it may not
044: * be utilized as an attribute key or property name.</p>
045: *
046: * @author Craig R. McClanahan
047: * @version $Revision: 412150 $ $Date: 2006-06-06 16:31:50 +0100 (Tue, 06 Jun 2006) $
048: */
049:
050: public class ContextBase extends HashMap implements Context {
051:
052: // ------------------------------------------------------------ Constructors
053:
054: /**
055: * Default, no argument constructor.
056: */
057: public ContextBase() {
058:
059: super ();
060: initialize();
061:
062: }
063:
064: /**
065: * <p>Initialize the contents of this {@link Context} by copying the
066: * values from the specified <code>Map</code>. Any keys in <code>map</code>
067: * that correspond to local properties will cause the setter method for
068: * that property to be called.</p>
069: *
070: * @param map Map whose key-value pairs are added
071: *
072: * @exception IllegalArgumentException if an exception is thrown
073: * writing a local property value
074: * @exception UnsupportedOperationException if a local property does not
075: * have a write method.
076: */
077: public ContextBase(Map map) {
078:
079: super (map);
080: initialize();
081: putAll(map);
082:
083: }
084:
085: // ------------------------------------------------------ Instance Variables
086:
087: // NOTE - PropertyDescriptor instances are not Serializable, so the
088: // following variables must be declared as transient. When a ContextBase
089: // instance is deserialized, the no-arguments constructor is called,
090: // and the initialize() method called there will repoopulate them.
091: // Therefore, no special restoration activity is required.
092:
093: /**
094: * <p>The <code>PropertyDescriptor</code>s for all JavaBeans properties
095: * of this {@link Context} implementation class, keyed by property name.
096: * This collection is allocated only if there are any JavaBeans
097: * properties.</p>
098: */
099: private transient Map descriptors = null;
100:
101: /**
102: * <p>The same <code>PropertyDescriptor</code>s as an array.</p>
103: */
104: private transient PropertyDescriptor[] pd = null;
105:
106: /**
107: * <p>Distinguished singleton value that is stored in the map for each
108: * key that is actually a property. This value is used to ensure that
109: * <code>equals()</code> comparisons will always fail.</p>
110: */
111: private static Object singleton;
112:
113: static {
114:
115: singleton = new Serializable() {
116: public boolean equals(Object object) {
117: return (false);
118: }
119: };
120:
121: }
122:
123: /**
124: * <p>Zero-length array of parameter values for calling property getters.
125: * </p>
126: */
127: private static Object[] zeroParams = new Object[0];
128:
129: // ------------------------------------------------------------- Map Methods
130:
131: /**
132: * <p>Override the default <code>Map</code> behavior to clear all keys and
133: * values except those corresponding to JavaBeans properties.</p>
134: */
135: public void clear() {
136:
137: if (descriptors == null) {
138: super .clear();
139: } else {
140: Iterator keys = keySet().iterator();
141: while (keys.hasNext()) {
142: Object key = keys.next();
143: if (!descriptors.containsKey(key)) {
144: keys.remove();
145: }
146: }
147: }
148:
149: }
150:
151: /**
152: * <p>Override the default <code>Map</code> behavior to return
153: * <code>true</code> if the specified value is present in either the
154: * underlying <code>Map</code> or one of the local property values.</p>
155: *
156: * @param value the value look for in the context.
157: * @return <code>true</code> if found in this context otherwise
158: * <code>false</code>.
159: * @exception IllegalArgumentException if a property getter
160: * throws an exception
161: */
162: public boolean containsValue(Object value) {
163:
164: // Case 1 -- no local properties
165: if (descriptors == null) {
166: return (super .containsValue(value));
167: }
168:
169: // Case 2 -- value found in the underlying Map
170: else if (super .containsValue(value)) {
171: return (true);
172: }
173:
174: // Case 3 -- check the values of our readable properties
175: for (int i = 0; i < pd.length; i++) {
176: if (pd[i].getReadMethod() != null) {
177: Object prop = readProperty(pd[i]);
178: if (value == null) {
179: if (prop == null) {
180: return (true);
181: }
182: } else if (value.equals(prop)) {
183: return (true);
184: }
185: }
186: }
187: return (false);
188:
189: }
190:
191: /**
192: * <p>Override the default <code>Map</code> behavior to return a
193: * <code>Set</code> that meets the specified default behavior except
194: * for attempts to remove the key for a property of the {@link Context}
195: * implementation class, which will throw
196: * <code>UnsupportedOperationException</code>.</p>
197: *
198: * @return Set of entries in the Context.
199: */
200: public Set entrySet() {
201:
202: return (new EntrySetImpl());
203:
204: }
205:
206: /**
207: * <p>Override the default <code>Map</code> behavior to return the value
208: * of a local property if the specified key matches a local property name.
209: * </p>
210: *
211: * <p><strong>IMPLEMENTATION NOTE</strong> - If the specified
212: * <code>key</code> identifies a write-only property, <code>null</code>
213: * will arbitrarily be returned, in order to avoid difficulties implementing
214: * the contracts of the <code>Map</code> interface.</p>
215: *
216: * @param key Key of the value to be returned
217: * @return The value for the specified key.
218: *
219: * @exception IllegalArgumentException if an exception is thrown
220: * reading this local property value
221: * @exception UnsupportedOperationException if this local property does not
222: * have a read method.
223: */
224: public Object get(Object key) {
225:
226: // Case 1 -- no local properties
227: if (descriptors == null) {
228: return (super .get(key));
229: }
230:
231: // Case 2 -- this is a local property
232: if (key != null) {
233: PropertyDescriptor descriptor = (PropertyDescriptor) descriptors
234: .get(key);
235: if (descriptor != null) {
236: if (descriptor.getReadMethod() != null) {
237: return (readProperty(descriptor));
238: } else {
239: return (null);
240: }
241: }
242: }
243:
244: // Case 3 -- retrieve value from our underlying Map
245: return (super .get(key));
246:
247: }
248:
249: /**
250: * <p>Override the default <code>Map</code> behavior to return
251: * <code>true</code> if the underlying <code>Map</code> only contains
252: * key-value pairs for local properties (if any).</p>
253: *
254: * @return <code>true</code> if this Context is empty, otherwise
255: * <code>false</code>.
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: * <p>Override the default <code>Map</code> behavior to return a
271: * <code>Set</code> that meets the specified default behavior except
272: * for attempts to remove the key for a property of the {@link Context}
273: * implementation class, which will throw
274: * <code>UnsupportedOperationException</code>.</p>
275: *
276: * @return The set of keys for objects in this Context.
277: */
278: public Set keySet() {
279:
280: return (super .keySet());
281:
282: }
283:
284: /**
285: * <p>Override the default <code>Map</code> behavior to set the value
286: * of a local property if the specified key matches a local property name.
287: * </p>
288: *
289: * @param key Key of the value to be stored or replaced
290: * @param value New value to be stored
291: * @return The value added to the Context.
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 = (PropertyDescriptor) descriptors
308: .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: * <p>Override the default <code>Map</code> behavior to call the
326: * <code>put()</code> method individually for each key-value pair
327: * in the specified <code>Map</code>.</p>
328: *
329: * @param map <code>Map</code> containing key-value pairs to store
330: * (or replace)
331: *
332: * @exception IllegalArgumentException if an exception is thrown
333: * reading or wrting a local property value
334: * @exception UnsupportedOperationException if a local property does not
335: * have both a read method and a write method
336: */
337: public void putAll(Map map) {
338:
339: Iterator pairs = map.entrySet().iterator();
340: while (pairs.hasNext()) {
341: Map.Entry pair = (Map.Entry) pairs.next();
342: put(pair.getKey(), pair.getValue());
343: }
344:
345: }
346:
347: /**
348: * <p>Override the default <code>Map</code> behavior to throw
349: * <code>UnsupportedOperationException</code> on any attempt to
350: * remove a key that is the name of a local property.</p>
351: *
352: * @param key Key to be removed
353: * @return The value removed from the Context.
354: *
355: * @exception UnsupportedOperationException if the specified
356: * <code>key</code> matches the name of a local property
357: */
358: public Object remove(Object key) {
359:
360: // Case 1 -- no local properties
361: if (descriptors == null) {
362: return (super .remove(key));
363: }
364:
365: // Case 2 -- this is a local property
366: if (key != null) {
367: PropertyDescriptor descriptor = (PropertyDescriptor) descriptors
368: .get(key);
369: if (descriptor != null) {
370: throw new UnsupportedOperationException(
371: "Local property '" + key
372: + "' cannot be removed");
373: }
374: }
375:
376: // Case 3 -- remove from underlying Map
377: return (super .remove(key));
378:
379: }
380:
381: /**
382: * <p>Override the default <code>Map</code> behavior to return a
383: * <code>Collection</code> that meets the specified default behavior except
384: * for attempts to remove the key for a property of the {@link Context}
385: * implementation class, which will throw
386: * <code>UnsupportedOperationException</code>.</p>
387: *
388: * @return The collection of values in this Context.
389: */
390: public Collection values() {
391:
392: return (new ValuesImpl());
393:
394: }
395:
396: // --------------------------------------------------------- Private Methods
397:
398: /**
399: * <p>Eliminate the specified property descriptor from the list of
400: * property descriptors in <code>pd</code>.</p>
401: *
402: * @param name Name of the property to eliminate
403: *
404: * @exception IllegalArgumentException if the specified property name
405: * is not present
406: */
407: private void eliminate(String name) {
408:
409: int j = -1;
410: for (int i = 0; i < pd.length; i++) {
411: if (name.equals(pd[i].getName())) {
412: j = i;
413: break;
414: }
415: }
416: if (j < 0) {
417: throw new IllegalArgumentException("Property '" + name
418: + "' is not present");
419: }
420: PropertyDescriptor[] results = new PropertyDescriptor[pd.length - 1];
421: System.arraycopy(pd, 0, results, 0, j);
422: System.arraycopy(pd, j + 1, results, j, pd.length - (j + 1));
423: pd = results;
424:
425: }
426:
427: /**
428: * <p>Return an <code>Iterator</code> over the set of <code>Map.Entry</code>
429: * objects representing our key-value pairs.</p>
430: */
431: private Iterator entriesIterator() {
432:
433: return (new EntrySetIterator());
434:
435: }
436:
437: /**
438: * <p>Return a <code>Map.Entry</code> for the specified key value, if it
439: * is present; otherwise, return <code>null</code>.</p>
440: *
441: * @param key Attribute key or property name
442: */
443: private Map.Entry entry(Object key) {
444:
445: if (containsKey(key)) {
446: return (new MapEntryImpl(key, get(key)));
447: } else {
448: return (null);
449: }
450:
451: }
452:
453: /**
454: * <p>Customize the contents of our underlying <code>Map</code> so that
455: * it contains keys corresponding to all of the JavaBeans properties of
456: * the {@link Context} implementation class.</p>
457: *
458: *
459: * @exception IllegalArgumentException if an exception is thrown
460: * writing this local property value
461: * @exception UnsupportedOperationException if this local property does not
462: * have a write method.
463: */
464: private void initialize() {
465:
466: // Retrieve the set of property descriptors for this Context class
467: try {
468: pd = Introspector.getBeanInfo(getClass())
469: .getPropertyDescriptors();
470: } catch (IntrospectionException e) {
471: pd = new PropertyDescriptor[0]; // Should never happen
472: }
473: eliminate("class"); // Because of "getClass()"
474: eliminate("empty"); // Because of "isEmpty()"
475:
476: // Initialize the underlying Map contents
477: if (pd.length > 0) {
478: descriptors = new HashMap();
479: for (int i = 0; i < pd.length; i++) {
480: descriptors.put(pd[i].getName(), pd[i]);
481: super .put(pd[i].getName(), singleton);
482: }
483: }
484:
485: }
486:
487: /**
488: * <p>Get and return the value for the specified property.</p>
489: *
490: * @param descriptor <code>PropertyDescriptor</code> for the
491: * specified property
492: *
493: * @exception IllegalArgumentException if an exception is thrown
494: * reading this local property value
495: * @exception UnsupportedOperationException if this local property does not
496: * have a read method.
497: */
498: private Object readProperty(PropertyDescriptor descriptor) {
499:
500: try {
501: Method method = descriptor.getReadMethod();
502: if (method == null) {
503: throw new UnsupportedOperationException("Property '"
504: + descriptor.getName() + "' is not readable");
505: }
506: return (method.invoke(this , zeroParams));
507: } catch (Exception e) {
508: throw new UnsupportedOperationException(
509: "Exception reading property '"
510: + descriptor.getName() + "': "
511: + e.getMessage());
512: }
513:
514: }
515:
516: /**
517: * <p>Remove the specified key-value pair, if it exists, and return
518: * <code>true</code>. If this pair does not exist, return
519: * <code>false</code>.</p>
520: *
521: * @param entry Key-value pair to be removed
522: *
523: * @exception UnsupportedOperationException if the specified key
524: * identifies a property instead of an attribute
525: */
526: private boolean remove(Map.Entry entry) {
527:
528: Map.Entry actual = entry(entry.getKey());
529: if (actual == null) {
530: return (false);
531: } else if (!entry.equals(actual)) {
532: return (false);
533: } else {
534: remove(entry.getKey());
535: return (true);
536: }
537:
538: }
539:
540: /**
541: * <p>Return an <code>Iterator</code> over the set of values in this
542: * <code>Map</code>.</p>
543: */
544: private Iterator valuesIterator() {
545:
546: return (new ValuesIterator());
547:
548: }
549:
550: /**
551: * <p>Set the value for the specified property.</p>
552: *
553: * @param descriptor <code>PropertyDescriptor</code> for the
554: * specified property
555: * @param value The new value for this property (must be of the
556: * correct type)
557: *
558: * @exception IllegalArgumentException if an exception is thrown
559: * writing this local property value
560: * @exception UnsupportedOperationException if this local property does not
561: * have a write method.
562: */
563: private void writeProperty(PropertyDescriptor descriptor,
564: Object value) {
565:
566: try {
567: Method method = descriptor.getWriteMethod();
568: if (method == null) {
569: throw new UnsupportedOperationException("Property '"
570: + descriptor.getName() + "' is not writeable");
571: }
572: method.invoke(this , new Object[] { value });
573: } catch (Exception e) {
574: throw new UnsupportedOperationException(
575: "Exception writing property '"
576: + descriptor.getName() + "': "
577: + e.getMessage());
578: }
579:
580: }
581:
582: // --------------------------------------------------------- Private Classes
583:
584: /**
585: * <p>Private implementation of <code>Set</code> that implements the
586: * semantics required for the value returned by <code>entrySet()</code>.</p>
587: */
588: private class EntrySetImpl extends AbstractSet {
589:
590: public void clear() {
591: ContextBase.this .clear();
592: }
593:
594: public boolean contains(Object obj) {
595: if (!(obj instanceof Map.Entry)) {
596: return (false);
597: }
598: Map.Entry entry = (Map.Entry) obj;
599: Entry actual = ContextBase.this .entry(entry.getKey());
600: if (actual != null) {
601: return (actual.equals(entry));
602: } else {
603: return (false);
604: }
605: }
606:
607: public boolean isEmpty() {
608: return (ContextBase.this .isEmpty());
609: }
610:
611: public Iterator iterator() {
612: return (ContextBase.this .entriesIterator());
613: }
614:
615: public boolean remove(Object obj) {
616: if (obj instanceof Map.Entry) {
617: return (ContextBase.this .remove((Map.Entry) obj));
618: } else {
619: return (false);
620: }
621: }
622:
623: public int size() {
624: return (ContextBase.this .size());
625: }
626:
627: }
628:
629: /**
630: * <p>Private implementation of <code>Iterator</code> for the
631: * <code>Set</code> returned by <code>entrySet()</code>.</p>
632: */
633: private class EntrySetIterator implements Iterator {
634:
635: private Map.Entry entry = null;
636: private Iterator keys = ContextBase.this .keySet().iterator();
637:
638: public boolean hasNext() {
639: return (keys.hasNext());
640: }
641:
642: public Object next() {
643: entry = ContextBase.this .entry(keys.next());
644: return (entry);
645: }
646:
647: public void remove() {
648: ContextBase.this .remove(entry);
649: }
650:
651: }
652:
653: /**
654: * <p>Private implementation of <code>Map.Entry</code> for each item in
655: * <code>EntrySetImpl</code>.</p>
656: */
657: private class MapEntryImpl implements Map.Entry {
658:
659: MapEntryImpl(Object key, Object value) {
660: this .key = key;
661: this .value = value;
662: }
663:
664: private Object key;
665: private Object value;
666:
667: public boolean equals(Object obj) {
668: if (obj == null) {
669: return (false);
670: } else if (!(obj instanceof Map.Entry)) {
671: return (false);
672: }
673: Map.Entry entry = (Map.Entry) obj;
674: if (key == null) {
675: return (entry.getKey() == null);
676: }
677: if (key.equals(entry.getKey())) {
678: if (value == null) {
679: return (entry.getValue() == null);
680: } else {
681: return (value.equals(entry.getValue()));
682: }
683: } else {
684: return (false);
685: }
686: }
687:
688: public Object getKey() {
689: return (this .key);
690: }
691:
692: public Object getValue() {
693: return (this .value);
694: }
695:
696: public int hashCode() {
697: return (((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0
698: : value.hashCode()));
699: }
700:
701: public Object setValue(Object value) {
702: Object previous = this .value;
703: ContextBase.this .put(this .key, value);
704: this .value = value;
705: return (previous);
706: }
707:
708: public String toString() {
709: return getKey() + "=" + getValue();
710: }
711: }
712:
713: /**
714: * <p>Private implementation of <code>Collection</code> that implements the
715: * semantics required for the value returned by <code>values()</code>.</p>
716: */
717: private class ValuesImpl extends AbstractCollection {
718:
719: public void clear() {
720: ContextBase.this .clear();
721: }
722:
723: public boolean contains(Object obj) {
724: if (!(obj instanceof Map.Entry)) {
725: return (false);
726: }
727: Map.Entry entry = (Map.Entry) obj;
728: return (ContextBase.this .containsValue(entry.getValue()));
729: }
730:
731: public boolean isEmpty() {
732: return (ContextBase.this .isEmpty());
733: }
734:
735: public Iterator iterator() {
736: return (ContextBase.this .valuesIterator());
737: }
738:
739: public boolean remove(Object obj) {
740: if (obj instanceof Map.Entry) {
741: return (ContextBase.this .remove((Map.Entry) obj));
742: } else {
743: return (false);
744: }
745: }
746:
747: public int size() {
748: return (ContextBase.this .size());
749: }
750:
751: }
752:
753: /**
754: * <p>Private implementation of <code>Iterator</code> for the
755: * <code>Collection</code> returned by <code>values()</code>.</p>
756: */
757: private class ValuesIterator implements Iterator {
758:
759: private Map.Entry entry = null;
760: private Iterator keys = ContextBase.this .keySet().iterator();
761:
762: public boolean hasNext() {
763: return (keys.hasNext());
764: }
765:
766: public Object next() {
767: entry = ContextBase.this .entry(keys.next());
768: return (entry.getValue());
769: }
770:
771: public void remove() {
772: ContextBase.this.remove(entry);
773: }
774:
775: }
776:
777: }
|