001    /*
002     * Copyright 2002-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.clazz.reflect.extended;
017    
018    import java.lang.reflect.Array;
019    import java.lang.reflect.InvocationTargetException;
020    import java.lang.reflect.Method;
021    import java.util.AbstractMap;
022    import java.util.AbstractSet;
023    import java.util.Arrays;
024    import java.util.Collection;
025    import java.util.Collections;
026    import java.util.HashMap;
027    import java.util.HashSet;
028    import java.util.Iterator;
029    import java.util.Map;
030    import java.util.Set;
031    
032    import org.apache.commons.clazz.ClazzAccessException;
033    
034    
035    /**
036     * This is an implementation of the <code>Map</code> interface
037     * that is based on a Mapped property.  Whenever possible, it
038     * uses concrete methods on the owner of the property to manipulate the map.
039     * <p>
040     * Consider the following example:
041     * <pre>
042     *      Map map = (Map)clazz.getProperty("fooMap").get(instance);
043     *      Object value = map.get("bar");
044     * </pre>
045     * 
046     * If <code>instance</code> has a <code>getFoo(String key)</code> method,
047     * this code will implicitly invoke it like this: <code>getFoo("bar")</code>.
048     * otherwise it will obtain the whole map and extract the 
049     * requested value.
050     * 
051     * @author <a href="mailto:dmitri@apache.org">Dmitri Plotnikov</a>
052     * @version $Id: ReflectedMap.java 155436 2005-02-26 13:17:48Z dirkv $
053     */
054    public class ReflectedMap extends AbstractMap {
055        private Object instance;
056        private ReflectedMappedProperty property;
057        private int modCount = 0;
058        
059        /**
060         * Constructor for ReflectedMap.
061         */
062        public ReflectedMap(
063                Object instance,
064                ReflectedMappedProperty property) 
065        {
066            this.instance = instance;
067            this.property = property;
068        }
069        
070        public Map getPropertyValue() {
071            Method readMethod = property.getReadMethod();
072            if (readMethod == null) {
073                throw new ClazzAccessException(
074                    "Cannot read property "
075                        + property.getName()
076                        + ": no read method");
077            }
078            try {
079                return (Map) readMethod.invoke(instance, null);
080            }
081            catch (Exception ex) {
082                throw accessException("Cannot read property", readMethod, ex);
083            }
084        }
085    
086        public void setPropertyValue(Map value) {
087            Method writeMethod = property.getWriteMethod();
088            if (writeMethod == null) {
089                throw new ClazzAccessException(
090                    "Cannot set property: "
091                        + property.getName()
092                        + ": no set(array) method");
093            }
094    
095            try {
096                writeMethod.invoke(instance, new Object[] { value });
097            }
098            catch (Exception ex) {
099                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    }