View Javadoc

1   /*
2    * Copyright (c) 1998, 2005 Gargoyle Software Inc. All rights reserved.
3    *
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted provided that the following conditions are met:
6    *
7    * 1. Redistributions of source code must retain the above copyright notice,
8    *    this list of conditions and the following disclaimer.
9    * 2. Redistributions in binary form must reproduce the above copyright notice,
10   *    this list of conditions and the following disclaimer in the documentation
11   *    and/or other materials provided with the distribution.
12   * 3. The end-user documentation included with the redistribution, if any, must
13   *    include the following acknowledgment:
14   *
15   *       "This product includes software developed by Gargoyle Software Inc.
16   *        (http://www.GargoyleSoftware.com/)."
17   *
18   *    Alternately, this acknowledgment may appear in the software itself, if
19   *    and wherever such third-party acknowledgments normally appear.
20   * 4. The name "Gargoyle Software" must not be used to endorse or promote
21   *    products derived from this software without prior written permission.
22   *    For written permission, please contact info@GargoyleSoftware.com.
23   * 5. Products derived from this software may not be called "GSBase", nor may
24   *    "GSBase" appear in their name, without prior written permission of
25   *    Gargoyle Software Inc.
26   *
27   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
28   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
29   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
30   * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
31   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
33   * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
34   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
36   * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37   */
38  package com.gargoylesoftware.base.gui;
39  
40  import com.gargoylesoftware.base.collections.NotificationList;
41  import com.gargoylesoftware.base.collections.NotificationListEvent;
42  import com.gargoylesoftware.base.collections.NotificationListListener;
43  import com.gargoylesoftware.base.trace.Trace;
44  import com.gargoylesoftware.base.trace.TraceChannel;
45  import com.gargoylesoftware.base.util.DetailedIllegalArgumentException;
46  import com.gargoylesoftware.base.util.DetailedNullPointerException;
47  import java.beans.BeanInfo;
48  import java.beans.IntrospectionException;
49  import java.beans.Introspector;
50  import java.beans.PropertyChangeEvent;
51  import java.beans.PropertyChangeListener;
52  import java.beans.PropertyDescriptor;
53  import java.lang.reflect.InvocationTargetException;
54  import java.lang.reflect.Method;
55  import java.util.ArrayList;
56  import java.util.Collections;
57  import java.util.HashMap;
58  import java.util.List;
59  import java.util.Map;
60  import javax.swing.table.AbstractTableModel;
61  import java.util.Iterator;
62  
63  /***
64   * A table model that uses reflection to retrieve values out of the row objects.
65   * <p>
66   * The sample below will create a JTable with one row of data and one column per property
67   * in the Date class (Date has 10 properties in JDK1.3).
68   * <pre>
69   * final JTable table = new JTable();
70   * final ReflectedTableModel model = new ReflectedTableModel(Date.class);
71   * model.getRows().add( new Date() );
72   * table.setModel(model);
73   * </pre>
74   *
75   * This sample will only provide columns for month and year.
76   *
77   * <pre>
78   * final JTable table = new JTable();
79   * final ReflectedTableModel model = new ReflectedTableModel();
80   * model.getRows().add( new Date() );
81   * model.getColumns().add( new ReflectedTableModel.ColumnInfo("month") );
82   * model.getColumns().add( new ReflectedTableModel.ColumnInfo("year") );
83   * table.setModel(model);
84   * </pre>
85   * <b>Tip: </b>To enable debugging information call {@link #setTraceChannel(TraceChannel)}
86   * with a non-null TraceChannel.
87   * <pre>
88   * model.setTraceChannel(Trace.out)
89   * </pre>
90   *
91   * @version $Revision: 1.8 $
92   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
93   */
94  public class ReflectedTableModel extends AbstractTableModel {
95  //TODO: Register ourselves as PropertyChangeListeners on the row objects so
96  //      that we know when to fire a model changed event.
97  //TODO: Listen for changes to the row object list so we can fire model
98  //      changed.
99  //TODO: Listen for changes to the column objects so we can update the model.
100 
101     private static final long serialVersionUID = -7537209244601820035L;
102 
103 	/***
104      * This class contains information about one specific column in the table.
105      */
106     public static class ColumnInfo {
107         private final String columnName_;
108         private final String propertyName_;
109 
110         /***
111          * Create a new ColumnInfo with the specified column name and property name.
112          * @param columnName The name used by the table
113          * @param propertyName The name of the property that we will get the data from.
114          */
115         public ColumnInfo( final String columnName, final String propertyName ) {
116             if( columnName == null ) {
117                 throw new DetailedNullPointerException("columnName");
118             }
119             if( propertyName == null ) {
120                 throw new DetailedNullPointerException("propertyName");
121             }
122 
123             columnName_ = columnName;
124             propertyName_ = propertyName;
125         }
126 
127         /***
128          * Create a new ColumnInfo where the column name and property name are the same.
129          * @param name The name.
130          */
131         public ColumnInfo( final String name ) {
132             this( name, name );
133         }
134 
135         /***
136          * Return the column name.
137          * @return The column name.
138          */
139         public String getColumnName() {
140             return columnName_;
141         }
142 
143         /***
144          * Return the property name
145          * @return The property name.
146          */
147         public String getPropertyName() {
148             return propertyName_;
149         }
150     }
151 
152     /***
153      * If the row list changes then fire the appropriate table event.
154      */
155     private NotificationListListener rowListener_
156         = new NotificationListListener() {
157 
158         public void listElementsAdded( final NotificationListEvent event ) {
159             addRowElements( event.getNewValues() );
160             fireTableRowsInserted( event.getStartIndex(), event.getEndIndex() );
161         }
162         public void listElementsRemoved( final NotificationListEvent event ) {
163             removeRowElements( event.getOldValues() );
164             fireTableRowsDeleted( event.getStartIndex(), event.getEndIndex() );
165         }
166         public void listElementsChanged( final NotificationListEvent event ) {
167             removeRowElements( event.getOldValues() );
168             addRowElements( event.getNewValues() );
169             fireTableRowsUpdated( event.getStartIndex(), event.getEndIndex() );
170         }
171     };
172 
173     /***
174      * If any change occurs to the columns then fire a structure changed.
175      */
176     private NotificationListListener columnListener_
177         = new NotificationListListener() {
178 
179         public void listElementsAdded( final NotificationListEvent event ) {
180             fireTableStructureChanged();
181         }
182         public void listElementsRemoved( final NotificationListEvent event ) {
183             fireTableStructureChanged();
184         }
185         public void listElementsChanged( final NotificationListEvent event ) {
186             fireTableStructureChanged();
187         }
188     };
189 
190     /***
191      *
192      */
193     private PropertyChangeListener propertyChangeListener_
194         = new PropertyChangeListener() {
195 
196         public void propertyChange( final PropertyChangeEvent event ) {
197             final int row = rows_.indexOf( event.getSource() );
198             final int columnCount = columns_.size();
199             final String propertyName = event.getPropertyName();
200             ColumnInfo columnInfo;
201 
202             int i;
203             for( i=0; i<columnCount; i++ ) {
204                 columnInfo = (ColumnInfo)columns_.get(i);
205                 if( columnInfo.getPropertyName().equals(propertyName) ) {
206                     fireTableCellUpdated( row, i );
207                     if( traceChannel_ != null ) {
208                         Trace.println(traceChannel_,
209                                       "ReflectedTableModel property changed:"
210                                       + " property=" + propertyName
211                                       + " row=" +row
212                                       + " column=" + i);
213                     }
214                 }
215             }
216         }
217     };
218 
219     private class RowElementControlData {
220         /*** How many times has this element been placed in the list.  Used to ensure
221          * that we only add/remove a PropertyChangeListener once per object.
222          */
223         public int instanceCounter_ = 0;
224 
225         /*** This will only be true if the specified object has both add and remove
226          * methods for PropertyChangeListeners.
227          */
228         public boolean supportsPropertyChangeEvents_ = false;
229     }
230 
231     // Keys are the row elements, values are instances of RowElementControlData
232     private final Map rowElementControlDatas_ = new HashMap();
233 
234     private final List rows_;
235     private final List columns_;
236 
237     private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
238 
239     // If non-null then tracing will be enabled.
240     private TraceChannel traceChannel_ = null;
241 
242     /***
243      * Create an empty model with no columns and no rows.
244      */
245     public ReflectedTableModel() {
246 
247         final NotificationList columnList = new NotificationList( new ArrayList() );
248         columnList.addNotificationListListener(columnListener_);
249         columns_ = Collections.synchronizedList( columnList );
250 
251         final NotificationList rowList = new NotificationList( new ArrayList() );
252         rowList.addNotificationListListener(rowListener_);
253         rows_ = Collections.synchronizedList( rowList );
254     }
255 
256     /***
257      * Create an empty model with no rows but the columns preset to match the
258      * properties in the given class.
259      *
260      * @param clazz The class to get properties from.
261      * @throws IntrospectionException If the Introspector is unable to get
262      * the properties for this class.
263      */
264     public ReflectedTableModel( final Class clazz ) throws IntrospectionException {
265         this();
266         final BeanInfo beanInfo = Introspector.getBeanInfo( clazz );
267         final PropertyDescriptor propertyDescriptors[]
268         = beanInfo.getPropertyDescriptors();
269 
270         String propertyName;
271 
272         int i;
273         for( i=0; i<propertyDescriptors.length; i++ ) {
274             propertyName = propertyDescriptors[i].getName();
275 
276             if( traceChannel_ != null ) {
277                 Trace.println( traceChannel_,
278                                "ReflectedTableModel(Class) adding column: ["
279                                + propertyName +"]" );
280             }
281             columns_.add( new ColumnInfo( propertyName ) );
282         }
283     }
284 
285     /***
286      * Return a list containing the objects that are used to create each row.  This
287      * list is backed by the original store such that changes to this list will be
288      * reflected in the table model.
289      * @return The rows.
290      */
291     public List getRows() {
292         return rows_;
293     }
294 
295     /***
296      * Return a list containing the ColumnInfo objects that are used to define each
297      * column.  This list is backed by the original store such that changes to this
298      * list will be reflected in the table model.
299      * @return The columns.
300      */
301     public List getColumns() {
302         return columns_;
303     }
304 
305     /***
306      * Return the number of columns.
307      * @return the number of columns.
308      */
309     public int getColumnCount() {
310         return columns_.size();
311     }
312 
313     /***
314      * Return the number of rows.
315      * @return The number of rows.
316      */
317     public int getRowCount() {
318         return rows_.size();
319     }
320 
321     /***
322      * Return the specified object.
323      * @param rowIndex The row index
324      * @param columnIndex The columnIndex
325      * @return The object at the specified row and column.
326      */
327     public Object getValueAt( final int rowIndex, final int columnIndex ) {
328         final Object rowObject = rows_.get(rowIndex);
329         final ColumnInfo columnInfo = (ColumnInfo)columns_.get(columnIndex);
330         final String propertyName = columnInfo.getPropertyName();
331 
332         try {
333             final BeanInfo beanInfo = Introspector.getBeanInfo( rowObject.getClass() );
334             final PropertyDescriptor propertyDescriptors[]
335             = beanInfo.getPropertyDescriptors();
336 
337             int i;
338             for( i=0; i<propertyDescriptors.length; i++ ) {
339                 if( propertyDescriptors[i].getName().equals( propertyName ) ) {
340                     final Method readMethod = propertyDescriptors[i].getReadMethod();
341                     return readMethod.invoke( rowObject, EMPTY_OBJECT_ARRAY );
342                 }
343             }
344         }
345         catch( final Exception e ) {
346             if( traceChannel_ != null ) {
347                 Trace.printStackTrace( traceChannel_, e );
348             }
349         }
350 
351         return null;
352     }
353 
354     /***
355      * Return the name of the column at the specified index.
356      * @param index The index of the column.
357      * @return The name of the column at the specified index.
358      */
359     public String getColumnName( final int index ) {
360         return((ColumnInfo)columns_.get(index)).getColumnName();
361     }
362 
363     /***
364      * Set the channel to be used for tracing.
365      *
366      * @param channel The channel to be used for tracing or null if tracing is
367      * to be disabled.
368      */
369     public void setTraceChannel( final TraceChannel channel ) {
370         traceChannel_ = channel;
371     }
372 
373     /***
374      * Return the channel currently being used for tracing or null if tracing
375      * is disabled.
376      * @return The trace channel or null if a channel hasn't been set.
377      */
378     public TraceChannel getTraceChannel() {
379         return traceChannel_;
380     }
381 
382     /***
383      * Add a row element
384      * @param object the object that will be used to populate this row
385      */
386     private synchronized void addRowElement( final Object object ) {
387 
388         RowElementControlData data =
389         (RowElementControlData)rowElementControlDatas_.get(object);
390 
391         if( data == null ) {
392             data = new RowElementControlData();
393 
394             final Class clazz = object.getClass();
395             final Class parms[] = { PropertyChangeListener.class};
396             Method method;
397             try {
398                 // We try the "remove" first just to ensure that there is one.
399                 method = clazz.getMethod("removePropertyChangeListener", parms);
400                 method = clazz.getMethod("addPropertyChangeListener", parms);
401                 method.invoke(object, new Object[]{propertyChangeListener_} );
402             }
403             catch( NoSuchMethodException e ) {
404                 data.supportsPropertyChangeEvents_ = false;
405             }
406             catch( IllegalAccessException e ) {
407                 if( traceChannel_ != null ) {
408                     Trace.printStackTrace(traceChannel_, e);
409                 }
410             }
411             catch( InvocationTargetException e ) {
412                 if( traceChannel_ != null ) {
413                     Trace.printStackTrace(traceChannel_, e.getTargetException());
414                 }
415             }
416 
417             rowElementControlDatas_.put(object, data);
418         }
419 
420         data.instanceCounter_++;
421     }
422 
423     /***
424      * @param list The list of objects that will be used to create the rows.
425      */
426     private void addRowElements( final List list ) {
427         final Iterator iterator = list.iterator();
428         while( iterator.hasNext() ) {
429             addRowElement( iterator.next() );
430         }
431     }
432 
433     /***
434      * @param list The list of object that will be removed from the model
435      */
436     private void removeRowElements( final List list ) {
437         final Iterator iterator = list.iterator();
438         while( iterator.hasNext() ) {
439             removeRowElement( iterator.next() );
440         }
441     }
442 
443     /***
444      * Remove one row
445      * @param object The object that was used to create this row.
446      */
447     private synchronized void removeRowElement( final Object object ) {
448 
449         RowElementControlData data =
450         (RowElementControlData)rowElementControlDatas_.get(object);
451 
452         if( data == null ) {
453             throw new DetailedIllegalArgumentException("object", object, "Not in row list");
454         }
455 
456         final Class clazz = object.getClass();
457         final Class parms[] = { PropertyChangeListener.class};
458         Method method;
459         try {
460             method = clazz.getMethod("removePropertyChangeListener", parms);
461             method.invoke(object, new Object[]{propertyChangeListener_} );
462         }
463         catch( final NoSuchMethodException e ) {
464             throw new IllegalStateException(
465                 "object doesn't have a removePropertyChange(): "+object);
466         }
467         catch( IllegalAccessException e ) {
468             if( traceChannel_ != null ) {
469                 Trace.printStackTrace(traceChannel_, e);
470             }
471         }
472         catch( InvocationTargetException e ) {
473             if( traceChannel_ != null ) {
474                 Trace.printStackTrace(traceChannel_, e.getTargetException());
475             }
476         }
477 
478         data.instanceCounter_--;
479         if( data.instanceCounter_ == 0 ) {
480             rowElementControlDatas_.remove(object);
481         }
482     }
483 
484 
485     /***
486      * Throw an exception if the specified object is null
487      * @param fieldName The name of the paremeter we are checking
488      * @param object The value of the parameter we are checking
489      */
490     protected final void assertNotNull( final String fieldName, final Object object ) {
491         if( object == null ) {
492             throw new DetailedNullPointerException(fieldName);
493         }
494     }
495 }