1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
96
97
98
99
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
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
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
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 }