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 }