View Javadoc

1   /*
2    * Copyright 2002-2004 The Apache Software Foundation
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.apache.commons.clazz.reflect.extended;
17  
18  import java.lang.reflect.Array;
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.util.AbstractMap;
22  import java.util.AbstractSet;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import org.apache.commons.clazz.ClazzAccessException;
33  
34  
35  /**
36   * This is an implementation of the <code>Map</code> interface
37   * that is based on a Mapped property.  Whenever possible, it
38   * uses concrete methods on the owner of the property to manipulate the map.
39   * <p>
40   * Consider the following example:
41   * <pre>
42   *      Map map = (Map)clazz.getProperty("fooMap").get(instance);
43   *      Object value = map.get("bar");
44   * </pre>
45   * 
46   * If <code>instance</code> has a <code>getFoo(String key)</code> method,
47   * this code will implicitly invoke it like this: <code>getFoo("bar")</code>.
48   * otherwise it will obtain the whole map and extract the 
49   * requested value.
50   * 
51   * @author <a href="mailto:dmitri@apache.org">Dmitri Plotnikov</a>
52   * @version $Id: ReflectedMap.java 155436 2005-02-26 13:17:48Z dirkv $
53   */
54  public class ReflectedMap extends AbstractMap {
55      private Object instance;
56      private ReflectedMappedProperty property;
57      private int modCount = 0;
58      
59      /**
60       * Constructor for ReflectedMap.
61       */
62      public ReflectedMap(
63              Object instance,
64              ReflectedMappedProperty property) 
65      {
66          this.instance = instance;
67          this.property = property;
68      }
69      
70      public Map getPropertyValue() {
71          Method readMethod = property.getReadMethod();
72          if (readMethod == null) {
73              throw new ClazzAccessException(
74                  "Cannot read property "
75                      + property.getName()
76                      + ": no read method");
77          }
78          try {
79              return (Map) readMethod.invoke(instance, null);
80          }
81          catch (Exception ex) {
82              throw accessException("Cannot read property", readMethod, ex);
83          }
84      }
85  
86      public void setPropertyValue(Map value) {
87          Method writeMethod = property.getWriteMethod();
88          if (writeMethod == null) {
89              throw new ClazzAccessException(
90                  "Cannot set property: "
91                      + property.getName()
92                      + ": no set(array) method");
93          }
94  
95          try {
96              writeMethod.invoke(instance, new Object[] { value });
97          }
98          catch (Exception ex) {
99              throw accessException("Cannot set property", writeMethod, ex);
100         }
101     }
102 
103     public Set getPropertyKeySet() {
104         Method keySetMethod = property.getKeySetMethod();
105         if (keySetMethod != null) {
106             Set set;
107             try {
108                 Object value = keySetMethod.invoke(instance, null);
109                 if (value == null) {
110                     set = Collections.EMPTY_SET;
111                 }
112                 else if (value instanceof Set) {
113                     set = (Set) value;
114                 }
115                 else if (value instanceof Collection) {
116                     set = new ConcurrentChangeSafeSet((Collection) value);
117                 }
118                 else {
119                     set =
120                         new ConcurrentChangeSafeSet(
121                             Arrays.asList((Object[]) value));
122                 }
123             }
124             catch (Exception ex) {
125                 throw new ClazzAccessException(
126                     "Cannot get key set: "
127                         + property.getName()
128                         + ": cannot invoke method: "
129                         + keySetMethod.getName(),
130                     ex);
131             }
132             return set;
133         }
134         else {
135             Map map = getPropertyValue();
136             if (map == null) {
137                 return Collections.EMPTY_SET;
138             }
139             return map.keySet();
140         }
141     }
142 
143     /**
144      * If there is a getFoo(key) method, calls that for every key.
145      * Otherwise, calls getFooMap().get(key)
146      * 
147      * @see java.util.Map#get(java.lang.Object)
148      */
149     public Object get(Object key) {
150         Method getMethod = property.getGetMethod();
151         if (getMethod != null) {
152             Object value;
153             try {
154                 value = getMethod.invoke(instance, new Object[]{key});
155             }
156             catch (Exception ex) {
157                 throw new ClazzAccessException(
158                     "Cannot get property : "
159                         + property.getName()
160                         + ": cannot invoke method: "
161                         + getMethod.getName(),
162                     ex);
163             }
164             return value;
165         }
166         else {
167             Map map = getPropertyValue();
168             if (map == null) {
169                 return null;
170             }
171             return map.get(key);
172         }
173     }
174 
175     /**
176      * @see java.util.Map#size()
177      */
178     public int size() {
179         Method keySetMethod = property.getKeySetMethod();
180         if (keySetMethod != null) {
181             try {
182                 Object value = keySetMethod.invoke(instance, null);
183                 if (value == null) {
184                     return 0;
185                 }
186                 else if (value instanceof Collection) {
187                     return ((Collection) value).size();
188                 }
189                 else {
190                     return Array.getLength(value);
191                 }
192             }
193             catch (Exception ex) {
194                 throw accessException("Cannot get key set", keySetMethod, ex);
195             }
196         }
197         else {
198             Map map = getPropertyValue();
199             if (map == null) {
200                 return 0;
201             }
202             return map.size();
203         }
204     }
205 
206     /**
207      * @see java.util.Map#isEmpty()
208      */
209     public boolean isEmpty() {
210         return size() == 0;
211     }
212 
213     /**
214      * @see java.util.Map#keySet()
215      */
216     public Set keySet() {
217         return new EntrySet(KEYS);
218     }
219     
220     /**
221      * @see java.util.Map#entrySet()
222      */
223     public Set entrySet() {
224         return new EntrySet(ENTRIES);
225     }
226     
227     /**
228      * @see java.util.Map#put(java.lang.Object, java.lang.Object)
229      */
230     public Object put(Object key, Object value) {        
231         Method putMethod = property.getPutMethod();
232         if (putMethod != null) {
233             Object oldValue = null;
234             try {
235                 oldValue = get(key);
236             }
237             catch (Throwable t) {
238                 // Ignore
239             }
240             
241             try {
242                 putMethod.invoke(instance, new Object[]{key, value});
243             }
244             catch (Exception ex) {
245                 throw new ClazzAccessException(
246                     "Cannot set property : "
247                         + property.getName()
248                         + ": cannot invoke method: "
249                         + putMethod.getName(),
250                     ex);
251             }
252             return oldValue;
253         }
254         else {
255             Map map = getPropertyValue();
256             if (map == null) {
257                 map = new HashMap();
258                 setPropertyValue(map);
259             }
260             return map.put(key, value);
261         }
262     }
263 
264     /**
265      * @see java.util.Map#remove(java.lang.Object)
266      */
267     public Object remove(Object key) {
268         Method removeMethod = property.getRemoveMethod();
269         if (removeMethod != null) {
270             Object oldValue = null;
271             try {
272                 oldValue = get(key);
273             }
274             catch (Throwable t) {
275                 // Ignore
276             }
277 
278             try {
279                 removeMethod.invoke(instance, new Object[]{key});
280             }
281             catch (Exception ex) {
282                 throw new ClazzAccessException(
283                     "Cannot set property : "
284                         + property.getName()
285                         + ": cannot invoke method: "
286                         + removeMethod.getName(),
287                     ex);
288             }
289             return oldValue;
290         }
291         else {
292             Map map = getPropertyValue();
293             if (map != null) {
294                 return map.remove(key);
295             }
296             return null;
297         }
298     }
299     
300     private RuntimeException accessException(
301         String message,
302         Method method,
303         Throwable ex)
304     {
305         if (ex instanceof InvocationTargetException) {
306             ex = ((InvocationTargetException) ex).getTargetException();
307         }
308 
309         // Just re-throw all runtime exceptions - there is really no
310         // point in wrapping them
311         if (ex instanceof RuntimeException) {
312             throw (RuntimeException) ex;
313         }
314         if (ex instanceof Error) {
315             throw (Error) ex;
316         }
317 
318         throw new ClazzAccessException(
319             message
320                 + ": "
321                 + property.getName()
322                 + ": cannot invoke method: "
323                 + method.getName(),
324             ex);
325     }
326 
327     private static final int ENTRIES = 0;
328     private static final int KEYS = 1;
329     
330     /**
331      * An implementation of Set that delegates object deletion to the
332      * encompassing ReflectedMap.
333      */
334     private class EntrySet extends AbstractSet {
335         private int type;
336         private int modCount = -1;
337         private Set keySet;
338         private int size;        
339 
340         public EntrySet(int type) {
341             this.type = type;
342             update();            
343         }
344 
345         public void refresh() {
346             // If EntrySet is out of sync with the 
347             // parent ReflectedMap, update the cached key set and size
348             if (modCount != ReflectedMap.this.modCount) {
349                 update();
350             }
351         }
352 
353         public void update() {
354             // Make sure modCount of EntrySet is maintained in sync with
355             // that of the embracing List
356             modCount = ReflectedMap.this.modCount;
357             
358             keySet = ReflectedMap.this.getPropertyKeySet();
359             size = keySet.size();
360         }
361         
362         public int size() {
363             refresh();
364             return size;
365         }
366         
367         public Iterator iterator() {
368             refresh();
369             return new EntryIterator(keySet, type);
370         }
371         
372         public boolean remove(Object object) {
373             refresh();
374             Object key =
375                 (type == KEYS ? object : ((Map.Entry) object).getKey());
376                 
377             boolean exists = true;
378             try {
379                 exists = keySet.contains(key);
380             }
381             catch (Throwable t) {
382                 // Ignore
383             }
384             if (exists) {
385                 ReflectedMap.this.remove(key);
386             }
387             return exists;
388         }
389     }
390     
391     /**
392      * An implementation of Iterator that delegates object deletion to the
393      * encompassing ReflectedMap.
394      */
395     private class EntryIterator implements Iterator {
396         private int type;
397         private Set keySet;
398         private Iterator keyIterator;
399         private Object lastReturned = UNINITIALIZED;
400         
401         public EntryIterator(Set keySet, int type) {
402             this.type = type;
403             this.keySet = keySet;
404             this.keyIterator = keySet.iterator();
405         }
406         
407         /**
408          * @see java.util.Iterator#hasNext()
409          */
410         public boolean hasNext() {
411             return keyIterator.hasNext();
412         }
413 
414         /**
415          * @see java.util.Iterator#next()
416          */
417         public Object next() {            
418             lastReturned = keyIterator.next();
419             if (type == KEYS) {
420                 return lastReturned;
421             }
422             else {
423                 return new Entry(lastReturned);
424             }
425         }
426 
427         /**
428          * @see java.util.Iterator#remove()
429          */
430         public void remove() {
431             if (lastReturned == UNINITIALIZED) {
432                 throw new IllegalStateException();
433             }
434             ensureConcurrentChangeSafety();
435             ReflectedMap.this.remove(lastReturned);
436         }
437 
438         /**
439          * This is called when we are about to delete a key from the map.
440          * The method checks if the set of keys we are iterating over is in fact
441          * a copy of the set of keys of the original collection.  If it is not,
442          * the method creates such copy and re-executes the iteration steps that
443          * have already been made.  The assumption made here is that as long as
444          * set remains unchanged, an iteration will always present elements in
445          * the same order (which is not to say that the order is predicatble).
446          */
447         private void ensureConcurrentChangeSafety() {
448             if (!(keySet instanceof ConcurrentChangeSafeSet)) {
449                 keySet = new ConcurrentChangeSafeSet(keySet);
450                 keyIterator = keySet.iterator(); 
451                 while (keyIterator.hasNext()) {
452                     Object key = keyIterator.next();
453                     if ((key == null && lastReturned == null)
454                         || (key != null && key.equals(lastReturned))) {
455                         return;
456                     }
457                 }
458                 throw new IllegalStateException(
459                     "The second iteration over the key set"
460                         + " did not produce the same elements");
461             }
462         }
463     }
464     
465     private static final Object UNINITIALIZED = new Object();
466     
467     /**
468      * An implementation of Map.Entry that maintains a key and gets the value
469      * from the property, if needed.
470      */
471     private class Entry implements Map.Entry {
472         private Object key;
473         
474         public Entry(Object key) {
475             this.key = key;            
476         }
477         
478         /**
479          * @see java.util.Map.Entry#getKey()
480          */
481         public Object getKey() {
482             return key;
483         }
484         /**
485          * @see java.util.Map.Entry#getValue()
486          */
487         public Object getValue() {
488             return ReflectedMap.this.get(key);
489         }
490         
491         /**
492          * @see java.util.Map.Entry#setValue(java.lang.Object)
493          */
494         public Object setValue(Object value) {
495             return ReflectedMap.this.put(key, value);
496         }        
497 
498         public boolean equals(Object o) {
499             if (!(o instanceof Map.Entry)) {
500                 return false;
501             }
502             
503             Map.Entry e = (Map.Entry) o;
504             return (key == null ? e.getKey() == null : key.equals(e.getKey()));
505         }
506 
507         public int hashCode() {
508             return key == null ? 0 : key.hashCode();
509         }
510 
511         public String toString() {
512             return key + "=" + getValue();
513         }        
514     }
515     
516     /**
517      * This is a simple HashSet. We are only introducing the subclass as a
518      * marker of the fact that we created the set rather than getting it
519      * directly from the map value of the property.
520      */
521     private static class ConcurrentChangeSafeSet extends HashSet {
522         public ConcurrentChangeSafeSet(Collection collection) {
523             super(collection);
524         }
525     }
526 }