package prefuse.data.column; import java.util.BitSet; import java.util.Iterator; import java.util.Set; import prefuse.data.DataTypeException; import prefuse.data.Table; import prefuse.data.event.ColumnListener; import prefuse.data.event.EventConstants; import prefuse.data.event.ExpressionListener; import prefuse.data.expression.Expression; import prefuse.data.expression.ExpressionAnalyzer; /** * <p>Column instance that stores values provided by an Expression * instance. These expressions can reference other column values within the * same table. Values are evaluated when first requested and then cached to * increase performance. This column maintains listeners for all referenced * columns discovered in the expression and for the expression itself, * invalidating all cached entries when an update to either occurs.</p> * * <p> * WARNING: Infinite recursion, eventually resulting in a StackOverflowError, * could occur if an expression refers to its own column, or if two * ExpressionColumns have expressions referring to each other. The * responsibility for avoiding such situations is left with client programmers. * Note that it is fine for one ExpressionColumn to reference another; * however, the graph induced by such references must not contain any cycles. * </p> * * @author <a href="http://jheer.org">jeffrey heer</a> * @see prefuse.data.expression */ public class ExpressionColumn extends AbstractColumn { private Expression m_expr; private Table m_table; private Set m_columns; private BitSet m_valid; private Column m_cache; private Listener m_lstnr; /** * Create a new ExpressionColumn. * @param table the table this column is a member of * @param expr the expression used to provide the column values */ public ExpressionColumn(Table table, Expression expr) { super(expr.getType(table.getSchema())); m_table = table; m_expr = expr; m_lstnr = new Listener(); init(); int nrows = m_table.getRowCount(); m_cache = ColumnFactory.getColumn(getColumnType(), nrows); m_valid = new BitSet(nrows); m_expr.addExpressionListener(m_lstnr); } protected void init() { // first remove listeners on any current columns if ( m_columns != null && m_columns.size() > 0 ) { Iterator iter = m_columns.iterator(); while ( iter.hasNext() ) { String field = (String)iter.next(); Column col = m_table.getColumn(field); col.removeColumnListener(m_lstnr); } } // now get the current set of columns m_columns = ExpressionAnalyzer.getReferencedColumns(m_expr); // sanity check table and expression Iterator iter = m_columns.iterator(); while ( iter.hasNext() ) { String name = (String)iter.next(); if ( m_table.getColumn(name) == null ) throw new IllegalArgumentException("Table must contain all " + "columns referenced by the expression." + " Bad column name: "+name); } // passed check, so now listen to columns iter = m_columns.iterator(); while ( iter.hasNext() ) { String field = (String)iter.next(); Column col = m_table.getColumn(field); col.addColumnListener(m_lstnr); } } // ------------------------------------------------------------------------ // Column Metadata /** * @see prefuse.data.column.Column#getRowCount() */ public int getRowCount() { return m_cache.getRowCount(); } /** * @see prefuse.data.column.Column#setMaximumRow(int) */ public void setMaximumRow(int nrows) { m_cache.setMaximumRow(nrows); } // ------------------------------------------------------------------------ // Cache Management /** * Check if this ExpressionColumn has a valid cached value at the given * row. * @param row the row to check for a valid cache entry * @return true if the cache row is valid, false otherwise */ public boolean isCacheValid(int row) { return m_valid.get(row); } /** * Invalidate a range of the cache. * @param start the start of the range to invalidate * @param end the end of the range to invalidate, inclusive */ public void invalidateCache(int start, int end ) { m_valid.clear(start, end+1); } // ------------------------------------------------------------------------ // Data Access Methods /** * Has no effect, as all values in this column are derived. * @param row the row to revert */ public void revertToDefault(int row) { // do nothing, as we don't have default values. } /** * @see prefuse.data.column.AbstractColumn#canSet(java.lang.Class) */ public boolean canSet(Class type) { return false; } /** * @see prefuse.data.column.Column#get(int) */ public Object get(int row) { rangeCheck(row); if ( isCacheValid(row) ) { return m_cache.get(row); } Object val = m_expr.get(m_table.getTuple(row)); Class type = val==null ? Object.class : val.getClass(); if ( m_cache.canSet(type) ) { m_cache.set(val, row); m_valid.set(row); } return val; } /** * @see prefuse.data.column.Column#set(java.lang.Object, int) */ public void set(Object val, int row) throws DataTypeException { throw new UnsupportedOperationException(); } private void rangeCheck(int row) { if ( row < 0 || row >= getRowCount() ) throw new IndexOutOfBoundsException(); } // ------------------------------------------------------------------------ /** * @see prefuse.data.column.Column#getBoolean(int) */ public boolean getBoolean(int row) throws DataTypeException { if ( !canGetBoolean() ) throw new DataTypeException(boolean.class); rangeCheck(row); if ( isCacheValid(row) ) { return m_cache.getBoolean(row); } else { boolean value = m_expr.getBoolean(m_table.getTuple(row)); m_cache.setBoolean(value, row); m_valid.set(row); return value; } } private void computeNumber(int row) { if ( m_columnType == int.class || m_columnType == byte.class ) { m_cache.setInt(m_expr.getInt(m_table.getTuple(row)), row); } else if ( m_columnType == long.class ) { m_cache.setLong(m_expr.getLong(m_table.getTuple(row)), row); } else if ( m_columnType == float.class ) { m_cache.setFloat(m_expr.getFloat(m_table.getTuple(row)), row); } else { m_cache.setDouble(m_expr.getDouble(m_table.getTuple(row)), row); } m_valid.set(row); } /** * @see prefuse.data.column.Column#getInt(int) */ public int getInt(int row) throws DataTypeException { if ( !canGetInt() ) throw new DataTypeException(int.class); rangeCheck(row); if ( !isCacheValid(row) ) computeNumber(row); return m_cache.getInt(row); } /** * @see prefuse.data.column.Column#getDouble(int) */ public double getDouble(int row) throws DataTypeException { if ( !canGetDouble() ) throw new DataTypeException(double.class); rangeCheck(row); if ( !isCacheValid(row) ) computeNumber(row); return m_cache.getDouble(row); } /** * @see prefuse.data.column.Column#getFloat(int) */ public float getFloat(int row) throws DataTypeException { if ( !canGetFloat() ) throw new DataTypeException(float.class); rangeCheck(row); if ( !isCacheValid(row) ) computeNumber(row); return m_cache.getFloat(row); } /** * @see prefuse.data.column.Column#getLong(int) */ public long getLong(int row) throws DataTypeException { if ( !canGetLong() ) throw new DataTypeException(long.class); rangeCheck(row); if ( !isCacheValid(row) ) computeNumber(row); return m_cache.getLong(row); } // ------------------------------------------------------------------------ // Listener Methods private class Listener implements ColumnListener, ExpressionListener { public void columnChanged(int start, int end) { // for a single index change with a valid cache value, // propagate a change event with the previous value if ( start == end && isCacheValid(start) ) { if ( !m_table.isValidRow(start) ) return; // invalidate the cache index invalidateCache(start, end); // fire change event including previous value Class type = getColumnType(); if ( int.class == type ) { fireColumnEvent(start, m_cache.getInt(start)); } else if ( long.class == type ) { fireColumnEvent(start, m_cache.getLong(start)); } else if ( float.class == type ) { fireColumnEvent(start, m_cache.getFloat(start)); } else if ( double.class == type ) { fireColumnEvent(start, m_cache.getDouble(start)); } else if ( boolean.class == type ) { fireColumnEvent(start, m_cache.getBoolean(start)); } else { fireColumnEvent(start, m_cache.get(start)); } // otherwise send a generic update } else { // invalidate cache indices invalidateCache(start, end); // fire change event fireColumnEvent(EventConstants.UPDATE, start, end); } } public void columnChanged(Column src, int idx, boolean prev) { columnChanged(idx, idx); } public void columnChanged(Column src, int idx, double prev) { columnChanged(idx, idx); } public void columnChanged(Column src, int idx, float prev) { columnChanged(idx, idx); } public void columnChanged(Column src, int type, int start, int end) { columnChanged(start, end); } public void columnChanged(Column src, int idx, int prev) { columnChanged(idx, idx); } public void columnChanged(Column src, int idx, long prev) { columnChanged(idx, idx); } public void columnChanged(Column src, int idx, Object prev) { columnChanged(idx, idx); } public void expressionChanged(Expression expr) { // mark everything as changed columnChanged(0, m_cache.getRowCount()-1); // re-initialize our setup init(); } } } // end of class DerivedColumn