package prefuse.data;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.event.TableModelEvent;
import prefuse.data.column.Column;
import prefuse.data.column.ColumnMetadata;
import prefuse.data.event.EventConstants;
import prefuse.data.event.ExpressionListener;
import prefuse.data.event.ProjectionListener;
import prefuse.data.event.TableListener;
import prefuse.data.expression.BooleanLiteral;
import prefuse.data.expression.Expression;
import prefuse.data.expression.Predicate;
import prefuse.data.tuple.TableTuple;
import prefuse.data.util.AcceptAllColumnProjection;
import prefuse.data.util.CascadedRowManager;
import prefuse.data.util.ColumnProjection;
import prefuse.util.collections.CompositeIterator;
import prefuse.util.collections.IntIterator;
/**
* <p>Table subclass featuring a "cascaded" table design - a CascadedTable can
* have a parent table, from which it inherits a potentially filtered set of
* rows and columns. Child tables may override the columns of the parent by
* having a column of the same name as that of the parent, in which case the
* parent's column will not be accessible.</p>
*
* <p>Table rows of the parent table can be selectively included by providing
* a {@link prefuse.data.expression.Predicate} that filters the parent rows.
* Columns of the parent table can be selectively included by providing
* a {@link prefuse.data.util.ColumnProjection} indicating the columns to
* include.</p>
*
* <p>Tuple instances backed by a CascadedTable will be not be equivalent to
* the tuples backed by the parent table. However, setting a value in a
* CascadedTable that is inherited from a parent table <em>will</em> update
* the value in the parent table.</p>
*
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class CascadedTable extends Table {
/** Cascaded parent table */
protected Table m_parent;
/** List of included parent column names */
protected ArrayList m_pnames;
/** ColumnProjection determining which columns of the parent table
* are included in this table. */
protected ColumnProjection m_colFilter;
/** Selection Predicate determining which rows of the parent table
* are included in this table. */
protected Predicate m_rowFilter;
/** An internal listener class */
protected Listener m_listener;
// ------------------------------------------------------------------------
// Constructor
/**
* Create a new CascadedTable. By default all rows and columns of the
* parent table are included in this one.
* @param parent the parent Table to use
*/
public CascadedTable(Table parent) {
this(parent, null, null);
}
/**
* Create a new CascadedTable. By default all columns of the parent
* table are included in this one.
* @param parent the parent Table to use
* @param rowFilter a Predicate determining which rows of the parent
* table to include in this one.
*/
public CascadedTable(Table parent, Predicate rowFilter) {
this(parent, rowFilter, null);
}
/**
* Create a new CascadedTable. By default all rows of the parent
* table are included in this one.
* @param parent the parent Table to use
* @param colFilter a ColumnProjection determining which columns of the
* parent table to include in this one.
*/
public CascadedTable(Table parent, ColumnProjection colFilter) {
this(parent, null, colFilter);
}
/**
* Create a new CascadedTable.
* @param parent the parent Table to use
* @param rowFilter a Predicate determining which rows of the parent
* table to include in this one.
* @param colFilter a ColumnProjection determining which columns of the
* parent table to include in this one.
*/
public CascadedTable(Table parent, Predicate rowFilter,
ColumnProjection colFilter)
{
this(parent, rowFilter, colFilter, TableTuple.class);
}
/**
* Create a new CascadedTable.
* @param parent the parent Table to use
* @param rowFilter a Predicate determining which rows of the parent
* table to include in this one.
* @param colFilter a ColumnProjection determining which columns of the
* parent table to include in this one.
* @param tupleType the class type of the Tuple instances to use
*/
protected CascadedTable(Table parent, Predicate rowFilter,
ColumnProjection colFilter, Class tupleType)
{
super(0, 0, tupleType);
m_parent = parent;
m_pnames = new ArrayList();
m_rows = new CascadedRowManager(this);
m_listener = new Listener();
setColumnProjection(colFilter);
setRowFilter(rowFilter);
m_parent.addTableListener(m_listener);
}
// -- non-cascading version -----------------------------------------------
/**
* Create a CascadedTable without a backing parent table.
*/
protected CascadedTable() {
this(TableTuple.class);
}
/**
* Create a CascadedTable without a backing parent table.
* @param tupleType the class type of the Tuple instances to use
*/
protected CascadedTable(Class tupleType) {
super(0, 0, tupleType);
m_pnames = new ArrayList();
}
// ------------------------------------------------------------------------
// Filter Methods
/**
* Determines which columns are inherited from the backing parent table.
*/
protected void filterColumns() {
if ( m_parent == null ) return;
for ( int i=0; i<m_pnames.size(); ++i ) {
String name = (String)m_pnames.get(i);
Column col = m_parent.getColumn(name);
boolean contained = m_names.contains(name);
boolean removedFromParent = (col == null);
if (removedFromParent || !m_colFilter.include(col, name) || contained ) {
m_pnames.remove(i--);
if ( !contained ) {
((ColumnEntry)m_entries.get(name)).dispose();
m_entries.remove(name);
}
// fire notification
fireTableEvent(m_rows.getMinimumRow(),
m_rows.getMaximumRow(),
i, EventConstants.DELETE);
}
}
m_pnames.clear();
Iterator pcols = m_parent.getColumnNames();
for ( int i=0, j=m_columns.size(); pcols.hasNext(); ++i ) {
String name = (String)pcols.next();
Column col = m_parent.getColumn(i);
if ( m_colFilter.include(col, name) && !m_names.contains(name) ) {
m_pnames.add(name);
ColumnEntry entry = (ColumnEntry)m_entries.get(name);
if ( entry == null ) {
entry = new ColumnEntry(j++, col,
new ColumnMetadata(this, name));
m_entries.put(name, entry);
// fire notification
fireTableEvent(m_rows.getMinimumRow(),
m_rows.getMaximumRow(),
i, EventConstants.INSERT);
} else {
entry.colnum = j++;
}
m_lastCol = m_columns.size()-1;
}
}
}
/**
* Manually trigger a re-filtering of the rows of this table. If the
* filtering predicate concerns only items within this table, calling
* this method should be unnecessary. It is only when the filtering
* predicate references data outside of this table that a manual
* re-filtering request may be necessary. For example, filtering
* valid edges of a graph from a pool of candidate edges will depend
* on the available nodes.
* @see prefuse.data.util.ValidEdgePredicate
*/
public void filterRows() {
if ( m_parent == null ) return;
CascadedRowManager rowman = (CascadedRowManager)m_rows;
IntIterator crows = m_rows.rows();
while ( crows.hasNext() ) {
int crow = crows.nextInt();
if ( !m_rowFilter.getBoolean(
m_parent.getTuple(rowman.getParentRow(crow))) )
{
removeCascadedRow(crow);
}
}
Iterator ptuples = m_parent.tuples(m_rowFilter);
while ( ptuples.hasNext() ) {
Tuple pt = (Tuple)ptuples.next();
int prow = pt.getRow();
if ( rowman.getChildRow(prow) == -1 )
addCascadedRow(prow);
}
}
/**
* Get the ColumnProjection determining which columns of the
* parent table are included in this one.
* @return the ColumnProjection of this CascadedTable
*/
public ColumnProjection getColumnProjection() {
return m_colFilter;
}
/**
* Sets the ColumnProjection determining which columns of the
* parent table are included in this one.
* @param colFilter a ColumnProjection determining which columns of the
* parent table to include in this one.
*/
public void setColumnProjection(ColumnProjection colFilter) {
if ( m_colFilter != null ) {
m_colFilter.removeProjectionListener(m_listener);
}
m_colFilter = colFilter==null ? new AcceptAllColumnProjection() : colFilter;
m_colFilter.addProjectionListener(m_listener);
filterColumns();
}
/**
* Gets ths Predicate determining which rows of the parent
* table are included in this one.
* @return the row filtering Predicate of this CascadedTable
*/
public Predicate getRowFilter() {
return m_rowFilter;
}
/**
* Sets the Predicate determining which rows of the parent
* table are included in this one.
* @param rowFilter a Predicate determining which rows of the parent
* table to include in this one.
*/
public void setRowFilter(Predicate rowFilter) {
if ( m_rowFilter != null ) {
m_rowFilter.removeExpressionListener(m_listener);
}
m_rowFilter = rowFilter==null ? BooleanLiteral.TRUE : rowFilter;
if ( m_rowFilter != BooleanLiteral.TRUE )
m_rowFilter.addExpressionListener(m_listener);
filterRows();
}
// ------------------------------------------------------------------------
// Table Metadata
/**
* @see prefuse.data.Table#getColumnCount()
*/
public int getColumnCount() {
return m_columns.size() + m_pnames.size();
}
/**
* Get the number of columns explicitly stored by this table (i.e., all
* columns that are not inherited from the parent table).
* @return the number of locally stored columns
*/
public int getLocalColumnCount() {
return m_columns.size();
}
// ------------------------------------------------------------------------
// Parent Table Methods
/**
* Get the parent table from which this cascaded table inherits values.
* @return the parent table
*/
public Table getParentTable() {
return m_parent;
}
/**
* Given a row in this table, return the corresponding row in the parent
* table.
* @param row a row in this table
* @return the corresponding row in the parent table
*/
public int getParentRow(int row) {
return ((CascadedRowManager)m_rows).getParentRow(row);
}
/**
* Given a row in the parent table, return the corresponding row, if any,
* in this table.
* @param prow a row in the parent table
* @return the corresponding row in this table, or -1 if the given parent
* row is not inherited by this table
*/
public int getChildRow(int prow) {
return ((CascadedRowManager)m_rows).getChildRow(prow);
}
// ------------------------------------------------------------------------
// Row Operations
/**
* @see prefuse.data.Table#addRow()
*/
public int addRow() {
if ( m_parent != null ) {
throw new IllegalStateException(
"Add row not supported for CascadedTable.");
} else {
return super.addRow();
}
}
/**
* @see prefuse.data.Table#addRows(int)
*/
public void addRows(int nrows) {
if ( m_parent != null ) {
throw new IllegalStateException(
"Add rows not supported for CascadedTable.");
} else {
super.addRows(nrows);
}
}
/**
* @see prefuse.data.Table#removeRow(int)
*/
public boolean removeRow(int row) {
if ( m_parent != null ) {
throw new IllegalStateException(
"Remove row not supported for CascadedTable.");
} else {
return super.removeRow(row);
}
}
/**
* Internal method for adding a new cascaded row backed by
* the given parent row.
* @param prow the parent row to inherit
* @return the row number ofr the newly added row in this table
*/
protected int addCascadedRow(int prow) {
int r = m_rows.addRow();
((CascadedRowManager)m_rows).put(r, prow);
updateRowCount();
fireTableEvent(r, r, TableModelEvent.ALL_COLUMNS,
TableModelEvent.INSERT);
return r;
}
/**
* Internal method for removing a cascaded row from this table.
* @param row the row to remove
* @return true if the row was successfully removed, false otherwise
*/
protected boolean removeCascadedRow(int row) {
boolean rv = super.removeRow(row);
if ( rv )
((CascadedRowManager)m_rows).remove(row);
return rv;
}
// ------------------------------------------------------------------------
// Column Operations
/**
* @see prefuse.data.Table#getColumnName(int)
*/
public String getColumnName(int col) {
int local = m_names.size();
if ( col >= local ) {
return (String)m_pnames.get(col-local);
} else {
return (String)m_names.get(col);
}
}
/**
* @see prefuse.data.Table#getColumnNumber(prefuse.data.column.Column)
*/
public int getColumnNumber(Column col) {
int idx = m_columns.indexOf(col);
if ( idx == -1 && m_parent != null ) {
idx = m_parent.getColumnNumber(col);
if ( idx == -1 ) return idx;
String name = m_parent.getColumnName(idx);
idx = m_pnames.indexOf(name);
if ( idx != -1 ) idx += m_columns.size();
}
return idx;
}
/**
* @see prefuse.data.Table#getColumn(int)
*/
public Column getColumn(int col) {
m_lastCol = col;
int local = m_names.size();
if ( col >= local && m_parent != null ) {
return m_parent.getColumn((String)m_pnames.get(col-local));
} else {
return (Column)m_columns.get(col);
}
}
/**
* @see prefuse.data.Table#hasColumn(java.lang.String)
*/
protected boolean hasColumn(String name) {
int idx = getColumnNumber(name);
return idx >= 0 && idx < getLocalColumnCount();
}
/**
* @see prefuse.data.Table#getColumnNames()
*/
protected Iterator getColumnNames() {
if ( m_parent == null ) {
return m_names.iterator();
} else {
return new CompositeIterator(m_names.iterator(),
m_pnames.iterator());
}
}
/**
* Invalidates this table's cached schema. This method should be called
* whenever columns are added or removed from this table.
*/
protected void invalidateSchema() {
super.invalidateSchema();
this.filterColumns();
}
// ------------------------------------------------------------------------
// Listener Methods
/**
* Internal listener class handling updates from the backing parent table,
* the column projection, or the row selection predicate.
*/
private class Listener
implements TableListener, ProjectionListener, ExpressionListener
{
public void tableChanged(Table t, int start, int end, int col, int type) {
// must come from parent
if ( t != m_parent )
return;
CascadedRowManager rowman = (CascadedRowManager)m_rows;
// switch on the event type
switch ( type ) {
case EventConstants.UPDATE:
{
// do nothing if update on all columns, as this is only
// used to indicate a non-measurable update.
if ( col == EventConstants.ALL_COLUMNS ) {
break;
}
// process each update, check if filtered state changes
for ( int r=start, cr=-1; r<=end; ++r ) {
if ( (cr=rowman.getChildRow(r)) != -1 ) {
// the parent row has a corresponding row in this table
if ( m_rowFilter.getBoolean(m_parent.getTuple(r)) ) {
// row still passes the filter, check the column
int idx = getColumnNumber(m_parent.getColumnName(col));
if ( idx >= getLocalColumnCount() )
fireTableEvent(cr, cr, idx, EventConstants.UPDATE);
} else {
// row no longer passes the filter, remove it
removeCascadedRow(cr);
}
} else {
// does it now pass the filter due to the update?
if ( m_rowFilter.getBoolean(m_parent.getTuple(r)) ) {
if ( (cr=rowman.getChildRow(r)) < 0 )
addCascadedRow(r);
}
}
}
break;
}
case EventConstants.DELETE:
{
if ( col == EventConstants.ALL_COLUMNS ) {
// entire rows deleted
for ( int r=start, cr=-1; r<=end; ++r ) {
if ( (cr=rowman.getChildRow(r)) != -1 )
removeCascadedRow(cr);
}
} else {
// column deleted
filterColumns();
}
break;
}
case EventConstants.INSERT:
if ( col == EventConstants.ALL_COLUMNS ) {
// entire rows added
for ( int r=start; r<=end; ++r ) {
if ( m_rowFilter.getBoolean(m_parent.getTuple(r)) ) {
if ( rowman.getChildRow(r) < 0 )
addCascadedRow(r);
}
}
} else {
// column added
filterColumns();
}
break;
}
}
public void projectionChanged(ColumnProjection projection) {
if ( projection == m_colFilter )
filterColumns();
}
public void expressionChanged(Expression expr) {
if ( expr == m_rowFilter )
filterRows();
}
}
} // end of class CascadedTable