001    /*
002     * Copyright 2002-2004 the original author or authors.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * 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, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.table;
017    
018    import java.util.ArrayList;
019    import java.util.Arrays;
020    import java.util.List;
021    
022    import javax.swing.table.AbstractTableModel;
023    
024    import org.springframework.binding.value.support.ObservableList;
025    import org.springframework.richclient.util.Assert;
026    
027    /**
028     * A skeleton {@link TableModel} implementation that adds to the {@link AbstractTableModel} class
029     * from the core Java API by providing the functionality to manage the underlying collection 
030     * of data for the table.
031     * 
032     * @author Keith Donald
033     */
034    public abstract class BaseTableModel extends AbstractTableModel implements MutableTableModel {
035        
036        private Class[] columnClasses;
037    
038        /** The names for the column headers. Must never be null. */
039        private String[] columnNames;
040    
041        /** 
042         * The collection of objects that represent the rows of the table. 
043         * Class invariant; this must never be null. 
044         */
045        private List rows;
046    
047        private boolean rowNumbers = true;
048    
049        /**
050         * Creates a new uninitialized {@code BaseTableModel}.
051         */
052        public BaseTableModel() {
053            this.rows = new ArrayList();
054        }
055    
056        /**
057         * Creates a new {@code BaseTableModel} containing the given collection of rows.
058         *
059         * @param rows The rows of the table model. May be null or empty.
060         */
061        public BaseTableModel(List rows) {
062            if (rows == null) {
063                this.rows = new ArrayList();
064            }
065            else {
066                this.rows =  rows;
067            }
068            
069        }
070    
071        /**
072         * Overwrites the existing table rows with the given collection and fires an appropriate event
073         * to all registered listeners.
074         *
075         * @param rows The collection of rows that will overwrite the existing collection. May be 
076         * null or empty.
077         */
078        public void setRows(List rows) {
079            // first check null, if somehow field was null it may not return (in second if)
080            if (rows == null) {
081                this.rows = new ArrayList();
082            }
083            if (this.rows == rows) {
084                return;
085            }
086            this.rows = rows;
087            fireTableDataChanged();
088        }
089    
090        /**
091         * Sets the flag that indicates whether or not row numbers are to appear in the first column
092         * of the displayed table.
093         *
094         * @param rowNumbers The flag to display row numbers in the first column.
095         */
096        public void setRowNumbers(boolean rowNumbers) {
097            if (this.rowNumbers != rowNumbers) {
098                this.rowNumbers = rowNumbers;
099                // modify tableModel to add/remove rowNo-Column
100                createColumnInfo();
101            }
102        }
103    
104        /**
105         * Returns true if row numbers are to appear in the first column of the displayed table (default
106         * is true).
107         *
108         * @return The flag to show row numbers in the first column of the displayed table.
109         */
110        public boolean hasRowNumbers() {
111            return rowNumbers;
112        }
113    
114        /**
115         * Creates the required column information based on the value provided by the 
116         * {@link #createColumnClasses()} and {@link #createColumnNames()} methods.
117         */
118        protected void createColumnInfo() {
119        
120            Class[] newColumnClasses = createColumnClasses();
121            String[] newColumnNames = createColumnNames();
122            
123            if (rowNumbers) {
124                // modify columns to add rowNo as first column
125                this.columnClasses = new Class[newColumnClasses.length + 1];
126                this.columnClasses[0] = Integer.class;
127                System.arraycopy(newColumnClasses, 0, this.columnClasses, 1, newColumnClasses.length);
128    
129                this.columnNames = new String[newColumnNames.length + 1];
130                this.columnNames[0] = " ";
131                System.arraycopy(newColumnNames, 0, this.columnNames, 1, newColumnNames.length);
132            }
133            else {
134                // take columns as they are
135                this.columnClasses = newColumnClasses;
136                this.columnNames = newColumnNames;
137            }
138            
139        }
140    
141        /**
142         * {@inheritDoc}
143         */
144        public int getRowCount() {
145            return rows.size();
146        }
147    
148        /**
149         * {@inheritDoc}
150         */
151        public int getColumnCount() {
152            return columnNames.length;
153        }
154    
155        /**
156         * Returns the number of columns, excluding the column that displays row numbers if present.
157         *
158         * @return The number of columns, not counting the row number column if present.
159         */
160        public int getDataColumnCount() {
161            return rowNumbers ? columnNames.length - 1 : getColumnCount();
162        }
163    
164        /**
165         * Returns the type of the object to be displayed in the given column.
166         * 
167         * @param columnIndex The zero-based index of the column whose type will be returned.
168         * 
169         * @throws IndexOutOfBoundsException if {@code columnIndex} is not within the bounds of the
170         * column range for a row of the table.
171         */
172        public Class getColumnClass(int columnIndex) {
173            return columnClasses[columnIndex];
174        }
175    
176        /**
177         * Returns the name to be displayed in the header for the column at the given position.
178         * 
179         * @param columnIndex The zero-based index of the column whose name will be returned.
180         * 
181         * @throws IndexOutOfBoundsException if {@code columnInde} is not within the bounds of the 
182         * column range for a row of the table.
183         */
184        public String getColumnName(int columnIndex) {
185            return columnNames[columnIndex];
186        }
187    
188        /**
189         * Returns the array of column headers.
190         *
191         * @return The array of column headers, never null.
192         */
193        public String[] getColumnHeaders() {
194            return columnNames;
195        }
196    
197        /**
198         * Returns the array of column headers other than the column displaying the row numbers,
199         * if present.
200         *
201         * @return The column headers, not including the row number column. Never null.
202         */
203        public String[] getDataColumnHeaders() {
204            
205            String[] headers = getColumnHeaders();
206            
207            if (!hasRowNumbers()) {
208                return headers;
209            }
210    
211            String[] dataHeaders = new String[headers.length - 1];
212            System.arraycopy(headers, 1, dataHeaders, 0, headers.length - 1);
213            return dataHeaders;
214            
215        }
216    
217        /**
218         * {@inheritDoc}
219         */
220        public Object getValueAt(int rowIndex, int columnIndex) {
221            if (rowNumbers) {
222                if (columnIndex == 0) {
223                    return new Integer(rowIndex + 1);
224                }
225                columnIndex--;
226            }
227            return getValueAtInternal(rows.get(rowIndex), columnIndex);
228        }
229    
230        /**
231         * Subclasses must implement this method to return the value at the given column index for 
232         * the given object.
233         *
234         * @param row The object representing a row of data from the table.
235         * @param columnIndex The column index of the value to be returned.
236         * @return The value at the given index for the given object. May be null.
237         */
238        protected abstract Object getValueAtInternal(Object row, int columnIndex);
239    
240        /**
241         * {@inheritDoc}
242         */
243        //FIXME this method should probably become a template method by making it final
244        public boolean isCellEditable(int rowIndex, int columnIndex) {
245           
246            if (rowNumbers) {
247                if (columnIndex == 0) {
248                    return false;
249                }
250                columnIndex--;
251            }
252            
253            return isCellEditableInternal(rows.get(rowIndex), columnIndex);
254            
255        }
256    
257        /**
258         * Subclasses may override this method to determine if the cell at the specified row and 
259         * column position can be edited. Default behaviour is to always return false.
260         *
261         * @param row The object representing the table row.
262         * @param columnIndex The zero-based index of the column to be checked.
263         * @return true if the given cell is editable, false otherwise.
264         */
265        protected boolean isCellEditableInternal(Object row, int columnIndex) {
266            return false;
267        }
268    
269        /**
270         * {@inheritDoc}
271         */
272        public void setValueAt(Object value, int rowIndex, int columnIndex) {
273            if (rowNumbers) {
274                columnIndex--;
275            }
276            setValueAtInternal(value, rows.get(rowIndex), columnIndex);
277            if (getRows() instanceof ObservableList) {
278                ((ObservableList) getRows()).getIndexAdapter(rowIndex).fireIndexedObjectChanged();
279            }
280            fireTableCellUpdated(rowIndex, columnIndex);
281        }
282    
283        /**
284         * Subclasses may implement this method to set the given value on the property at the given 
285         * column index of the given row object. The default implementation is to do nothing. 
286         *
287         * @param value The value to be set.
288         * @param row The object representing the row in the table.
289         * @param columnIndex The column position of the property on the given row object.
290         */
291        protected void setValueAtInternal(Object value, Object row, int columnIndex) {
292            //do nothing
293        }
294    
295        /**
296         * Returns the object representing the row at the given zero-based index.
297         *
298         * @param rowIndex The zero-based index of the row whose object should be returned.
299         * @return The object for the given row.
300         * 
301         * @throws IndexOutOfBoundsException if {@code rowIndex} is not within the bounds of the 
302         * collection of rows for this table model.
303         */
304        public Object getRow(int rowIndex) {
305            return rows.get(rowIndex);
306        }
307    
308        /**
309         * Returns the collection of all the rows in this table model.
310         *
311         * @return The collection rows. The collection may be empty but will never be null.
312         */
313        public List getRows() {
314            return rows;
315        }
316    
317        /**
318         * Returns the collection of data from the given column of each row in the table model.
319         *
320         * @param column The zero-based index of the column whose data should be returned.
321         * @return The collection of data from the given column.
322         * 
323         * @throws IndexOutOfBoundsException if the given column is not within the bounds of the 
324         * number of columns for the rows in this table model.
325         */
326        public List getColumnData(int column) {
327            
328            int columnCount = getColumnCount();
329            
330            if (column < 0 || column >= columnCount) {
331                throw new IndexOutOfBoundsException("The given column index ["
332                                                    + column
333                                                    + "] is outside the bounds of the number of columns ["
334                                                    + columnCount
335                                                    + "] in the rows of this table model.");
336            }
337            
338            if (columnCount == 1) {
339                return rows;
340            }
341    
342            List colData = new ArrayList(getRowCount());
343            
344            for (int i = 0; i < getRowCount(); i++) {
345                colData.add(getValueAt(i, column));
346            }
347            
348            return colData;
349            
350        }
351    
352        /**
353         *  Returns the index of the first row containing the specified element, or -1 if this table 
354         *  model does not contain this element.
355         *
356         * @param obj The object whose row number will be returned.
357         * @return The index of the first row containing the given object, or -1 if the table model 
358         * does not contain the object.
359         */
360        public int rowOf(Object obj) {
361            
362            if (obj == null) {
363                return -1;
364            }
365            
366            return rows.indexOf(obj);
367            
368        }
369    
370        /**
371         * {@inheritDoc}
372         */
373        public void addRow(Object row) {
374            
375            Assert.required(row, "row");
376            
377            this.rows.add(row);
378            int index = this.rows.size() - 1;
379            fireTableRowsInserted(index, index);
380        }
381    
382        /**
383         * {@inheritDoc}
384         */
385        public void addRows(List newRows) {
386            Assert.required(newRows, "newRows");
387            
388            if (newRows.isEmpty()) {
389                return;
390            }
391            
392            int firstRow = this.rows.size();
393            this.rows.addAll(newRows);
394            int lastRow = this.rows.size() - 1;
395            fireTableRowsInserted(firstRow, lastRow);
396        }
397    
398        /**
399         * {@inheritDoc}
400         */
401        public void remove(int index) {
402            this.rows.remove(index);
403            fireTableRowsDeleted(index, index);
404        }
405    
406        /**
407         * {@inheritDoc}
408         */
409        public void remove(int firstIndex, int lastIndex) {
410            
411            if (lastIndex < firstIndex) {
412                throw new IllegalArgumentException("lastIndex ["
413                                                   + lastIndex
414                                                   + "] cannot be less than firstIndex ["
415                                                   + firstIndex
416                                                   + "]");
417            }
418            
419            if (firstIndex < 0 || firstIndex >= this.rows.size()) {
420                throw new IndexOutOfBoundsException("The specified starting index ["
421                                                    + firstIndex
422                                                    + "] is outside the bounds of the rows collection "
423                                                    + "which only has ["
424                                                    + this.rows.size()
425                                                    + "] elements.");
426            }
427            
428            if (lastIndex >= this.rows.size()) {
429                throw new IndexOutOfBoundsException("The specified end index ["
430                                                    + lastIndex
431                                                    + "] is outside the bounds of the rows collection "
432                                                    + "which only has ["
433                                                    + this.rows.size()
434                                                    + "] elements.");
435            }
436            
437            int rowCount = lastIndex - firstIndex + 1;
438            
439            for (int i = 0; i < rowCount; i++) {
440                rows.remove(firstIndex);
441            }
442            
443            fireTableRowsDeleted(firstIndex, lastIndex);
444            
445        }
446    
447        /**
448         * {@inheritDoc}
449         */
450        public void remove(int[] indexes) {
451            
452            Assert.required(indexes, "indexes");
453            
454            if (indexes.length == 0) {
455                return;
456            }
457    
458            // must sort the indexes first!!!
459            Arrays.sort(indexes);
460            
461            if (indexes[0] < 0 || indexes[0] >= this.rows.size()) {
462                throw new IndexOutOfBoundsException("The specified index ["
463                                                    + indexes[0]
464                                                    + "] is outside the bounds of the rows collection "
465                                                    + "which only has ["
466                                                    + this.rows.size()
467                                                    + "] elements.");
468            }
469            
470            if ((indexes[indexes.length -1]) >= this.rows.size()) {
471                throw new IndexOutOfBoundsException("The specified end index ["
472                                                    + indexes[indexes.length -1]
473                                                    + "] is outside the bounds of the rows collection "
474                                                    + "which only has ["
475                                                    + this.rows.size()
476                                                    + "] elements.");
477            }
478            
479            int firstIndex = indexes[0];
480            int lastIndex = indexes[0];
481            int i = 0;
482            int shift = 0;
483            // this is kind of complicated - only removes contiguous selection
484            // intervals to minimize number of events published to GUI.
485            while (i < indexes.length - 1) {
486                if (indexes[i + 1] == (lastIndex + 1)) {
487                    lastIndex++;
488                }
489                else {
490                    remove(firstIndex - shift, lastIndex - shift);
491                    shift += lastIndex - firstIndex + 1;
492                    firstIndex = indexes[i + 1];
493                    lastIndex = indexes[i + 1];
494                }
495                i++;
496            }
497            remove(firstIndex - shift, lastIndex - shift);
498        }
499    
500        /**
501         * {@inheritDoc}
502         */
503        public void clear() {
504            this.rows.clear();
505            fireTableDataChanged();
506        }
507    
508        /**
509         * Returns the array of class types for the columns displayed by this table model.
510         *
511         * @return The array of column class types, never null.
512         */
513        protected Class[] getColumnClasses() {
514            return columnClasses;
515        }
516    
517        /**
518         * Returns the array of column headers for the columns displayed by this table model.
519         *
520         * @return The array of column headers, never null.
521         */
522        protected String[] getColumnNames() {
523            return columnNames;
524        }
525    
526        /**
527         * Subclasses must implement this method to return the array of class types for the columns 
528         * displayed by this table model.
529         *
530         * @return The array of column class types, never null.
531         */
532        protected abstract Class[] createColumnClasses();
533    
534        /**
535         * Subclasses must implement this method to return the array of column headers for the 
536         * columns to be displayed by this table model.
537         * 
538         * @return The array of column headers, never null.
539         */
540        protected abstract String[] createColumnNames();
541    
542    }