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 }