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.common;
017    
018    import java.lang.reflect.Array;
019    import java.lang.reflect.InvocationTargetException;
020    import java.lang.reflect.Method;
021    import java.util.AbstractList;
022    import java.util.ArrayList;
023    import java.util.Iterator;
024    import java.util.List;
025    import java.util.ListIterator;
026    
027    import org.apache.commons.clazz.ClazzAccessException;
028    
029    
030    /**
031     * This is an implementation of the <code>List</code> interface
032     * that is based on a List property.  Whenever possible, it
033     * uses concrete methods on the owner of the property to manipulate 
034     * the list or array.
035     * <p>
036     * Consider the following example:
037     * <pre>
038     *      List list = (List)clazz.getProperty("fooList").get(instance);
039     *      Object value = list.get(3);
040     * </pre>
041     * 
042     * If <code>instance</code> has a <code>getFoo(int index)</code> method,
043     * this code will implicitly invoke it like this: <code>getFoo(3)</code>,
044     * otherwise it will obtain the whole list or array and extract the 
045     * requested element.
046     * 
047     * @author <a href="mailto:dmitri@apache.org">Dmitri Plotnikov</a>
048     * @version $Id: ReflectedList.java 155436 2005-02-26 13:17:48Z dirkv $
049     */
050    public class ReflectedList extends AbstractList {
051        /*
052         * We have a concurrent modification issue with ReflectedList. We have no
053         * way of knowing if somebody has modified the property value unless it was
054         * modified through this very ReflectedList. So, in some cases we will not
055         * get a ConcurrentModificationException when we are supposed to.
056         */
057    
058        private Object instance;
059        private ReflectedListProperty property;
060    
061        private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
062    
063        /**
064         * Constructor for ReflectedList.
065         */
066        public ReflectedList(Object instance, ReflectedListProperty property) {
067            this.instance = instance;
068            this.property = property;
069        }
070    
071        public Object getPropertyValue() {
072            Method readMethod = property.getReadMethod();
073            if (readMethod == null) {
074                throw new ClazzAccessException(
075                    "Cannot read property "
076                        + property.getName()
077                        + ": no read method");
078            }
079            try {
080                return readMethod.invoke(instance, null);
081            }
082            catch (Exception ex) {
083                throw accessException("Cannot read property", readMethod, ex);
084            }
085        }
086    
087        public void setPropertyValue(Object value) {
088            Method writeMethod = property.getWriteMethod();
089            if (writeMethod == null) {
090                throw new ClazzAccessException(
091                    "Cannot set property: "
092                        + property.getName()
093                        + ": no set(array) method");
094            }
095    
096            try {
097                writeMethod.invoke(instance, new Object[] { value });
098            }
099            catch (Exception ex) {
100                throw accessException("Cannot set property", writeMethod, ex);
101            }
102        }
103    
104        /**
105         * @see java.util.Collection#size()
106         */
107        public int size() {
108            Method sizeMethod = property.getSizeMethod();
109            if (sizeMethod != null) {
110                try {
111                    Object value = sizeMethod.invoke(instance, null);
112                    return ((Integer) value).intValue();
113                }
114                catch (Exception ex) {
115                    throw accessException("Cannot get list size", sizeMethod, ex);
116                }
117            }
118            else {
119                Object list = getPropertyValue();
120                if (list == null) {
121                    return 0;
122                }
123    
124                if (list instanceof List) {
125                    return ((List) list).size();
126                }
127    
128                return Array.getLength(list);
129            }
130        }
131    
132        /**
133         * @see java.util.List#get(int)
134         */
135        public Object get(int index) {
136            Method getMethod = property.getGetMethod();
137            if (getMethod != null) {
138                Object value;
139                try {
140                    value =
141                        getMethod.invoke(
142                            instance,
143                            new Object[] { new Integer(index)});
144                }
145                catch (Throwable ex) {
146                    throw accessException("Cannot get property", getMethod, ex);
147                }
148                return value;
149            }
150            else {
151                Object list = getPropertyValue();
152                if (list == null) {
153                    return null;
154                }
155    
156                if (list instanceof List) {
157                    return ((List) list).get(index);
158                }
159    
160                return Array.get(list, index);
161            }
162        }
163    
164        /**
165         * @see java.util.Collection#iterator()
166         */
167        public Iterator iterator() {
168            return new QuickList().iterator();
169        }
170    
171        /**
172         * @see java.util.List#listIterator()
173         */
174        public ListIterator listIterator() {
175            return new QuickList().listIterator();
176        }
177    
178        /**
179         * @see java.util.List#listIterator(int)
180         */
181        public ListIterator listIterator(int index) {
182            return new QuickList().listIterator(index);
183        }
184    
185        /**
186         * @see java.util.List#set(int, java.lang.Object)
187         */
188        public Object set(int index, Object element) {
189            modCount++;
190            Method setMethod = property.getSetMethod();
191            if (setMethod != null) {
192                Object oldValue = null;
193                try {
194                    oldValue = get(index);
195                }
196                catch (Throwable t) {
197                    // Ignore
198                }
199                try {
200                    setMethod.invoke(
201                        instance,
202                        new Object[] { new Integer(index), element });
203                }
204                catch (Exception ex) {
205                    throw accessException(
206                        "Cannot set property element",
207                        setMethod,
208                        ex);
209                }
210                return oldValue;
211            }
212            else {
213                Object list = getPropertyValue();
214                if (list == null) {
215                    return null;
216                }
217    
218                if (list instanceof List) {
219                    return ((List) list).set(index, element);
220                }
221    
222                Object oldValue = Array.get(list, index);
223                Array.set(list, index, element);
224                return oldValue;
225            }
226        }
227    
228        /**
229         * Will perform the following steps:
230         * <ol>
231         * 
232         * <li>If the instance has an <code>addFoo(element)</code>, calls that
233         * method.</li>
234         * 
235         * <li>Otherwise, if the instance has an <code>add(index,element)</code>,
236         * computes the size of the list and calls <code>add(size(),element)
237         * </code>.<li>
238         * 
239         * <li>Othewise, if the instance has a <code>List getFoo<i>[plural suffix]
240         * </i>()</code> method, calls that and adds the element to the list.</li>
241         * 
242         * <li>Othewise, if the instance has a <code>Foo[] getFoo<i>[plural suffix]
243         * </i>()</code> method as well as a <code>setFoo<i>[plural suffix]
244         * </i>(Foo[])</code> method, calls the read method, copies the array into a
245         * new array with an additional element and calls the write method to assign
246         * the new array to the property.</li>
247         * 
248         * </ol>
249         * 
250         * @see java.util.Collection#add(java.lang.Object)
251         */
252        public boolean add(Object element) {
253            modCount++;
254            if (property.getAddMethod() != null) {
255                Method addMethod = property.getAddMethod();
256                try {
257                    addMethod.invoke(instance, new Object[] { element });
258                }
259                catch (Exception ex) {
260                    throw accessException(
261                        "Cannot add value to property",
262                        addMethod,
263                        ex);
264                }
265            }
266            else {
267                add(-1, element);
268            }
269            return true;
270        }
271    
272        /**
273         * Will perform the following steps:
274         * <ol>
275         * <li>If the instance has an <code>add(index,element)</code>,
276         * calls that method.<li>
277         * 
278         * <li>Othewise, if the instance has an <code>add(element)</code> method and
279         * index == size(), calls that method.</li>
280         * 
281         * <li>Othewise, if the instance has a <code>List getFoo<i> [plural
282         * suffix]</i>()</code> method, calls that and inserts the element into the
283         * list.</li>
284         * 
285         * <li>Othewise, if the instance has a <code>Foo[] getFoo<i>[plural suffix]
286         * </i>()</code> method as well as a <code>setFoo<i>[plural suffix]
287         * </i>(Foo[])</code> method, calls the read method, copies the array into a
288         * new, one-longer, array inserting the additional element and calls the
289         * write method to assign the new array to the property.</li>
290         * 
291         * </ol>
292         * 
293         * @see java.util.List#add(int, java.lang.Object)
294         */
295        public void add(int index, Object element) {
296            modCount++;
297            if (property.getAddIndexedMethod() != null) {
298                if (index == -1) { // This would indicate that the call
299                    // is coming from add(element) and
300                    // the addMethod does not exist
301                    index = size();
302                }
303                Method addIndexedMethod = property.getAddIndexedMethod();
304                try {
305                    addIndexedMethod.invoke(
306                        instance,
307                        new Object[] { new Integer(index), element });
308                }
309                catch (Exception ex) {
310                    throw accessException(
311                        "Cannot add value to property",
312                        addIndexedMethod,
313                        ex);
314                }
315                return;
316            }
317    
318            if (property.getAddMethod() != null && index == size()) { // This
319                // guarantees that the call is not coming from add(element),
320                // therefore we can call it without the fear of recursion
321                add(element);
322            }
323            else if (property.getType().isArray()) {
324                addToArray(index, element);
325            }
326            else {
327                addToList(index, element);
328            }
329        }
330    
331        /**
332         * Inserts a new element into an array.  Creates the array if necessary. 
333         */
334        private void addToArray(int index, Object element) {
335            Object newList;
336            Object list = getPropertyValue();
337            if (list == null) {
338                if (index != 0 && index != -1) {
339                    throw new ArrayIndexOutOfBoundsException(
340                        "Size: 0; Index: " + index);
341                }
342                Class contentType = property.getContentType();
343                newList = Array.newInstance(contentType, 1);
344                Array.set(newList, 0, element);
345            }
346            else {
347                Class contentType = property.getContentType();
348                int size = Array.getLength(list);
349                if (index == -1) {
350                    index = size;
351                }
352                if (index < 0 || index > size) {
353                    throw new ArrayIndexOutOfBoundsException(
354                        "Size: " + size + "; Index: " + index);
355                }
356                newList = Array.newInstance(contentType, size + 1);
357                System.arraycopy(list, 0, newList, 0, index);
358                Array.set(newList, index, element);
359                System.arraycopy(list, index, newList, index + 1, size - index);
360            }
361    
362            setPropertyValue(newList);
363        }
364    
365        /**
366         * Inserts a new element into an List.  Creates the list if necessary.
367         */
368        private void addToList(int index, Object element) {
369            Object list = getPropertyValue();
370            if (list == null) {
371                List newList;
372                Class type = property.getType();
373                if (!type.isInterface()) {
374                    try {
375                        newList = (List) type.newInstance();
376                    }
377                    catch (Exception ex) {
378                        throw new ClazzAccessException(
379                            "Cannot add value to property : "
380                                + property.getName()
381                                + ": cannot create List of type "
382                                + type.getName(),
383                            ex);
384                    }
385                }
386                else {
387                    newList = new ArrayList();
388                }
389                newList.add(element);
390    
391                setPropertyValue(newList);
392            }
393            else if (index == -1) {
394                ((List) list).add(element);
395            }
396            else {
397                ((List) list).add(index, element);
398            }
399        }
400    
401        /**
402         * Will perform the following steps:
403         * <ol>
404         *
405         * <li>If the instance has an <code>removeFoo(element)</code>, calls that
406         * method.</li>
407         *
408         * <li>Otherwise, if iterates over elements of the collection until it
409         * finds one equal to the supplied value. Then it removes it by calling
410         * <code>remove(index)</code><li>
411         *
412         * </ol>
413         *
414         * @see java.util.Collection#remove(java.lang.Object)
415         */
416        public boolean remove(Object element) {
417            modCount++;
418            if (property.getRemoveMethod() != null) {
419                Method removeMethod = property.getRemoveMethod();
420                try {
421                    removeMethod.invoke(instance, new Object[] { element });
422                }
423                catch (Exception ex) {
424                    throw accessException(
425                        "Cannot remove value from property",
426                        removeMethod,
427                        ex);
428                }
429                return true; // @todo: we really don't know if it got removed
430            }
431            else {
432                return super.remove(element);
433            }
434        }
435    
436        /**
437         * Will perform the following steps:
438         * <ol>
439         * <li>If the instance has a <code>remove(index)</code>, calls that method.
440         * <li>
441         *
442         * <li>Othewise, if the instance has an <code>add(element)</code> method and
443         * index == size(), calls that method.</li>
444         *
445         * <li>Othewise, if the instance has a <code>List getFoo<i> [plural
446         * suffix]</i>()</code> method, calls that and removes the element from the
447         * list.
448         * </li>
449         *
450         * <li>Othewise, if the instance has a <code>Foo[] getFoo<i>[plural suffix]
451         * </i>()</code> method as well as a <code>setFoo<i>[plural suffix]
452         * </i>(Foo[])</code> method, calls the read method, copies the array into a
453         * new, one-shorter, array removing the supplied element and calls the write
454         * method to assign the new array to the property.</li>
455         *
456         * </ol>
457         *
458         * @see java.util.List#add(int, java.lang.Object)
459         */
460        public Object remove(int index) {
461            modCount++;
462    
463            if (property.getRemoveIndexedMethod() != null) {
464                Object value = null;
465    
466                try {
467                    value = get(index);
468                }
469                catch (Throwable t) {
470                    // Ignore
471                }
472    
473                Method removeIndexedMethod = property.getRemoveIndexedMethod();
474                try {
475                    removeIndexedMethod.invoke(
476                        instance,
477                        new Object[] { new Integer(index)});
478                }
479                catch (Exception ex) {
480                    throw accessException(
481                        "Cannot remove value from property",
482                        removeIndexedMethod,
483                        ex);
484                }
485    
486                return value;
487            }
488    
489            Object list = getPropertyValue();
490            if (list == null) {
491                throw new ArrayIndexOutOfBoundsException(
492                    "Size: 0; Index: " + index);
493            }
494            else if (property.getType().isArray()) {
495                Object value;
496                Class contentType = property.getContentType();
497                int size = Array.getLength(list);
498                if (index < 0 || index >= size) {
499                    throw new ArrayIndexOutOfBoundsException(
500                        "Size: " + size + "; Index: " + index);
501                }
502                value = Array.get(list, index);
503    
504                Object newList = Array.newInstance(contentType, size - 1);
505                System.arraycopy(list, 0, newList, 0, index);
506                System.arraycopy(list, index + 1, newList, index, size - index - 1);
507                setPropertyValue(newList);
508                return value;
509            }
510            else {
511                return ((List) list).remove(index);
512            }
513        }
514    
515        private RuntimeException accessException(
516            String message,
517            Method method,
518            Throwable ex) 
519        {
520            if (ex instanceof InvocationTargetException) {
521                ex = ((InvocationTargetException) ex).getTargetException();
522            }
523    
524            // Just re-throw all runtime exceptions - there is really no
525            // point in wrapping them 
526            if (ex instanceof RuntimeException) {
527                throw (RuntimeException) ex;
528            }
529            if (ex instanceof Error) {
530                throw (Error) ex;
531            }
532    
533            throw new ClazzAccessException(
534                message
535                    + ": "
536                    + property.getName()
537                    + ": cannot invoke method: "
538                    + method.getName(),
539                ex);
540        }
541    
542        private int getModCount() {
543            return modCount;
544        }
545    
546        /**
547         * QuickList is used exclusively as short-lived helper object for
548         * an optimization of Iterator.
549         * 
550         * The point is to avoid requesting the collection or its size from the
551         * instance on every step of the iteration. The instance may be creating the
552         * collection every time we ask for it, or it may be wrapping it into an
553         * unmodifiable list. We want to avoid that overhead. The computation of
554         * size can be rather expensive too.
555         * 
556         * A QuickList maintains temporary cache of the reflected collection and its
557         * size.
558         */
559        private class QuickList extends AbstractList {
560            private Object list;
561            private int size;
562    
563            public QuickList() {
564                update();
565            }
566    
567            public void refresh() {
568                // If QuickList is out of sync with the parent ReflectedList,
569                // update the cached list and size
570                if (super.modCount != getModCount()) {
571                    update();
572                }
573            }
574    
575            public void update() {
576                // Make sure modCount of QuickList is maintained in sync with
577                // that of the embracing List 
578                super.modCount = getModCount();
579    
580                if (property.getReadMethod() != null) {
581                    list = getPropertyValue();
582                    if (list == null) {
583                        size = 0;
584                    }
585                    else if (list instanceof List) {
586                        size = ((List) list).size();
587                    }
588                    else {
589                        size = Array.getLength(list);
590                    }
591                }
592                else {
593                    list = NOT_ACCESSIBLE;
594                    size = ReflectedList.this.size();
595                }
596            }
597    
598            public int size() {
599                refresh();
600                return size;
601            }
602    
603            public Object get(int index) {
604                refresh();
605                if (list != NOT_ACCESSIBLE) {
606                    if (list == null) {
607                        throw new IndexOutOfBoundsException(
608                            "Size= 0; index=" + index);
609                    }
610    
611                    if (list instanceof List) {
612                        return ((List) list).get(index);
613                    }
614    
615                    return Array.get(list, index);
616                }
617                else {
618                    return ReflectedList.this.get(index);
619                }
620            }
621    
622            public Object set(int index, Object value) {
623                return ReflectedList.this.set(index, value);
624            }
625    
626            public void add(int index, Object value) {
627                ReflectedList.this.add(index, value);
628            }
629    
630            public Object remove(int index) {
631                return ReflectedList.this.remove(index);
632            }
633    
634        }
635    
636        private static final Object NOT_ACCESSIBLE = new Object();
637    }