package hermes.swing;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import javax.swing.table.AbstractTableModel;
/**
* A TableModel that better supports the processing of rows of data. That
* is, the data is treated more like a row than an individual cell. Hopefully
* this class can be used as a parent class instead of extending the
* AbstractTableModel when you need custom models that contain row related
* data.
*
* A few methods have also been added to make it easier to customize
* properties of the model, such as the column class and column editability.
*
* Any class that extends this class must make sure to invoke the
* setRowClass() and setDataAndColumnNames() methods either directly,
* by using the various constructors, or indirectly.
*
*/
abstract class RowTableModel<T> extends AbstractTableModel
{
protected List<T> modelData;
protected List<String> columnNames;
protected Class[] columnClasses;
protected Boolean[] isColumnEditable;
private Class rowClass = Object.class;
private boolean isModelEditable = true;
/**
* Constructs a <code>RowTableModel</code> with the row class.
*
* This value is used by the getRowsAsArray() method.
*
* Sub classes creating a model using this constructor must make sure
* to invoke the setDataAndColumnNames() method.
*
* @param rowClass the class of row data to be added to the model
*/
protected RowTableModel(Class rowClass)
{
setRowClass( rowClass );
}
/**
* Constructs a <code>RowTableModel</code> with column names.
*
* Each column's name will be taken from the <code>columnNames</code>
* List and the number of colums is determined by thenumber of items
* in the <code>columnNames</code> List.
*
* Sub classes creating a model using this constructor must make sure
* to invoke the setRowClass() method.
*
* @param columnNames <code>List</code> containing the names
* of the new columns
*/
protected RowTableModel(List<String> columnNames)
{
this(new ArrayList<T>(), columnNames);
}
/**
* Constructs a <code>RowTableModel</code> with initial data and
* customized column names.
*
* Each item in the <code>modelData</code> List must also be a List Object
* containing items for each column of the row.
*
* Each column's name will be taken from the <code>columnNames</code>
* List and the number of colums is determined by thenumber of items
* in the <code>columnNames</code> List.
*
* Sub classes creating a model using this constructor must make sure
* to invoke the setRowClass() method.
*
* @param modelData the data of the table
* @param columnNames <code>List</code> containing the names
* of the new columns
*/
protected RowTableModel(List<T> modelData, List<String> columnNames)
{
setDataAndColumnNames(modelData, columnNames);
}
/**
* Full Constructor for creating a <code>RowTableModel</code>.
*
* Each item in the <code>modelData</code> List must also be a List Object
* containing items for each column of the row.
*
* Each column's name will be taken from the <code>columnNames</code>
* List and the number of colums is determined by thenumber of items
* in the <code>columnNames</code> List.
*
* @param modelData the data of the table
* @param columnNames <code>List</code> containing the names
* of the new columns
* @param rowClass the class of row data to be added to the model
*/
protected RowTableModel(List<T> modelData, List<String> columnNames, Class rowClass)
{
setDataAndColumnNames(modelData, columnNames);
setRowClass( rowClass );
}
/**
* Reset the data and column names of the model.
*
* A fireTableStructureChanged event will be generated.
*
* @param modelData the data of the table
* @param columnNames <code>List</code> containing the names
* of the new columns
*/
protected void setDataAndColumnNames(List<T> modelData, List<String> columnNames)
{
this.modelData = modelData;
this.columnNames = columnNames;
columnClasses = new Class[getColumnCount()];
isColumnEditable = new Boolean[getColumnCount()];
fireTableStructureChanged();
}
/**
* The class of the Row being stored in the TableModel
*
* This is required for the getRowsAsArray() method to return the
* proper class of row.
*
* @param rowClas the class of the row
*/
protected void setRowClass(Class rowClass)
{
this.rowClass = rowClass;
}
//
// Implement the TableModel interface
//
/**
* Returns the Class of the queried <code>column</code>.
* First it will check to see if a Class has been specified for the
* <code>column</code> by using the <code>setColumnClass</code> method.
* If not, then the superclass value is returned.
*
* @param column the column being queried
* @return the Class of the column being queried
*/
public Class getColumnClass(int column)
{
Class columnClass = null;
// Get the class, if set, for the specified column
if (column < columnClasses.length)
columnClass = columnClasses[column];
// Get the default class
if (columnClass == null)
columnClass = super.getColumnClass(column);
return columnClass;
}
/**
* Returns the number of columns in this table model.
*
* @return the number of columns in the model
*/
public int getColumnCount()
{
return columnNames.size();
}
/**
* Returns the column name.
*
* @return a name for this column using the string value of the
* appropriate member in <code>columnNames</code>. If
* <code>columnNames</code> does not have an entry for this index
* then the default name provided by the superclass is returned
*/
public String getColumnName(int column)
{
Object columnName = null;
if (column < columnNames.size())
{
columnName = columnNames.get( column );
}
return (columnName == null) ? super.getColumnName( column ) : columnName.toString();
}
/**
* Returns the number of rows in this table model.
*
* @return the number of rows in the model
*/
public int getRowCount()
{
return modelData.size();
}
/**
* Returns true regardless of parameter values.
*
* @param row the row whose value is to be queried
* @param column the column whose value is to be queried
* @return true
*/
public boolean isCellEditable(int row, int column)
{
Boolean isEditable = null;
// Check is column editability has been set
if (column < isColumnEditable.length)
isEditable = isColumnEditable[column];
return (isEditable == null) ? isModelEditable : isEditable.booleanValue();
}
//
// Implement custom methods
//
/**
* Adds a row of data to the end of the model.
* Notification of the row being added will be generated.
*
* @param rowData data of the row being added
*/
public void addRow(T rowData)
{
insertRow(getRowCount(), rowData);
}
/**
* Returns the Object of the requested <code>row</code>.
*
* @return the Object of the requested row.
*/
public T getRow(int row)
{
return modelData.get( row );
}
/**
* Returns an array of Objects for the requested <code>rows</code>.
*
* @return an array of Objects for the requested rows.
*/
@SuppressWarnings("unchecked")
public T[] getRowsAsArray(int... rows)
{
List<T> rowData = getRowsAsList(rows);
T[] array = (T[])Array.newInstance(rowClass, rowData.size());
return (T[]) rowData.toArray( array );
}
/**
* Returns a List of Objects for the requested <code>rows</code>.
*
* @return a List of Objects for the requested rows.
*/
public List<T> getRowsAsList(int... rows)
{
ArrayList<T> rowData = new ArrayList<T>(rows.length);
for (int i = 0; i < rows.length; i++)
{
rowData.add( getRow(rows[i]) );
}
return rowData;
}
/**
* Insert a row of data at the <code>row</code> location in the model.
* Notification of the row being added will be generated.
*
* @param row row in the model where the data will be inserted
* @param rowData data of the row being added
*/
public void insertRow(int row, T rowData)
{
modelData.add(row, rowData);
fireTableRowsInserted(row, row);
}
/**
* Insert multiple rows of data at the <code>row</code> location in the model.
* Notification of the row being added will be generated.
*
* @param row row in the model where the data will be inserted
* @param rowList each item in the list is a separate row of data
*/
public void insertRows(int row, List<T> rowList)
{
modelData.addAll(row, rowList);
fireTableRowsInserted(row, row + rowList.size() - 1);
}
/**
* Insert multiple rows of data at the <code>row</code> location in the model.
* Notification of the row being added will be generated.
*
* @param row row in the model where the data will be inserted
* @param rowArray each item in the Array is a separate row of data
*/
public void insertRows(int row, T[] rowArray)
{
List<T> rowList = new ArrayList<T>(rowArray.length);
for (int i = 0; i < rowArray.length; i++)
{
rowList.add( rowArray[i] );
}
insertRows(row, rowList);
}
/**
* Moves one or more rows from the inlcusive range <code>start</code> to
* <code>end</code> to the <code>to</code> position in the model.
* After the move, the row that was at index <code>start</code>
* will be at index <code>to</code>.
* This method will send a <code>tableRowsUpdated</code> notification
* message to all the listeners. <p>
*
* <pre>
* Examples of moves:
* <p>
* 1. moveRow(1,3,5);
* a|B|C|D|e|f|g|h|i|j|k - before
* a|e|f|g|h|B|C|D|i|j|k - after
* <p>
* 2. moveRow(6,7,1);
* a|b|c|d|e|f|G|H|i|j|k - before
* a|G|H|b|c|d|e|f|i|j|k - after
* <p>
* </pre>
*
* @param start the starting row index to be moved
* @param end the ending row index to be moved
* @param to the destination of the rows to be moved
* @exception IllegalArgumentException
* if any of the elements would be moved out
* of the table's range
*/
public void moveRow(int start, int end, int to)
{
if (start < 0)
{
String message = "Start index must be positive: " + start;
throw new IllegalArgumentException( message );
}
if (end > getRowCount() - 1)
{
String message = "End index must be less than total rows: " + end;
throw new IllegalArgumentException( message );
}
if (start > end)
{
String message = "Start index cannot be greater than end index";
throw new IllegalArgumentException( message );
}
int rowsMoved = end - start + 1;
if (to < 0
|| to > getRowCount() - rowsMoved)
{
String message = "New destination row (" + to + ") is invalid";
throw new IllegalArgumentException( message );
}
// Save references to the rows that are about to be moved
ArrayList<T> temp = new ArrayList<T>(rowsMoved);
for (int i = start; i < end + 1; i++)
{
temp.add(modelData.get(i));
}
// Remove the rows from the current location and add them back
// at the specified new location
modelData.subList(start, end + 1).clear();
modelData.addAll(to, temp);
// Determine the rows that need to be repainted to reflect the move
int first;
int last;
if (to < start)
{
first = to;
last = end;
}
else
{
first = start;
last = to + end - start;
}
fireTableRowsUpdated(first, last);
}
/**
* Remove the specified rows from the model. Rows between the starting
* and ending indexes, inclusively, will be removed.
* Notification of the rows being removed will be generated.
*
* @param start starting row index
* @param end ending row index
* @exception ArrayIndexOutOfBoundsException
* if any row index is invalid
*/
public void removeRowRange(int start, int end)
{
modelData.subList(start, end + 1).clear();
fireTableRowsDeleted(start, end);
}
/**
* Remove the specified rows from the model. The row indexes in the
* array must be in increasing order.
* Notification of the rows being removed will be generated.
*
* @param rows array containing indexes of rows to be removed
* @exception ArrayIndexOutOfBoundsException
* if any row index is invalid
*/
public void removeRows(int... rows)
{
for (int i = rows.length - 1; i >= 0; i--)
{
int row = rows[i];
modelData.remove(row);
fireTableRowsDeleted(row, row);
}
}
/**
* Replace a row of data at the <code>row</code> location in the model.
* Notification of the row being replaced will be generated.
*
* @param row row in the model where the data will be replaced
* @param rowData data of the row to replace the existing data
* @exception IllegalArgumentException when the Class of the row data
* does not match the row Class of the model.
*/
public void replaceRow(int row, T rowData)
{
modelData.set(row, rowData);
fireTableRowsUpdated(row, row);
}
/**
* Sets the Class for the specified column.
*
* @param column the column whose Class is being changed
* @param columnClass the new Class of the column
* @exception ArrayIndexOutOfBoundsException
* if an invalid column was given
*/
public void setColumnClass(int column, Class columnClass)
{
columnClasses[column] = columnClass;
fireTableRowsUpdated(0, getColumnCount() - 1);
}
/**
* Sets the editability for the specified column.
*
* @param column the column whose Class is being changed
* @param isEditable indicates if the column is editable or not
* @exception ArrayIndexOutOfBoundsException
* if an invalid column was given
*/
public void setColumnEditable(int column, boolean isEditable)
{
isColumnEditable[column] = isEditable ? Boolean.TRUE : Boolean.FALSE;
}
/**
* Set the ability to edit cell data for the entire model
*
* Note: values set by the setColumnEditable(...) method will have
* prioritiy over this value.
*
* @param isModelEditable true/false
*/
public void setModelEditable(boolean isModelEditable)
{
this.isModelEditable = isModelEditable;
}
/*
* Convert an unformatted column name to a formatted column name. That is:
*
* - insert a space when a new uppercase character is found, insert
* multiple upper case characters are grouped together.
* - replace any "_" with a space
*
* @param columnName unformatted column name
* @return the formatted column name
*/
public static String formatColumnName(String columnName)
{
if (columnName.length() < 3) return columnName;
StringBuffer buffer = new StringBuffer( columnName );
boolean isPreviousLowerCase = false;
for (int i = 1; i < buffer.length(); i++)
{
boolean isCurrentUpperCase = Character.isUpperCase( buffer.charAt(i) );
if (isCurrentUpperCase && isPreviousLowerCase)
{
buffer.insert(i, " ");
i++;
}
isPreviousLowerCase = ! isCurrentUpperCase;
}
return buffer.toString().replaceAll("_", " ");
}
}