package prefuse.data; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import javax.swing.event.TableModelEvent; import prefuse.data.column.Column; import prefuse.data.column.ColumnFactory; import prefuse.data.column.ColumnMetadata; import prefuse.data.event.ColumnListener; import prefuse.data.event.EventConstants; import prefuse.data.event.TableListener; import prefuse.data.expression.Expression; import prefuse.data.expression.Predicate; import prefuse.data.expression.parser.ExpressionParser; import prefuse.data.tuple.AbstractTupleSet; import prefuse.data.tuple.TableTuple; import prefuse.data.tuple.TupleManager; import prefuse.data.util.FilterIteratorFactory; import prefuse.data.util.Index; import prefuse.data.util.RowManager; import prefuse.data.util.Sort; import prefuse.data.util.TableIterator; import prefuse.data.util.TreeIndex; import prefuse.util.TypeLib; import prefuse.util.collections.CopyOnWriteArrayList; import prefuse.util.collections.IncompatibleComparatorException; import prefuse.util.collections.IntIterator; /** * <p>A Table organizes a collection of data into rows and columns, each row * containing a data record, and each column containing data values for a * named data field with a specific data type. Table data can be accessed * directly using the row number and column name, or rows can be treated * in an object-oriented fashion using {@link prefuse.data.Tuple} * instances that represent a single row of data in the table. As such, * tables implement the {@link prefuse.data.tuple.TupleSet} interface.</p> * * <p>Table rows can be inserted or deleted. In any case, none of the other * existing table rows are effected by an insertion or deletion. A deleted * row simply becomes invalid--any subsequent attempts to access the row * either directly or through a pre-existing Tuple instance will result * in an exception. However, if news rows are later added to the table, * the row number for previously deleted nodes can be reused. In fact, the * lower row number currently unused is assigned to the new row. This results * in an efficient reuse of the table rows, but carries an important side * effect -- rows do not necesarily maintain the order in which they were * added once deletions have occurred on the table. If not deletions * occur, the ordering of table rows will reflect the order in which * rows were added to the table.</p> * * <p>Collections of table rows can be accessed using both iterators over * the actual row numbers and iterators over the Tuple instances that * encapsulate access to that row. Both types of iteration can also be * filtered by providing a {@link prefuse.data.expression.Predicate}, * allowing tables to be queried for specific values.</p> * * <p>Columns (alternativele referred to as data fields) can be added to * the Table using {@link #addColumn(String, Class)} and a host of * similar methods. This method will automatically determine the right * kind of backing column instance to use. Furthermore, Table columns * can be specified using a {@link Schema} instance, which describes * the column names, data types, and default values. The Table class * also maintains its own internal Schema, which be accessed (in a * read-only way) using the {@link #getSchema()} method.</p> * * <p>Tables also support additional structures. The {@link ColumnMetadata} * class returned by the {@link #getMetadata(String)} method supports * calculation of different statistics for a column, including minimum * and maximum values, and the number of unique data values in the column. * {@link prefuse.data.util.Index} instances can be created and retrieved * using the {@link #index(String)} method and retrieved without triggering * creation using {@link #getIndex(String)} method. An index keeps a * sorted collection of all data values in a column, accelerating the creation * of filtered iterators by optimizing query calculations and also providing * faster computation of many of the {@link ColumnMetadata} methods. If * you will be issuing a number of queries (i.e., requesting filtered * iterators) dependent on the values of a given column, indexing that column * may result in a significant performance increase, though at the cost * of storing and maintaining the backing index structure.</p> * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class Table extends AbstractTupleSet implements ColumnListener { /** Listeners for changes to this table */ protected CopyOnWriteArrayList m_listeners; /** Locally stored data columns */ protected ArrayList m_columns; /** Column names for locally store data columns */ protected ArrayList m_names; /** Mapping between column names and column entries * containing column, metadata, and index references */ protected HashMap m_entries; /** Manager for valid row indices */ protected RowManager m_rows; /** manager for tuples, which are object representations for rows */ protected TupleManager m_tuples; /** Tracks the number of edits of this table */ protected int m_modCount = 0; /** Memoize the index of the last column operated on, * used to expedite handling of column updates. */ protected int m_lastCol = -1; /** A cached schema instance, loaded lazily */ protected Schema m_schema; // ------------------------------------------------------------------------ // Constructors /** * Create a new, empty Table. Rows can be added to the table using * the {@link #addRow()} method. */ public Table() { this(0, 0); } /** * Create a new Table with a given number of rows, and the starting * capacity for a given number of columns. * @param nrows the starting number of table rows * @param ncols the starting capacity for columns */ public Table(int nrows, int ncols) { this(nrows, ncols, TableTuple.class); } /** * Create a new Table. * @param nrows the starting number of table rows * @param ncols the starting capacity for columns * @param tupleType the class of the Tuple instances to use */ protected Table(int nrows, int ncols, Class tupleType) { m_listeners = new CopyOnWriteArrayList(); m_columns = new ArrayList(ncols); m_names = new ArrayList(ncols); m_rows = new RowManager(this); m_entries = new HashMap(ncols+5); m_tuples = new TupleManager(this, null, tupleType); if ( nrows > 0 ) addRows(nrows); } // ------------------------------------------------------------------------ // Table Metadata /** * Get the number of columns / data fields in this table. * @return the number of columns */ public int getColumnCount() { return m_columns.size(); } /** * Get the data type of the column at the given column index. * @param col the column index * @return the data type (as a Java Class) of the column */ public Class getColumnType(int col) { return getColumn(col).getColumnType(); } /** * Get the data type of the column with the given data field name. * @param field the column / data field name * @return the data type (as a Java Class) of the column */ public Class getColumnType(String field) { Column c = getColumn(field); return (c==null ? null : c.getColumnType()); } /** * Get the number of rows in the table. * @return the number of rows */ public int getRowCount() { return m_rows.getRowCount(); } /** * Get the minimum row index currently in use by this Table. * @return the minimum row index */ public int getMinimumRow() { return m_rows.getMinimumRow(); } /** * Get the maximum row index currently in use by this Table. * @return the maximum row index */ public int getMaximumRow() { return m_rows.getMaximumRow(); } /** * Indicates if the value of the given table cell can be changed. * @param row the row number * @param col the column number * @return true if the value can be edited/changed, false otherwise */ public boolean isCellEditable(int row, int col) { if ( !m_rows.isValidRow(row) ) { return false; } else { return getColumn(col).isCellEditable(row); } } /** * Get the number of times this Table has been modified. Adding rows, * deleting rows, and updating table cell values all contribute to * this count. * @return the number of modifications to this table */ public int getModificationCount() { return m_modCount; } /** * Sets the TupleManager used by this Table. Use this method * carefully, as it will cause all existing Tuples retrieved * from this Table to be invalidated. * @param tm the TupleManager to use */ public void setTupleManager(TupleManager tm) { m_tuples.invalidateAll(); m_tuples = tm; } /** * Returns this Table's schema. The returned schema will be * locked, which means that any attempts to edit the returned schema * by adding additional columns will result in a runtime exception. * * If this Table subsequently has columns added or removed, this will not * be reflected in the returned schema. Instead, this method will need to * be called again to get a current schema. Accordingly, it is not * recommended that Schema instances returned by this method be stored * or reused across scopes unless that exact schema snapshot is * desired. * * @return a copy of this Table's schema */ public Schema getSchema() { if ( m_schema == null ) { Schema s = new Schema(); for ( int i=0; i<getColumnCount(); ++i ) { s.addColumn(getColumnName(i), getColumnType(i), getColumn(i).getDefaultValue()); } s.lockSchema(); m_schema = s; } return m_schema; } /** * Invalidates this table's cached schema. This method should be called * whenever columns are added or removed from this table. */ protected void invalidateSchema() { m_schema = null; } // ------------------------------------------------------------------------ // Row Operations /** * Get the row value for accessing an underlying Column instance, * corresponding to the given table cell. For basic tables this just * returns the input row value. However, for tables that inherit * data columns from a parent table and present a filtered view on * this data, a mapping between the row numbers of the table and * the row numbers of the backing data column is needed. In those cases, * this method returns the result of that mapping. The method * {@link #getTableRow(int, int)} accesses this map in the reverse * direction. * @param row the table row to lookup * @param col the table column to lookup * @return the column row number for accessing the desired table cell */ public int getColumnRow(int row, int col) { return m_rows.getColumnRow(row, col); } /** * Get the row number for this table given a row number for a backing * data column and the column number for the data column. For basic * tables this just returns the column row value. However, for tables that * inherit data columns from a parent table and present a filtered view on * this data, a mapping between the row numbers of the table and * the row numbers of the backing data column is needed. In those cases, * this method returns the result of this mapping, in the direction of * the backing column rows to the table rows of the cascaded table. The * method {@link #getColumnRow(int, int)} accesses this map in the reverse * direction. * @param colrow the row of the backing data column * @param col the table column to lookup. * @return the table row number for accessing the desired table cell */ public int getTableRow(int colrow, int col) { return m_rows.getTableRow(colrow, col); } /** * Add a row to this table. All data columns will be notified and will * take on the appropriate default values for the added row. * @return the row number of the newly added row */ public int addRow() { int r = m_rows.addRow(); updateRowCount(); fireTableEvent(r, r, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT); return r; } /** * Add a given number of rows to this table. All data columns will be * notified and will take on the appropriate default values for the * added rows. * @param nrows the number of rows to add. */ public void addRows(int nrows) { for ( int i=0; i<nrows; ++i ) { addRow(); } } /** * Internal method that updates the row counts for local data columns. */ protected void updateRowCount() { int maxrow = m_rows.getMaximumRow() + 1; // update columns Iterator cols = getColumns(); while ( cols.hasNext() ) { Column c = (Column)cols.next(); c.setMaximumRow(maxrow); } } /** * Removes a row from this table. * @param row the row to delete * @return true if the row was successfully deleted, false if the * row was already invalid */ public boolean removeRow(int row) { if ( m_rows.isValidRow(row) ) { // the order of operations here is extremely important // otherwise listeners may end up with corrupted state. // fire update *BEFORE* clearing values // allow listeners (e.g., indices) to perform clean-up fireTableEvent(row, row, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE); // invalidate the tuple m_tuples.invalidate(row); // release row with row manager // do this before clearing column values, so that any // listeners can determine that the row is invalid m_rows.releaseRow(row); // now clear column values for ( Iterator cols = getColumns(); cols.hasNext(); ) { Column c = (Column)cols.next(); c.revertToDefault(row); } return true; } return false; } /** * Clear this table, removing all rows. * @see prefuse.data.tuple.TupleSet#clear() */ public void clear() { IntIterator rows = rows(true); while ( rows.hasNext() ) { removeRow(rows.nextInt()); } } /** * Indicates if the given row number corresponds to a valid table row. * @param row the row number to check for validity * @return true if the row is valid, false if it is not */ public boolean isValidRow(int row) { return m_rows.isValidRow(row); } // ------------------------------------------------------------------------ // Column Operations /** * Internal method indicating if the given data field is included as a * data column. */ protected boolean hasColumn(String name) { return getColumnNumber(name) != -1; } /** * Get the data field name of the column at the given column number. * @param col the column number * @return the data field name of the column */ public String getColumnName(int col) { return (String)m_names.get(col); } /** * Get the column number for a given data field name. * @param field the name of the column to lookup * @return the column number of the column, or -1 if the name is not found */ public int getColumnNumber(String field) { ColumnEntry e = (ColumnEntry)m_entries.get(field); return ( e==null ? -1 : e.colnum ); } /** * Get the column number for the given Column instance. * @param col the Column instance to lookup * @return the column number of the column, or -1 if the name is not found */ public int getColumnNumber(Column col) { return m_columns.indexOf(col); } /** * Get the column at the given column number. * @param col the column number * @return the Column instance */ public Column getColumn(int col) { m_lastCol = col; return (Column)m_columns.get(col); } /** * Get the column with the given data field name * @param field the data field name of the column * @return the Column instance */ public Column getColumn(String field) { ColumnEntry e = (ColumnEntry)m_entries.get(field); return ( e != null ? e.column : null ); } /** * Add a column with the given name and data type to this table. * @param name the data field name for the column * @param type the data type, as a Java Class, for the column * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, java.lang.Class) */ public void addColumn(String name, Class type) { addColumn(name, type, null); } /** * Add a column with the given name and data type to this table. * @param name the data field name for the column * @param type the data type, as a Java Class, for the column * @param defaultValue the default value for column data values * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, java.lang.Class, java.lang.Object) */ public void addColumn(String name, Class type, Object defaultValue) { Column col = ColumnFactory.getColumn(type, m_rows.getMaximumRow()+1, defaultValue); addColumn(name, col); } /** * Add a derived column to this table, using an Expression instance to * dynamically calculate the column data values. * @param name the data field name for the column * @param expr a String expression in the prefuse expression language, to * be parsed into an {@link prefuse.data.expression.Expression} instance. * The string is parsed by the * {@link prefuse.data.expression.parser.ExpressionParser}. If an error * occurs during parsing, an exception will be thrown. * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, java.lang.String) */ public void addColumn(String name, String expr) { Expression ex = ExpressionParser.parse(expr); Throwable t = ExpressionParser.getError(); if ( t != null ) { throw new RuntimeException(t); } else { addColumn(name, ex); } } /** * Add a derived column to this table, using an Expression instance to * dynamically calculate the column data values. * @param name the data field name for the column * @param expr the Expression that will determine the column values * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, prefuse.data.expression.Expression) */ public void addColumn(String name, Expression expr) { addColumn(name, ColumnFactory.getColumn(this, expr)); } /** * Add a constant column to this table, which returns one constant value * for all column rows. * @param name the data field name for the column * @param type the data type, as a Java Class, for the column * @param dflt the default value for column data values */ public void addConstantColumn(String name, Class type, Object dflt) { addColumn(name, ColumnFactory.getConstantColumn(type, dflt)); } /** * Internal method for adding a column. * @param name the name of the column * @param col the actual Column instance */ protected void addColumn(String name, Column col) { int idx = getColumnNumber(name); if ( idx >= 0 && idx < m_columns.size() ) { throw new IllegalArgumentException( "Table already has column with name \""+name+"\""); } // add the column m_columns.add(col); m_names.add(name); m_lastCol = m_columns.size()-1; ColumnEntry entry = new ColumnEntry(m_lastCol, col, new ColumnMetadata(this, name)); // add entry, dispose of an overridden entry if needed ColumnEntry oldEntry = (ColumnEntry)m_entries.put(name, entry); if ( oldEntry != null ) oldEntry.dispose(); invalidateSchema(); // listen to what the column has to say col.addColumnListener(this); // fire notification fireTableEvent(m_rows.getMinimumRow(), m_rows.getMaximumRow(), m_lastCol, TableModelEvent.INSERT); } /** * Internal method for removing a column. * @param idx the column number of the column to remove * @return the removed Column instance */ protected Column removeColumn(int idx) { // make sure index is legal if ( idx < 0 || idx >= m_columns.size() ) { throw new IllegalArgumentException("Column index is not legal."); } String name = (String)m_names.get(idx); ((ColumnEntry)m_entries.get(name)).dispose(); Column col = (Column)m_columns.remove(idx); m_entries.remove(name); m_names.remove(idx); renumberColumns(); m_lastCol = -1; invalidateSchema(); // ignore what the old column has to say col.removeColumnListener(this); // fire notification fireTableEvent(m_rows.getMinimumRow(), m_rows.getMaximumRow(), idx, TableModelEvent.DELETE); return col; } /** * Remove a data field from this table * @param field the name of the data field / column to remove * @return the removed Column instance */ public Column removeColumn(String field) { int idx = m_names.indexOf(field); if ( idx < 0 ) { throw new IllegalArgumentException("No such column."); } return removeColumn(idx); } /** * Remove a column from this table * @param c the column instance to remove */ public void removeColumn(Column c) { int idx = m_columns.indexOf(c); if ( idx < 0 ) { throw new IllegalArgumentException("No such column."); } removeColumn(idx); } /** * Internal method that re-numbers columns upon column removal. */ protected void renumberColumns() { Iterator iter = m_names.iterator(); for ( int idx=0; iter.hasNext(); ++idx ) { String name = (String)iter.next(); ColumnEntry e = (ColumnEntry)m_entries.get(name); e.colnum = idx; } } /** * Internal method that returns an iterator over columns * @return an iterator over columns */ protected Iterator getColumns() { return m_columns.iterator(); } /** * Internal method that returns an iterator over column names * @return an iterator over column name */ protected Iterator getColumnNames() { return m_names.iterator(); } // ------------------------------------------------------------------------ // Column Metadata /** * Return a metadata instance providing summary information about a column. * @param field the data field name of the column * @return the columns' associated ColumnMetadata instance */ public ColumnMetadata getMetadata(String field) { ColumnEntry e = (ColumnEntry)m_entries.get(field); if ( e == null ) { throw new IllegalArgumentException("Unknown column name: "+field); } return e.metadata; } // ------------------------------------------------------------------------ // Index Methods /** * Create (if necessary) and return an index over the given data field. * The first call to this method with a given field name will cause the * index to be created and stored. Subsequent calls will simply return * the stored index. To attempt to retrieve an index without triggering * creation of a new index, use the {@link #getIndex(String)} method. * @param field the data field name of the column to index * @return the index over the specified data column */ public Index index(String field) { ColumnEntry e = (ColumnEntry)m_entries.get(field); if ( e == null ) { throw new IllegalArgumentException("Unknown column name: "+field); } else if ( e.index != null ) { return e.index; // already indexed } Column col = e.column; try { e.index = new TreeIndex(this, m_rows, col, null); } catch ( IncompatibleComparatorException ice ) { /* can't happen */ } return e.index; } /** * Retrieve, without creating, an index for the given data field. * @param field the data field name of the column * @return the stored index for the column, or null if no index has * been created */ public Index getIndex(String field) { ColumnEntry e = (ColumnEntry)m_entries.get(field); if ( e == null ) { throw new IllegalArgumentException("Unknown column name: "+field); } return e.index; } /** * Internal method for index creation and retrieval. * @param field the data field name of the column * @param expType the expected data type of the index * @param create indicates whether or not a new index should be created * if none currently exists for the given data field * @return the Index for the given data field */ protected Index getIndex(String field, Class expType, boolean create) { if ( !expType.equals(getColumnType(field)) ) { // TODO: need more nuanced type checking here? throw new IllegalArgumentException("Column type does not match."); } if ( getIndex(field)==null && create) { index(field); } return getIndex(field); } /** * Remove the Index associated with the given data field / column name. * @param field the name of the column for which to remove the index * @return true if an index was successfully removed, false if no * such index was found */ public boolean removeIndex(String field) { ColumnEntry e = (ColumnEntry)m_entries.get(field); if ( e == null ) { throw new IllegalArgumentException("Unknown column name: "+field); } if ( e.index == null ) { return false; } else { e.index.dispose(); e.index = null; return true; } } // ------------------------------------------------------------------------ // Tuple Methods /** * Get the Tuple instance providing object-oriented access to the given * table row. * @param row the table row * @return the Tuple for the given table row */ public Tuple getTuple(int row) { return m_tuples.getTuple(row); } /** * Add a Tuple to this table. If the Tuple is already a member of this * table, nothing is done and null is returned. If the Tuple is not * a member of this Table but has a compatible data schema, as * determined by {@link Schema#isAssignableFrom(Schema)}, a new row * is created, the Tuple's values are copied, and the new Tuple that * is a member of this Table is returned. If the data schemas are not * compatible, nothing is done and null is returned. * @param t the Tuple to "add" to this table * @return the actual Tuple instance added to this table, or null if * no new Tuple has been added * @see prefuse.data.tuple.TupleSet#addTuple(prefuse.data.Tuple) */ public Tuple addTuple(Tuple t) { if ( t.getTable() == this ) { return null; } else { Schema s = t.getSchema(); if ( getSchema().isAssignableFrom(s) ) { int r = addRow(); for ( int i=0; i<s.getColumnCount(); ++i ) { String field = s.getColumnName(i); this.set(r, field, t.get(i)); } return getTuple(r); } else { return null; } } } /** * Clears the contents of this table and then attempts to add the given * Tuple instance. * @param t the Tuple to make the sole tuple in thie table * @return the actual Tuple instance added to this table, or null if * no new Tuple has been added * @see prefuse.data.tuple.TupleSet#setTuple(prefuse.data.Tuple) */ public Tuple setTuple(Tuple t) { clear(); return addTuple(t); } /** * Remove a tuple from this table. If the Tuple is a member of this table, * its row is deleted from the table. Otherwise, nothing is done. * @param t the Tuple to remove from the table * @return true if the Tuple row was successfully deleted, false if the * Tuple is invalid or not a member of this table * @see prefuse.data.tuple.TupleSet#removeTuple(prefuse.data.Tuple) */ public boolean removeTuple(Tuple t) { if ( containsTuple(t) ) { removeRow(t.getRow()); return true; } else { return false; } } /** * Indicates if this table contains the given Tuple instance. * @param t the Tuple to check for containment * @return true if the Tuple represents a row of this table, false if * it does not * @see prefuse.data.tuple.TupleSet#containsTuple(prefuse.data.Tuple) */ public boolean containsTuple(Tuple t) { return (t.getTable()==this && isValidRow(t.getRow())); } /** * Get the number of tuples in this table. This is the same as the * value returned by {@link #getRowCount()}. * @return the number of tuples, which is the same as the number of rows * @see prefuse.data.tuple.TupleSet#getTupleCount() */ public int getTupleCount() { return getRowCount(); } /** * Returns true, as this table supports the addition of new data fields. * @see prefuse.data.tuple.TupleSet#isAddColumnSupported() */ public boolean isAddColumnSupported() { return true; } // ------------------------------------------------------------------------ // Data Access Methods /** * Check if the <code>get</code> method for the given data field returns * values that are compatible with a given target type. * @param field the data field to check * @param type a Class instance to check for compatibility with the * data field values. * @return true if the data field is compatible with provided type, * false otherwise. If the value is true, objects returned by * the {@link #get(int, String)} can be cast to the given type. * @see #get(int, String) */ public boolean canGet(String field, Class type) { Column c = getColumn(field); return ( c==null ? false : c.canGet(type) ); } /** * Check if the <code>set</code> method for the given data field can * accept values of a given target type. * @param field the data field to check * @param type a Class instance to check for compatibility with the * data field values. * @return true if the data field is compatible with provided type, * false otherwise. If the value is true, objects of the given type * can be used as parameters of the {@link #set(int, String, Object)} * method. * @see #set(int, String, Object) */ public boolean canSet(String field, Class type) { Column c = getColumn(field); return ( c==null ? false : c.canSet(type) ); } /** * Get the data value at the given row and field as an Object. * @param row the table row to get * @param field the data field to retrieve * @return the data value as an Object. The concrete type of this * Object is dependent on the underlying data column used. * @see #canGet(String, Class) * @see #getColumnType(String) */ public Object get(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).get(row); } /** * Set the value of a given row and data field. * @param row the table row to set * @param field the data field to set * @param val the value for the field. If the concrete type of this * Object is not compatible with the underlying data model, an * Exception will be thrown. Use the {@link #canSet(String, Class)} * method to check the type-safety ahead of time. * @see #canSet(String, Class) * @see #getColumnType(String) */ public void set(int row, String field, Object val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).set(val, row); // we don't fire a notification here, as we catch the // notification from the column itself and then dispatch } /** * Get the data value at the given row and column numbers as an Object. * @param row the row number * @param col the column number * @return the data value as an Object. The concrete type of this * Object is dependent on the underlying data column used. * @see #canGet(String, Class) * @see #getColumnType(int) */ public Object get(int row, int col) { row = getColumnRow(row, col); return getColumn(col).get(row); } /** * Set the value of at the given row and column numbers. * @param row the row number * @param col the column number * @param val the value for the field. If the concrete type of this * Object is not compatible with the underlying data model, an * Exception will be thrown. Use the {@link #canSet(String, Class)} * method to check the type-safety ahead of time. * @see #canSet(String, Class) * @see #getColumnType(String) */ public void set(int row, int col, Object val) { row = getColumnRow(row, col); getColumn(col).set(val, row); // we don't fire a notification here, as we catch the // notification from the column itself and then dispatch } /** * Get the default value for the given data field. * @param field the data field * @return the default value, as an Object, used to populate rows * of the data field. */ public Object getDefault(String field) { int col = getColumnNumber(field); return getColumn(col).getDefaultValue(); } /** * Revert this tuple's value for the given field to the default value * for the field. * @param field the data field * @see #getDefault(String) */ public void revertToDefault(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).revertToDefault(row); } // ------------------------------------------------------------------------ // Convenience Data Access Methods /** * Check if the given data field can return primitive <code>int</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>int</code> * values, false otherwise. If true, the {@link #getInt(int, String)} * method can be used safely. */ public final boolean canGetInt(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetInt() ); } /** * Check if the <code>setInt</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setInt(int, String, int)} method can safely * be used for the given field, false otherwise. */ public final boolean canSetInt(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetInt() ); } /** * Get the data value at the given row and field as an * <code>int</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetInt(String) */ public final int getInt(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getInt(row); } /** * Set the data value of the given row and field as an * <code>int</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetInt(String) */ public final void setInt(int row, String field, int val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setInt(val, row); } /** * Get the data value at the given row and field as an * <code>int</code>. * @param row the table row to retrieve * @param col the column number of the data field to retrieve * @see #canGetInt(String) */ public final int getInt(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getInt(row); } /** * Set the data value of the given row and field as an * <code>int</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetInt(String) */ public final void setInt(int row, int col, int val) { row = getColumnRow(row, col); getColumn(col).setInt(val, row); } // -------------------------------------------------------------- /** * Check if the given data field can return primitive <code>long</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>long</code> * values, false otherwise. If true, the {@link #getLong(int, String)} * method can be used safely. */ public final boolean canGetLong(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetLong() ); } /** * Check if the <code>setLong</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setLong(int, String, long)} method can * safely be used for the given field, false otherwise. */ public final boolean canSetLong(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetLong() ); } /** * Get the data value at the given row and field as a * <code>long</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetLong(String) */ public final long getLong(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getLong(row); } /** * Set the data value of the given row and field as a * <code>long</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetLong(String) */ public final void setLong(int row, String field, long val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setLong(val, row); } /** * Get the data value at the given row and field as an * <code>long</code>. * @param row the table row to retrieve * @param col the column number of the data field to retrieve * @see #canGetLong(String) */ public final long getLong(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getLong(row); } /** * Set the data value of the given row and field as an * <code>long</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetLong(String) */ public final void setLong(int row, int col, long val) { row = getColumnRow(row, col); getColumn(col).setLong(val, row); } // -------------------------------------------------------------- /** * Check if the given data field can return primitive <code>float</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>float</code> * values, false otherwise. If true, the {@link #getFloat(int, String)} * method can be used safely. */ public final boolean canGetFloat(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetFloat() ); } /** * Check if the <code>setFloat</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setFloat(int, String, float)} method can * safely be used for the given field, false otherwise. */ public final boolean canSetFloat(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetFloat() ); } /** * Get the data value at the given row and field as a * <code>float</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetFloat(String) */ public final float getFloat(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getFloat(row); } /** * Set the data value of the given row and field as a * <code>float</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetFloat(String) */ public final void setFloat(int row, String field, float val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setFloat(val, row); } /** * Get the data value at the given row and field as a * <code>float</code>. * @param row the table row to retrieve * @param col the column number of the data field to get * @see #canGetFloat(String) */ public final float getFloat(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getFloat(row); } /** * Set the data value of the given row and field as a * <code>float</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetFloat(String) */ public final void setFloat(int row, int col, float val) { row = getColumnRow(row, col); getColumn(col).setFloat(val, row); } // -------------------------------------------------------------- /** * Check if the given data field can return primitive <code>double</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>double</code> * values, false otherwise. If true, the {@link #getDouble(int, String)} * method can be used safely. */ public final boolean canGetDouble(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetDouble() ); } /** * Check if the <code>setDouble</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setDouble(int, String, double)} method can * safely be used for the given field, false otherwise. */ public final boolean canSetDouble(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetDouble() ); } /** * Get the data value at the given row and field as a * <code>double</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetDouble(String) */ public final double getDouble(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getDouble(row); } /** * Set the data value of the given row and field as a * <code>double</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetDouble(String) */ public final void setDouble(int row, String field, double val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setDouble(val, row); } /** * Get the data value at the given row and field as a * <code>double</code>. * @param row the table row to retrieve * @param col the column number of the data field to get * @see #canGetDouble(String) */ public final double getDouble(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getDouble(row); } /** * Set the data value of the given row and field as a * <code>double</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetDouble(String) */ public final void setDouble(int row, int col, double val) { row = getColumnRow(row, col); getColumn(col).setDouble(val, row); } // -------------------------------------------------------------- /** * Check if the given data field can return primitive <code>boolean</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>boolean</code> * values, false otherwise. If true, the {@link #getBoolean(int, String)} * method can be used safely. */ public final boolean canGetBoolean(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetBoolean() ); } /** * Check if the <code>setBoolean</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setBoolean(int, String, boolean)} method can * safely be used for the given field, false otherwise. */ public final boolean canSetBoolean(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetBoolean() ); } /** * Get the data value at the given row and field as a * <code>boolean</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetBoolean(String) */ public final boolean getBoolean(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getBoolean(row); } /** * Set the data value of the given row and field as a * <code>boolean</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetBoolean(String) */ public final void setBoolean(int row, String field, boolean val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setBoolean(val, row); } /** * Get the data value at the given row and field as a * <code>boolean</code>. * @param row the table row to retrieve * @param col the column number of the data field to get * @see #canGetBoolean(String) */ public final boolean getBoolean(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getBoolean(row); } /** * Set the data value of the given row and field as a * <code>boolean</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetBoolean(String) */ public final void setBoolean(int row, int col, boolean val) { row = getColumnRow(row, col); getColumn(col).setBoolean(val, row); } // -------------------------------------------------------------- /** * Check if the given data field can return primitive <code>String</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>String</code> * values, false otherwise. If true, the {@link #getString(int, String)} * method can be used safely. */ public final boolean canGetString(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetString() ); } /** * Check if the <code>setString</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setString(int, String, String)} method can * safely be used for the given field, false otherwise. */ public final boolean canSetString(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetString() ); } /** * Get the data value at the given row and field as a * <code>String</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetString(String) */ public final String getString(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getString(row); } /** * Set the data value of the given row and field as a * <code>String</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetString(String) */ public final void setString(int row, String field, String val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setString(val, row); } /** * Get the data value at the given row and field as a * <code>String</code>. * @param row the table row to retrieve * @param col the column number of the data field to retrieve * @see #canGetString(String) */ public final String getString(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getString(row); } /** * Set the data value of the given row and field as a * <code>String</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetString(String) */ public final void setString(int row, int col, String val) { row = getColumnRow(row, col); getColumn(col).setString(val, row); } // -------------------------------------------------------------- /** * Check if the given data field can return primitive <code>Date</code> * values. * @param field the data field to check * @return true if the data field can return primitive <code>Date</code> * values, false otherwise. If true, the {@link #getDate(int, String)} * method can be used safely. */ public final boolean canGetDate(String field) { Column col = getColumn(field); return ( col==null ? false : col.canGetDate() ); } /** * Check if the <code>setDate</code> method can safely be used for the * given data field. * @param field the data field to check * @return true if the {@link #setDate(int, String, Date)} method can * safely be used for the given field, false otherwise. */ public final boolean canSetDate(String field) { Column col = getColumn(field); return ( col==null ? false : col.canSetDate() ); } /** * Get the data value at the given row and field as a * <code>Date</code>. * @param row the table row to retrieve * @param field the data field to retrieve * @see #canGetDate(String) */ public final Date getDate(int row, String field) { int col = getColumnNumber(field); row = getColumnRow(row, col); return getColumn(col).getDate(row); } /** * Set the data value of the given row and field as a * <code>Date</code>. * @param row the table row to set * @param field the data field to set * @param val the value to set * @see #canSetDate(String) */ public final void setDate(int row, String field, Date val) { int col = getColumnNumber(field); row = getColumnRow(row, col); getColumn(col).setDate(val, row); } /** * Get the data value at the given row and field as a * <code>Date</code>. * @param row the table row to retrieve * @param col the column number of the data field to retrieve * @see #canGetDate(String) */ public final Date getDate(int row, int col) { row = getColumnRow(row, col); return getColumn(col).getDate(row); } /** * Set the data value of the given row and field as a * <code>Date</code>. * @param row the table row to set * @param col the column number of the data field to set * @param val the value to set * @see #canSetDate(String) */ public final void setDate(int row, int col, Date val) { row = getColumnRow(row, col); getColumn(col).setDate(val, row); } // ------------------------------------------------------------------------ // Query Operations /** * Query this table for a filtered, sorted subset of this table. This * operation creates an entirely new table independent of this table. * If a filtered view of this same table is preferred, use the * {@link CascadedTable} class. * @param filter the predicate filter determining which rows to include * in the new table. If this value is null, all rows will be included. * @param sort the sorting criteria determining the order in which * rows are added to the new table. If this value is null, the rows * will not be sorted. * @return a new table meeting the query specification */ public Table select(Predicate filter, Sort sort) { Table t = getSchema().instantiate(); Iterator tuples = tuples(filter, sort); while ( tuples.hasNext() ) { t.addTuple((Tuple)tuples.next()); } return t; } /** * Removes all table rows that meet the input predicate filter. * @param filter a predicate specifying which rows to remove from * the table. */ public void remove(Predicate filter) { for ( IntIterator ii = rows(filter); ii.hasNext(); ) removeRow(ii.nextInt()); } // ------------------------------------------------------------------------ // Iterators /** * Return a TableIterator over the rows of this table. * @return a TableIterator over this table */ public TableIterator iterator() { return iterator(rows()); } /** * Return a TableIterator over the given rows of this table. * @param rows an iterator over the table rows to visit * @return a TableIterator over this table */ public TableIterator iterator(IntIterator rows) { return new TableIterator(this, rows); } /** * Get an iterator over the tuples in this table. * @return an iterator over the table tuples * @see prefuse.data.tuple.TupleSet#tuples() */ public Iterator tuples() { return m_tuples.iterator(rows()); } /** * Get an iterator over the tuples in this table in reverse order. * @return an iterator over the table tuples in reverse order */ public Iterator tuplesReversed() { return m_tuples.iterator(rows(true)); } /** * Get an iterator over the tuples for the given rows in this table. * @param rows an iterator over the table rows to visit * @return an iterator over the selected table tuples */ public Iterator tuples(IntIterator rows) { return m_tuples.iterator(rows); } /** * Get an interator over the row numbers of this table. * @return an iterator over the rows of this table */ public IntIterator rows() { return m_rows.rows(); } /** * Get a filtered iterator over the row numbers of this table, returning * only the rows whose tuples match the given filter predicate. * @param filter the filter predicate to apply * @return a filtered iterator over the rows of this table */ public IntIterator rows(Predicate filter) { return FilterIteratorFactory.rows(this, filter); } /** * Get an interator over the row numbers of this table. * @param reverse true to iterate in rever order, false for normal order * @return an iterator over the rows of this table */ public IntIterator rows(boolean reverse) { return m_rows.rows(reverse); } /** * Get an iterator over the rows of this table, sorted by the given data * field. This method will create an index over the field if one does * not yet exist. * @param field the data field to sort by * @param ascend true if the iteration should proceed in an ascending * (lowest to highest) sort order, false for a descending order * @return the sorted iterator over rows of this table */ public IntIterator rowsSortedBy(String field, boolean ascend) { Class type = getColumnType(field); Index index = getIndex(field, type, true); int t = ascend ? Index.TYPE_ASCENDING : Index.TYPE_DESCENDING; return index.allRows(t); } /** * Return an iterator over a range of rwos in this table, determined * by a bounded range for a given data field. A new index over the * data field will be created if it doesn't already exist. * @param field the data field for determining the bounded range * @param lo the minimum range value * @param hi the maximum range value * @param indexType indicate the sort order and inclusivity/exclusivity * of the range bounds, using the constants of the * {@link prefuse.data.util.Index} class. * @return an iterator over a range of table rows, determined by a * sorted bounded range of a data field */ public IntIterator rangeSortedBy(String field, int lo, int hi, int indexType) { Index index = getIndex(field, int.class, true); return index.rows(lo, hi, indexType); } /** * Return an iterator over a range of rwos in this table, determined * by a bounded range for a given data field. A new index over the * data field will be created if it doesn't already exist. * @param field the data field for determining the bounded range * @param lo the minimum range value * @param hi the maximum range value * @param indexType indicate the sort order and inclusivity/exclusivity * of the range bounds, using the constants of the * {@link prefuse.data.util.Index} class. * @return an iterator over a range of table rows, determined by a * sorted bounded range of a data field */ public IntIterator rangeSortedBy(String field, long lo, long hi, int indexType) { Index index = getIndex(field, long.class, true); return index.rows(lo, hi, indexType); } /** * Return an iterator over a range of rwos in this table, determined * by a bounded range for a given data field. A new index over the * data field will be created if it doesn't already exist. * @param field the data field for determining the bounded range * @param lo the minimum range value * @param hi the maximum range value * @param indexType indicate the sort order and inclusivity/exclusivity * of the range bounds, using the constants of the * {@link prefuse.data.util.Index} class. * @return an iterator over a range of table rows, determined by a * sorted bounded range of a data field */ public IntIterator rangeSortedBy(String field, float lo, float hi, int indexType) { Index index = getIndex(field, float.class, true); return index.rows(lo, hi, indexType); } /** * Return an iterator over a range of rwos in this table, determined * by a bounded range for a given data field. A new index over the * data field will be created if it doesn't already exist. * @param field the data field for determining the bounded range * @param lo the minimum range value * @param hi the maximum range value * @param indexType indicate the sort order and inclusivity/exclusivity * of the range bounds, using the constants of the * {@link prefuse.data.util.Index} class. * @return an iterator over a range of table rows, determined by a * sorted bounded range of a data field */ public IntIterator rangeSortedBy(String field, double lo, double hi, int indexType) { Index index = getIndex(field, double.class, true); return index.rows(lo, hi, indexType); } /** * Return an iterator over a range of rwos in this table, determined * by a bounded range for a given data field. A new index over the * data field will be created if it doesn't already exist. * @param field the data field for determining the bounded range * @param lo the minimum range value * @param hi the maximum range value * @param indexType indicate the sort order and inclusivity/exclusivity * of the range bounds, using the constants of the * {@link prefuse.data.util.Index} class. * @return an iterator over a range of table rows, determined by a * sorted bounded range of a data field */ public IntIterator rangeSortedBy(String field, Object lo, Object hi, int indexType) { Class type = TypeLib.getSharedType(lo, hi); // TODO: check this for correctness if ( type == null ) throw new IllegalArgumentException("Incompatible arguments"); Index index = getIndex(field, type, true); return index.rows(lo, hi, indexType); } // ------------------------------------------------------------------------ // Listener Methods // -- ColumnListeners ----------------------------------------------------- /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, boolean) */ public void columnChanged(Column src, int idx, boolean prev) { handleColumnChanged(src, idx, idx); } /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, double) */ public void columnChanged(Column src, int idx, double prev) { handleColumnChanged(src, idx, idx); } /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, float) */ public void columnChanged(Column src, int idx, float prev) { handleColumnChanged(src, idx, idx); } /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, int) */ public void columnChanged(Column src, int idx, int prev) { handleColumnChanged(src, idx, idx); } /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, long) */ public void columnChanged(Column src, int idx, long prev) { handleColumnChanged(src, idx, idx); } /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, java.lang.Object) */ public void columnChanged(Column src, int idx, Object prev) { handleColumnChanged(src, idx, idx); } /** * @see prefuse.data.event.ColumnListener#columnChanged(prefuse.data.column.Column, int, int, int) */ public void columnChanged(Column src, int type, int start, int end) { handleColumnChanged(src, start, end); } /** * Handle a column change event. * @param c the modified column * @param start the starting row of the modified range * @param end the ending row (inclusive) of the modified range */ protected void handleColumnChanged(Column c, int start, int end) { for ( ; !isValidRow(start) && start <= end; ++start ); if ( start > end ) return; // bail if no valid rows // determine the index of the updated column int idx; if ( m_lastCol != -1 && c == getColumn(m_lastCol) ) { // constant time idx = m_lastCol; } else { // linear time idx = getColumnNumber(c); } // if we have a valid index, fire a notification if ( idx >= 0 ) { fireTableEvent(start, end, idx, TableModelEvent.UPDATE); } } // -- TableListeners ------------------------------------------------------ /** * Add a table listener to this table. * @param listnr the listener to add */ public void addTableListener(TableListener listnr) { if ( !m_listeners.contains(listnr) ) m_listeners.add(listnr); } /** * Remove a table listener from this table. * @param listnr the listener to remove */ public void removeTableListener(TableListener listnr) { m_listeners.remove(listnr); } /** * Removes all table listeners from this table. */ public void removeAllTableListeners() { m_listeners.clear(); } /** * Fire a table event to notify listeners. * @param row0 the starting row of the modified range * @param row1 the ending row (inclusive) of the modified range * @param col the number of the column modified, or * {@link prefuse.data.event.EventConstants#ALL_COLUMNS} for operations * effecting all columns. * @param type the table modification type, one of * {@link prefuse.data.event.EventConstants#INSERT}, * {@link prefuse.data.event.EventConstants#DELETE}, or * {@link prefuse.data.event.EventConstants#UPDATE}. */ protected void fireTableEvent(int row0, int row1, int col, int type) { // increment the modification count ++m_modCount; if ( type != EventConstants.UPDATE && col == EventConstants.ALL_COLUMNS ) { // fire event to all tuple set listeners fireTupleEvent(this, row0, row1, type); } if ( !m_listeners.isEmpty() ) { // fire event to all table listeners Object[] lstnrs = m_listeners.getArray(); for ( int i=0; i<lstnrs.length; ++i ) { ((TableListener)lstnrs[i]).tableChanged( this, row0, row1, col, type); } } } // ------------------------------------------------------------------------ // String Methods /** * @see java.lang.Object#toString() */ public String toString() { StringBuffer sbuf = new StringBuffer(); sbuf.append("Table["); sbuf.append("rows=").append(getRowCount()); sbuf.append(", cols=").append(getColumnCount()); sbuf.append(", maxrow=").append(m_rows.getMaximumRow()); sbuf.append("]"); return sbuf.toString(); } // ------------------------------------------------------------------------ // ColumnEntry helper /** * Helper class that encapsulates a map entry for a column, including the * column itself and its metadata and index. * * @author <a href="http://jheer.org">jeffrey heer</a> */ protected static class ColumnEntry { /** The column number. */ public int colnum; /** The Column instance. */ public Column column; /** The column metadata instance. */ public ColumnMetadata metadata; /** The column Index instance. */ public Index index; /** * Create a new ColumnEntry. * @param col the column number * @param column the Column instance * @param metadata the ColumnMetadata instance */ public ColumnEntry(int col, Column column, ColumnMetadata metadata) { this.colnum = col; this.column = column; this.metadata = metadata; this.index = null; } /** * Dispose of this column entry, disposing of any allocated * metadata or index instances. */ public void dispose() { if ( metadata != null ) metadata.dispose(); if ( index != null ) index.dispose(); } } // end of inner class ColumnEntry } // end of class Table