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  import java.beans.IntrospectionException;
20  import java.beans.Introspector;
21  import java.beans.PropertyDescriptor;
22  import java.io.Serializable;
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.Objects;
31  import java.util.Set;
32  
33  import org.apache.commons.chain.Context;
34  
35  /**
36   * Convenience base class for {@link Context} implementations.
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},
43   * with the key being the name of the property itself.</p>
44   *
45   * <p><strong>IMPLEMENTATION NOTE</strong> - Because {@code empty} is a
46   * read-only property defined by the {@code Map} interface, it may not
47   * be utilized as an attribute key or property name.</p>
48   *
49   * @author Craig R. McClanahan
50   * @version $Revision$ $Date$
51   */
52  public class ContextBase extends HashMap<String, Object> implements Context {
53      private static final long serialVersionUID = -2482145117370708259L;
54  
55      // ------------------------------------------------------ Static Variables
56  
57      /**
58       * Distinguished singleton value that is stored in the map for each
59       * key that is actually a property. This value is used to ensure that
60       * {@code equals()} comparisons will always fail.
61       */
62      private static final Object SINGLETON = new Serializable() {
63          private static final long serialVersionUID = -6023767081282668587L;
64  
65          @Override
66          public boolean equals(Object object) {
67              return false;
68          }
69  
70          @Override
71          public int hashCode() {
72              return super.hashCode();
73          }
74      };
75  
76      // ------------------------------------------------------ Instance Variables
77  
78      // NOTE - PropertyDescriptor instances are not Serializable, so the
79      // following variables must be declared as transient. When a ContextBase
80      // instance is deserialized, the no-arguments constructor is called,
81      // and the initialize() method called there will repopulate them.
82      // Therefore, no special restoration activity is required.
83  
84      /**
85       * The {@code PropertyDescriptor}s for all JavaBeans properties
86       * of this {@link Context} implementation class as an array.
87       */
88      private final transient PropertyDescriptor[] pds = getPropertyDescriptors();
89  
90      /**
91       * The {@code PropertyDescriptor}s for all JavaBeans properties
92       * of this {@link Context} implementation class, keyed by property name.
93       * This collection is allocated only if there are any JavaBeans
94       * properties.
95       */
96      private final transient Map<String, PropertyDescriptor> descriptors = getMapDescriptors();
97  
98      // ------------------------------------------------------------ Constructors
99  
100     /**
101      * Default, no argument constructor.
102      */
103     public ContextBase() {
104     }
105 
106     /**
107      * Initialize the contents of this {@link Context} by copying the
108      * values from the specified {@code Map}. Any keys in {@code map}
109      * that correspond to local properties will cause the setter method for
110      * that property to be called.
111      *
112      * @param map Map whose key-value pairs are added
113      *
114      * @throws IllegalArgumentException if an exception is thrown
115      *         writing a local property value.
116      * @throws UnsupportedOperationException if a local property does not
117      *         have a write method.
118      */
119     public ContextBase(Map<String, Object> map) {
120         super(map);
121         putAll(map);
122     }
123 
124     // ------------------------------------------------------------- Map Methods
125 
126     /**
127      * Override the default {@code Map} behavior to clear all keys and
128      * values except those corresponding to JavaBeans properties.
129      */
130     @Override
131     public void clear() {
132         if (descriptors == null) {
133             super.clear();
134         } else {
135             Iterator<String> keys = keySet().iterator();
136             while (keys.hasNext()) {
137                 String key = keys.next();
138                 if (!descriptors.containsKey(key)) {
139                     keys.remove();
140                 }
141             }
142         }
143     }
144 
145     /**
146      * Override the default {@code Map} behavior to return
147      * {@code true} if the specified value is present in either the
148      * underlying {@code Map} or one of the local property values.
149      *
150      * @param value the value look for in the context.
151      *
152      * @return {@code true} if found in this context otherwise
153      *         {@code false}.
154      *
155      * @throws IllegalArgumentException if a property getter
156      *         throws an exception
157      */
158     @Override
159     public boolean containsValue(Object value) {
160         boolean b = super.containsValue(value);
161 
162         // Case 1 -- no local properties
163         if (descriptors == null) {
164             return b;
165         }
166 
167         // Case 2 -- value found in the underlying Map
168         if (b) {
169             return true;
170         }
171 
172         // Case 3 -- check the values of our readable properties
173         for (PropertyDescriptor pd : pds) {
174             if (pd.getReadMethod() != null) {
175                 Object prop = readProperty(pd);
176                 if (value == null) {
177                     if (prop == null) {
178                         return true;
179                     }
180                 } else if (value.equals(prop)) {
181                     return true;
182                 }
183             }
184         }
185         return false;
186     }
187 
188     /**
189      * Override the default {@code Map} behavior to return a
190      * {@code Set} that meets the specified default behavior except
191      * for attempts to remove the key for a property of the {@link Context}
192      * implementation class, which will throw
193      * {@code UnsupportedOperationException}.
194      *
195      * @return Set of entries in the Context.
196      */
197     @Override
198     public Set<Map.Entry<String, Object>> entrySet() {
199         return new EntrySetImpl();
200     }
201 
202     /**
203      * Override the default {@code Map} behavior to return the value
204      * of a local property if the specified key matches a local property name.
205      *
206      * <p><strong>IMPLEMENTATION NOTE</strong> - If the specified
207      * {@code key} identifies a write-only property, {@code null}
208      * will arbitrarily be returned, in order to avoid difficulties implementing
209      * the contracts of the {@code Map} interface.</p>
210      *
211      * @param key Key of the value to be returned
212      *
213      * @return The value for the specified key.
214      *
215      * @throws IllegalArgumentException if an exception is thrown
216      *         reading this local property value.
217      * @throws UnsupportedOperationException if this local property does not
218      *         have a read method.
219      */
220     @Override
221     public Object get(Object key) {
222         // Case 1 -- no local properties
223         if (descriptors == null) {
224             return super.get(key);
225         }
226 
227         // Case 2 -- this is a local property
228         if (key != null) {
229             final PropertyDescriptor descriptor = descriptors.get(key);
230             if (descriptor != null) {
231                 if (descriptor.getReadMethod() != null) {
232                     return readProperty(descriptor);
233                 } else {
234                     return null;
235                 }
236             }
237         }
238 
239         // Case 3 -- retrieve value from our underlying Map
240         return super.get(key);
241     }
242 
243     /**
244      * Override the default {@code Map} behavior to return
245      * {@code true} if the underlying {@code Map} only contains
246      * key-value pairs for local properties (if any).
247      *
248      * @return {@code true} if this Context is empty, otherwise
249      *         {@code false}.
250      */
251     @Override
252     public boolean isEmpty() {
253         // Case 1 -- no local properties
254         if (descriptors == null) {
255             return super.isEmpty();
256         }
257 
258         // Case 2 -- compare key count to property count
259         return super.size() <= descriptors.size();
260     }
261 
262     /**
263      * Override the default {@code Map} behavior to return a
264      * {@code Set} that meets the specified default behavior except
265      * for attempts to remove the key for a property of the {@link Context}
266      * implementation class, which will throw
267      * {@code UnsupportedOperationException}.
268      *
269      * @return The set of keys for objects in this Context.
270      */
271     public Set<String> keySet() {
272         return super.keySet();
273     }
274 
275     /**
276      * Override the default {@code Map} behavior to set the value of a
277      * local property if the specified key matches a local property name.
278      *
279      * @param key Key of the value to be stored or replaced
280      * @param value New value to be stored
281      *
282      * @return The value added to the Context.
283      *
284      * @throws IllegalArgumentException if an exception is thrown
285      *         reading or writing this local property value.
286      * @throws UnsupportedOperationException if this local property does not
287      *         have both a read method and a write method
288      */
289     @Override
290     public Object put(String key, Object value) {
291         // Case 1 -- no local properties
292         if (descriptors == null) {
293             return super.put(key, value);
294         }
295 
296         // Case 2 -- this is a local property
297         if (key != null) {
298             final PropertyDescriptor descriptor = descriptors.get(key);
299             if (descriptor != null) {
300                 Object previous = null;
301                 if (descriptor.getReadMethod() != null) {
302                     previous = readProperty(descriptor);
303                 }
304                 writeProperty(descriptor, value);
305                 return previous;
306             }
307         }
308 
309         // Case 3 -- store or replace value in our underlying map
310         return super.put(key, value);
311     }
312 
313     /**
314      * Override the default {@code Map} behavior to call the
315      * {@code put()} method individually for each key-value pair
316      * in the specified {@code Map}.
317      *
318      * @param map {@code Map} containing key-value pairs to store
319      *        (or replace)
320      *
321      * @throws IllegalArgumentException if an exception is thrown
322      *         reading or writing a local property value.
323      * @throws UnsupportedOperationException if a local property does not
324      *         have both a read method and a write method
325      */
326     @Override
327     public void putAll(Map<? extends String, ? extends Object> map) {
328         map.forEach(this::put);
329     }
330 
331     /**
332      * Override the default {@code Map} behavior to throw
333      * {@code UnsupportedOperationException} on any attempt to
334      * remove a key that is the name of a local property.
335      *
336      * @param key Key to be removed
337      *
338      * @return The value removed from the Context.
339      *
340      * @throws UnsupportedOperationException if the specified
341      *         {@code key} matches the name of a local property
342      */
343     @Override
344     public Object remove(Object key) {
345         // Case 1 -- no local properties
346         if (descriptors == null) {
347             return super.remove(key);
348         }
349 
350         // Case 2 -- this is a local property
351         if (key != null) {
352             PropertyDescriptor descriptor = descriptors.get(key);
353             if (descriptor != null) {
354                 throw new UnsupportedOperationException("Local property '" + key + "' cannot be removed");
355             }
356         }
357 
358         // Case 3 -- remove from underlying Map
359         return super.remove(key);
360     }
361 
362     /**
363      * Override the default {@code Map} behavior to return a
364      * {@code Collection} that meets the specified default behavior except
365      * for attempts to remove the key for a property of the {@link Context}
366      * implementation class, which will throw
367      * {@code UnsupportedOperationException}.
368      *
369      * @return The collection of values in this Context.
370      */
371     @Override
372     public Collection<Object> values() {
373         return new ValuesImpl();
374     }
375 
376     // --------------------------------------------------------- Private Methods
377 
378     /**
379      * Return an {@code Iterator} over the set of {@code Map.Entry}
380      * objects representing our key-value pairs.
381      *
382      * @return an {@code Iterator} over the set of {@code Map.Entry} objects
383      */
384     private Iterator<Map.Entry<String, Object>> entriesIterator() {
385         return new EntrySetIterator();
386     }
387 
388     /**
389      * Return a {@code Map.Entry} for the specified key value, if it
390      * is present; otherwise, return {@code null}.
391      *
392      * @param key Attribute key or property name
393      *
394      * @return a {@code Map.Entry} for the specified key value, if it
395      *         is present; otherwise, return {@code null}
396      */
397     private Map.Entry<String, Object> entry(Object key) {
398         if (containsKey(key)) {
399             return new MapEntryImpl(key.toString(), get(key));
400         } else {
401             return null;
402         }
403     }
404 
405     /**
406      * Get and return the value for the specified property.
407      *
408      * @param descriptor {@code PropertyDescriptor} for the
409      *        specified property
410      *
411      * @return the value of the specified property
412      *
413      * @throws IllegalArgumentException if an exception is thrown
414      *         reading this local property value.
415      * @throws UnsupportedOperationException if this local property does not
416      *         have a read method.
417      */
418     private Object readProperty(PropertyDescriptor descriptor) {
419         try {
420             Method method = descriptor.getReadMethod();
421             if (method == null) {
422                 throw new UnsupportedOperationException("Property '"
423                      + descriptor.getName() + "' is not readable");
424             }
425             return method.invoke(this);
426         } catch (Exception e) {
427             throw new UnsupportedOperationException("Exception reading property '"
428                  + descriptor.getName() + "': " + e.getMessage());
429         }
430     }
431 
432     /**
433      * Remove the specified key-value pair, if it exists, and return
434      * {@code true}. If this pair does not exist, return {@code false}.
435      *
436      * @param entry Key-value pair to be removed
437      *
438      * @return if the specified key-value pair is removed, return
439      *         {@code true}, otherwise {@code false}
440      *
441      * @throws UnsupportedOperationException if the specified key
442      *         identifies a property instead of an attribute.
443      */
444     private boolean remove(Map.Entry<?, ?> entry) {
445         Map.Entry<String, Object> actual = entry(entry.getKey());
446         if (actual == null) {
447             return false;
448         } else if (entry.equals(actual)) {
449             remove(entry.getKey());
450             return true;
451         } else {
452             return false;
453         }
454     }
455 
456     /**
457      * Return an {@code Iterator} over the set of values in this
458      * {@code Map}.
459      *
460      * @return an {@code Iterator} over the set of values in this
461      *         {@code Map}
462      */
463     private Iterator<Object> valuesIterator() {
464         return new ValuesIterator();
465     }
466 
467     /**
468      * Set the value for the specified property.
469      *
470      * @param descriptor {@code PropertyDescriptor} for the
471      *        specified property
472      * @param value The new value for this property (must be of the
473      *        correct type)
474      *
475      * @throws IllegalArgumentException if an exception is thrown
476      *         writing this local property value.
477      * @throws UnsupportedOperationException if this local property does not
478      *         have a write method.
479      */
480     private void writeProperty(PropertyDescriptor descriptor, Object value) {
481         try {
482             Method method = descriptor.getWriteMethod();
483             if (method == null) {
484                 throw new UnsupportedOperationException("Property '" + descriptor.getName()
485                      + "' is not writeable");
486             }
487             method.invoke(this, value);
488         } catch (Exception e) {
489             throw new UnsupportedOperationException("Exception writing property '"
490                  + descriptor.getName() + "': " + e.getMessage());
491         }
492     }
493 
494     /**
495      * Returns descriptors for all properties of the bean.
496      *
497      * @return descriptors for all properties of the bean
498      *         or an empty array if an problem occurs
499      */
500     private PropertyDescriptor[] getPropertyDescriptors() {
501         // Retrieve the set of property descriptors for this Context class
502         try {
503             return Introspector.getBeanInfo(getClass()).getPropertyDescriptors();
504         } catch (IntrospectionException e) {
505             return new PropertyDescriptor[0]; // Should never happen
506         }
507     }
508 
509     /**
510      * The {@code PropertyDescriptor}s for all JavaBeans properties
511      * of this {@link Context} implementation class, keyed by property
512      * name. This collection is allocated only if there are any JavaBeans
513      * properties.
514      *
515      * @return {@code PropertyDescriptor}s for all JavaBeans properties
516      *         as an collection or {@code null} if there are no JavaBeans
517      *         properties
518      */
519     private Map<String, PropertyDescriptor> getMapDescriptors() {
520         Map<String, PropertyDescriptor> ret = new HashMap<>();
521 
522         // Initialize the underlying Map contents
523         for (PropertyDescriptor pd : pds) {
524             String name = pd.getName();
525 
526             // Add descriptor (ignoring getClass() and isEmpty())
527             if (!("class".equals(name) || "empty".equals(name))) {
528                 ret.put(name, pd);
529                 super.put(name, SINGLETON);
530             }
531         }
532 
533         return ret.isEmpty() ? null : ret;
534     }
535 
536     // --------------------------------------------------------- Private Classes
537 
538     /**
539      * Private implementation of {@code Set} that implements the
540      * semantics required for the value returned by {@code entrySet()}.
541      */
542     private final class EntrySetImpl extends AbstractSet<Map.Entry<String, Object>> {
543 
544         @Override
545         public void clear() {
546             ContextBase.this.clear();
547         }
548 
549         @Override
550         public boolean contains(Object obj) {
551             if (!(obj instanceof Map.Entry)) {
552                 return false;
553             }
554             Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj;
555             Map.Entry<String, Object> actual = ContextBase.this.entry(entry.getKey());
556             if (actual != null) {
557                 return actual.equals(entry);
558             } else {
559                 return false;
560             }
561         }
562 
563         @Override
564         public boolean isEmpty() {
565             return ContextBase.this.isEmpty();
566         }
567 
568         @Override
569         public Iterator<Map.Entry<String, Object>> iterator() {
570             return ContextBase.this.entriesIterator();
571         }
572 
573         @Override
574         public boolean remove(Object obj) {
575             if (obj instanceof Map.Entry) {
576                 return ContextBase.this.remove((Map.Entry<?, ?>) obj);
577             } else {
578                 return false;
579             }
580         }
581 
582         @Override
583         public int size() {
584             return ContextBase.this.size();
585         }
586     }
587 
588     /**
589      * Private implementation of {@code Iterator} for the
590      * {@code Set} returned by {@code entrySet()}.
591      */
592     private final class EntrySetIterator implements Iterator<Map.Entry<String, Object>> {
593 
594         private Map.Entry<String, Object> entry = null;
595         private Iterator<String> keys = ContextBase.this.keySet().iterator();
596 
597         @Override
598         public boolean hasNext() {
599             return keys.hasNext();
600         }
601 
602         @Override
603         public Map.Entry<String, Object> next() {
604             entry = ContextBase.this.entry(keys.next());
605             return entry;
606         }
607 
608         @Override
609         public void remove() {
610             ContextBase.this.remove(entry);
611         }
612     }
613 
614     /**
615      * Private implementation of {@code Map.Entry} for each item in
616      * {@code EntrySetImpl}.
617      */
618     private class MapEntryImpl implements Map.Entry<String, Object> {
619 
620         private String key;
621         private Object value;
622 
623         MapEntryImpl(String key, Object value) {
624             this.key = key;
625             this.value = value;
626         }
627 
628         @Override
629         public boolean equals(Object obj) {
630             if (this == obj) {
631                 return true;
632             }
633             if (obj == null || getClass() != obj.getClass()) {
634                 return false;
635             }
636             MapEntryImpl other = (MapEntryImpl) obj;
637             if (!getEnclosingInstance().equals(other.getEnclosingInstance())) {
638                 return false;
639             }
640             return Objects.equals(key, other.key) && Objects.equals(value, other.value);
641         }
642 
643         @Override
644         public String getKey() {
645             return this.key;
646         }
647 
648         @Override
649         public Object getValue() {
650             return this.value;
651         }
652 
653         @Override
654         public int hashCode() {
655             return Objects.hashCode(key) ^ Objects.hashCode(value);
656         }
657 
658         @Override
659         public Object setValue(Object value) {
660             Object previous = this.value;
661             ContextBase.this.put(this.key, value);
662             this.value = value;
663             return previous;
664         }
665 
666         @Override
667         public String toString() {
668             return getKey() + "=" + getValue();
669         }
670 
671         private ContextBase getEnclosingInstance() {
672             return ContextBase.this;
673         }
674     }
675 
676     /**
677      * Private implementation of {@code Collection} that implements the
678      * semantics required for the value returned by {@code values()}.
679      */
680     private final class ValuesImpl extends AbstractCollection<Object> {
681 
682         @Override
683         public void clear() {
684             ContextBase.this.clear();
685         }
686 
687         @Override
688         public boolean contains(Object obj) {
689             if (!(obj instanceof Map.Entry)) {
690                 return false;
691             }
692             Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj;
693             return ContextBase.this.containsValue(entry.getValue());
694         }
695 
696         @Override
697         public boolean isEmpty() {
698             return ContextBase.this.isEmpty();
699         }
700 
701         @Override
702         public Iterator<Object> iterator() {
703             return ContextBase.this.valuesIterator();
704         }
705 
706         @Override
707         public boolean remove(Object obj) {
708             if (obj instanceof Map.Entry) {
709                 return ContextBase.this.remove((Map.Entry<?, ?>) obj);
710             } else {
711                 return false;
712             }
713         }
714 
715         @Override
716         public int size() {
717             return ContextBase.this.size();
718         }
719     }
720 
721     /**
722      * Private implementation of {@code Iterator} for the
723      * {@code Collection} returned by {@code values()}.
724      */
725     private final class ValuesIterator implements Iterator<Object> {
726 
727         private Map.Entry<String, Object> entry = null;
728         private Iterator<String> keys = ContextBase.this.keySet().iterator();
729 
730         @Override
731         public boolean hasNext() {
732             return keys.hasNext();
733         }
734 
735         @Override
736         public Object next() {
737             entry = ContextBase.this.entry(keys.next());
738             return entry.getValue();
739         }
740 
741         @Override
742         public void remove() {
743             ContextBase.this.remove(entry);
744         }
745     }
746 }