001: package prefuse.data;
002:
003: import java.util.ArrayList;
004: import java.util.Iterator;
005:
006: import javax.swing.event.TableModelEvent;
007:
008: import prefuse.data.column.Column;
009: import prefuse.data.column.ColumnMetadata;
010: import prefuse.data.event.EventConstants;
011: import prefuse.data.event.ExpressionListener;
012: import prefuse.data.event.ProjectionListener;
013: import prefuse.data.event.TableListener;
014: import prefuse.data.expression.BooleanLiteral;
015: import prefuse.data.expression.Expression;
016: import prefuse.data.expression.Predicate;
017: import prefuse.data.tuple.TableTuple;
018: import prefuse.data.util.AcceptAllColumnProjection;
019: import prefuse.data.util.CascadedRowManager;
020: import prefuse.data.util.ColumnProjection;
021: import prefuse.util.collections.CompositeIterator;
022: import prefuse.util.collections.IntIterator;
023:
024: /**
025: * <p>Table subclass featuring a "cascaded" table design - a CascadedTable can
026: * have a parent table, from which it inherits a potentially filtered set of
027: * rows and columns. Child tables may override the columns of the parent by
028: * having a column of the same name as that of the parent, in which case the
029: * parent's column will not be accessible.</p>
030: *
031: * <p>Table rows of the parent table can be selectively included by providing
032: * a {@link prefuse.data.expression.Predicate} that filters the parent rows.
033: * Columns of the parent table can be selectively included by providing
034: * a {@link prefuse.data.util.ColumnProjection} indicating the columns to
035: * include.</p>
036: *
037: * <p>Tuple instances backed by a CascadedTable will be not be equivalent to
038: * the tuples backed by the parent table. However, setting a value in a
039: * CascadedTable that is inherited from a parent table <em>will</em> update
040: * the value in the parent table.</p>
041: *
042: * @author <a href="http://jheer.org">jeffrey heer</a>
043: */
044: public class CascadedTable extends Table {
045:
046: /** Cascaded parent table */
047: protected Table m_parent;
048: /** List of included parent column names */
049: protected ArrayList m_pnames;
050:
051: /** ColumnProjection determining which columns of the parent table
052: * are included in this table. */
053: protected ColumnProjection m_colFilter;
054: /** Selection Predicate determining which rows of the parent table
055: * are included in this table. */
056: protected Predicate m_rowFilter;
057:
058: /** An internal listener class */
059: protected Listener m_listener;
060:
061: // ------------------------------------------------------------------------
062: // Constructor
063:
064: /**
065: * Create a new CascadedTable. By default all rows and columns of the
066: * parent table are included in this one.
067: * @param parent the parent Table to use
068: */
069: public CascadedTable(Table parent) {
070: this (parent, null, null);
071: }
072:
073: /**
074: * Create a new CascadedTable. By default all columns of the parent
075: * table are included in this one.
076: * @param parent the parent Table to use
077: * @param rowFilter a Predicate determining which rows of the parent
078: * table to include in this one.
079: */
080: public CascadedTable(Table parent, Predicate rowFilter) {
081: this (parent, rowFilter, null);
082: }
083:
084: /**
085: * Create a new CascadedTable. By default all rows of the parent
086: * table are included in this one.
087: * @param parent the parent Table to use
088: * @param colFilter a ColumnProjection determining which columns of the
089: * parent table to include in this one.
090: */
091: public CascadedTable(Table parent, ColumnProjection colFilter) {
092: this (parent, null, colFilter);
093: }
094:
095: /**
096: * Create a new CascadedTable.
097: * @param parent the parent Table to use
098: * @param rowFilter a Predicate determining which rows of the parent
099: * table to include in this one.
100: * @param colFilter a ColumnProjection determining which columns of the
101: * parent table to include in this one.
102: */
103: public CascadedTable(Table parent, Predicate rowFilter,
104: ColumnProjection colFilter) {
105: this (parent, rowFilter, colFilter, TableTuple.class);
106: }
107:
108: /**
109: * Create a new CascadedTable.
110: * @param parent the parent Table to use
111: * @param rowFilter a Predicate determining which rows of the parent
112: * table to include in this one.
113: * @param colFilter a ColumnProjection determining which columns of the
114: * parent table to include in this one.
115: * @param tupleType the class type of the Tuple instances to use
116: */
117: protected CascadedTable(Table parent, Predicate rowFilter,
118: ColumnProjection colFilter, Class tupleType) {
119: super (0, 0, tupleType);
120: m_parent = parent;
121: m_pnames = new ArrayList();
122: m_rows = new CascadedRowManager(this );
123: m_listener = new Listener();
124:
125: setColumnProjection(colFilter);
126: setRowFilter(rowFilter);
127: m_parent.addTableListener(m_listener);
128: }
129:
130: // -- non-cascading version -----------------------------------------------
131:
132: /**
133: * Create a CascadedTable without a backing parent table.
134: */
135: protected CascadedTable() {
136: this (TableTuple.class);
137: }
138:
139: /**
140: * Create a CascadedTable without a backing parent table.
141: * @param tupleType the class type of the Tuple instances to use
142: */
143: protected CascadedTable(Class tupleType) {
144: super (0, 0, tupleType);
145: m_pnames = new ArrayList();
146: }
147:
148: // ------------------------------------------------------------------------
149: // Filter Methods
150:
151: /**
152: * Determines which columns are inherited from the backing parent table.
153: */
154: protected void filterColumns() {
155: if (m_parent == null)
156: return;
157:
158: for (int i = 0; i < m_pnames.size(); ++i) {
159: String name = (String) m_pnames.get(i);
160: Column col = m_parent.getColumn(i);
161: boolean contained = m_names.contains(name);
162: if (!m_colFilter.include(col, name) || contained) {
163: m_pnames.remove(i--);
164: if (!contained) {
165: ((ColumnEntry) m_entries.get(name)).dispose();
166: m_entries.remove(name);
167: }
168:
169: // fire notification
170: fireTableEvent(m_rows.getMinimumRow(), m_rows
171: .getMaximumRow(), i, EventConstants.DELETE);
172: }
173: }
174:
175: m_pnames.clear();
176:
177: Iterator pcols = m_parent.getColumnNames();
178: for (int i = 0, j = m_columns.size(); pcols.hasNext(); ++i) {
179: String name = (String) pcols.next();
180: Column col = m_parent.getColumn(i);
181:
182: if (m_colFilter.include(col, name)
183: && !m_names.contains(name)) {
184: m_pnames.add(name);
185: ColumnEntry entry = (ColumnEntry) m_entries.get(name);
186: if (entry == null) {
187: entry = new ColumnEntry(j++, col,
188: new ColumnMetadata(this , name));
189: m_entries.put(name, entry);
190: // fire notification
191: fireTableEvent(m_rows.getMinimumRow(), m_rows
192: .getMaximumRow(), i, EventConstants.INSERT);
193: } else {
194: entry.colnum = j++;
195: }
196: m_lastCol = m_columns.size() - 1;
197: }
198: }
199:
200: }
201:
202: /**
203: * Manually trigger a re-filtering of the rows of this table. If the
204: * filtering predicate concerns only items within this table, calling
205: * this method should be unnecessary. It is only when the filtering
206: * predicate references data outside of this table that a manual
207: * re-filtering request may be necessary. For example, filtering
208: * valid edges of a graph from a pool of candidate edges will depend
209: * on the available nodes.
210: * @see prefuse.data.util.ValidEdgePredicate
211: */
212: public void filterRows() {
213: if (m_parent == null)
214: return;
215:
216: CascadedRowManager rowman = (CascadedRowManager) m_rows;
217: IntIterator crows = m_rows.rows();
218: while (crows.hasNext()) {
219: int crow = crows.nextInt();
220: if (!m_rowFilter.getBoolean(m_parent.getTuple(rowman
221: .getParentRow(crow)))) {
222: removeCascadedRow(crow);
223: }
224: }
225:
226: Iterator ptuples = m_parent.tuples(m_rowFilter);
227: while (ptuples.hasNext()) {
228: Tuple pt = (Tuple) ptuples.next();
229: int prow = pt.getRow();
230: if (rowman.getChildRow(prow) == -1)
231: addCascadedRow(prow);
232: }
233: }
234:
235: /**
236: * Get the ColumnProjection determining which columns of the
237: * parent table are included in this one.
238: * @return the ColumnProjection of this CascadedTable
239: */
240: public ColumnProjection getColumnProjection() {
241: return m_colFilter;
242: }
243:
244: /**
245: * Sets the ColumnProjection determining which columns of the
246: * parent table are included in this one.
247: * @param colFilter a ColumnProjection determining which columns of the
248: * parent table to include in this one.
249: */
250: public void setColumnProjection(ColumnProjection colFilter) {
251: if (m_colFilter != null) {
252: m_colFilter.removeProjectionListener(m_listener);
253: }
254: m_colFilter = colFilter == null ? new AcceptAllColumnProjection()
255: : colFilter;
256: m_colFilter.addProjectionListener(m_listener);
257: filterColumns();
258: }
259:
260: /**
261: * Gets ths Predicate determining which rows of the parent
262: * table are included in this one.
263: * @return the row filtering Predicate of this CascadedTable
264: */
265: public Predicate getRowFilter() {
266: return m_rowFilter;
267: }
268:
269: /**
270: * Sets the Predicate determining which rows of the parent
271: * table are included in this one.
272: * @param rowFilter a Predicate determining which rows of the parent
273: * table to include in this one.
274: */
275: public void setRowFilter(Predicate rowFilter) {
276: if (m_rowFilter != null) {
277: m_rowFilter.removeExpressionListener(m_listener);
278: }
279: m_rowFilter = rowFilter == null ? BooleanLiteral.TRUE
280: : rowFilter;
281: if (m_rowFilter != BooleanLiteral.TRUE)
282: m_rowFilter.addExpressionListener(m_listener);
283: filterRows();
284: }
285:
286: // ------------------------------------------------------------------------
287: // Table Metadata
288:
289: /**
290: * @see prefuse.data.Table#getColumnCount()
291: */
292: public int getColumnCount() {
293: return m_columns.size() + m_pnames.size();
294: }
295:
296: /**
297: * Get the number of columns explicitly stored by this table (i.e., all
298: * columns that are not inherited from the parent table).
299: * @return the number of locally stored columns
300: */
301: public int getLocalColumnCount() {
302: return m_columns.size();
303: }
304:
305: // ------------------------------------------------------------------------
306: // Parent Table Methods
307:
308: /**
309: * Get the parent table from which this cascaded table inherits values.
310: * @return the parent table
311: */
312: public Table getParentTable() {
313: return m_parent;
314: }
315:
316: /**
317: * Given a row in this table, return the corresponding row in the parent
318: * table.
319: * @param row a row in this table
320: * @return the corresponding row in the parent table
321: */
322: public int getParentRow(int row) {
323: return ((CascadedRowManager) m_rows).getParentRow(row);
324: }
325:
326: /**
327: * Given a row in the parent table, return the corresponding row, if any,
328: * in this table.
329: * @param prow a row in the parent table
330: * @return the corresponding row in this table, or -1 if the given parent
331: * row is not inherited by this table
332: */
333: public int getChildRow(int prow) {
334: return ((CascadedRowManager) m_rows).getChildRow(prow);
335: }
336:
337: // ------------------------------------------------------------------------
338: // Row Operations
339:
340: /**
341: * @see prefuse.data.Table#addRow()
342: */
343: public int addRow() {
344: if (m_parent != null) {
345: throw new IllegalStateException(
346: "Add row not supported for CascadedTable.");
347: } else {
348: return super .addRow();
349: }
350: }
351:
352: /**
353: * @see prefuse.data.Table#addRows(int)
354: */
355: public void addRows(int nrows) {
356: if (m_parent != null) {
357: throw new IllegalStateException(
358: "Add rows not supported for CascadedTable.");
359: } else {
360: super .addRows(nrows);
361: }
362: }
363:
364: /**
365: * @see prefuse.data.Table#removeRow(int)
366: */
367: public boolean removeRow(int row) {
368: if (m_parent != null) {
369: throw new IllegalStateException(
370: "Remove row not supported for CascadedTable.");
371: } else {
372: return super .removeRow(row);
373: }
374: }
375:
376: /**
377: * Internal method for adding a new cascaded row backed by
378: * the given parent row.
379: * @param prow the parent row to inherit
380: * @return the row number ofr the newly added row in this table
381: */
382: protected int addCascadedRow(int prow) {
383: int r = m_rows.addRow();
384: ((CascadedRowManager) m_rows).put(r, prow);
385: updateRowCount();
386:
387: fireTableEvent(r, r, TableModelEvent.ALL_COLUMNS,
388: TableModelEvent.INSERT);
389: return r;
390: }
391:
392: /**
393: * Internal method for removing a cascaded row from this table.
394: * @param row the row to remove
395: * @return true if the row was successfully removed, false otherwise
396: */
397: protected boolean removeCascadedRow(int row) {
398: boolean rv = super .removeRow(row);
399: if (rv)
400: ((CascadedRowManager) m_rows).remove(row);
401: return rv;
402: }
403:
404: // ------------------------------------------------------------------------
405: // Column Operations
406:
407: /**
408: * @see prefuse.data.Table#getColumnName(int)
409: */
410: public String getColumnName(int col) {
411: int local = m_names.size();
412: if (col >= local) {
413: return (String) m_pnames.get(col - local);
414: } else {
415: return (String) m_names.get(col);
416: }
417: }
418:
419: /**
420: * @see prefuse.data.Table#getColumnNumber(prefuse.data.column.Column)
421: */
422: public int getColumnNumber(Column col) {
423: int idx = m_columns.indexOf(col);
424: if (idx == -1 && m_parent != null) {
425: idx = m_parent.getColumnNumber(col);
426: if (idx == -1)
427: return idx;
428: String name = m_parent.getColumnName(idx);
429: idx = m_pnames.indexOf(name);
430: if (idx != -1)
431: idx += m_columns.size();
432: }
433: return idx;
434: }
435:
436: /**
437: * @see prefuse.data.Table#getColumn(int)
438: */
439: public Column getColumn(int col) {
440: m_lastCol = col;
441: int local = m_names.size();
442: if (col >= local && m_parent != null) {
443: return m_parent.getColumn((String) m_pnames
444: .get(col - local));
445: } else {
446: return (Column) m_columns.get(col);
447: }
448: }
449:
450: /**
451: * @see prefuse.data.Table#hasColumn(java.lang.String)
452: */
453: protected boolean hasColumn(String name) {
454: int idx = getColumnNumber(name);
455: return idx >= 0 && idx < getLocalColumnCount();
456: }
457:
458: /**
459: * @see prefuse.data.Table#getColumnNames()
460: */
461: protected Iterator getColumnNames() {
462: if (m_parent == null) {
463: return m_names.iterator();
464: } else {
465: return new CompositeIterator(m_names.iterator(), m_pnames
466: .iterator());
467: }
468: }
469:
470: /**
471: * Invalidates this table's cached schema. This method should be called
472: * whenever columns are added or removed from this table.
473: */
474: protected void invalidateSchema() {
475: super .invalidateSchema();
476: this .filterColumns();
477: }
478:
479: // ------------------------------------------------------------------------
480: // Listener Methods
481:
482: /**
483: * Internal listener class handling updates from the backing parent table,
484: * the column projection, or the row selection predicate.
485: */
486: private class Listener implements TableListener,
487: ProjectionListener, ExpressionListener {
488: public void tableChanged(Table t, int start, int end, int col,
489: int type) {
490: // must come from parent
491: if (t != m_parent)
492: return;
493:
494: CascadedRowManager rowman = (CascadedRowManager) m_rows;
495:
496: // switch on the event type
497: switch (type) {
498: case EventConstants.UPDATE: {
499: // do nothing if update on all columns, as this is only
500: // used to indicate a non-measurable update.
501: if (col == EventConstants.ALL_COLUMNS) {
502: break;
503: }
504:
505: // process each update, check if filtered state changes
506: for (int r = start, cr = -1; r <= end; ++r) {
507: if ((cr = rowman.getChildRow(r)) != -1) {
508: // the parent row has a corresponding row in this table
509: if (m_rowFilter
510: .getBoolean(m_parent.getTuple(r))) {
511: // row still passes the filter, check the column
512: int idx = getColumnNumber(m_parent
513: .getColumnName(col));
514: if (idx >= getLocalColumnCount())
515: fireTableEvent(cr, cr, idx,
516: EventConstants.UPDATE);
517: } else {
518: // row no longer passes the filter, remove it
519: removeCascadedRow(cr);
520: }
521: } else {
522: // does it now pass the filter due to the update?
523: if (m_rowFilter
524: .getBoolean(m_parent.getTuple(r))) {
525: if ((cr = rowman.getChildRow(r)) < 0)
526: addCascadedRow(r);
527: }
528: }
529: }
530: break;
531: }
532: case EventConstants.DELETE: {
533: if (col == EventConstants.ALL_COLUMNS) {
534: // entire rows deleted
535: for (int r = start, cr = -1; r <= end; ++r) {
536: if ((cr = rowman.getChildRow(r)) != -1)
537: removeCascadedRow(cr);
538: }
539: } else {
540: // column deleted
541: filterColumns();
542: }
543: break;
544: }
545: case EventConstants.INSERT:
546: if (col == EventConstants.ALL_COLUMNS) {
547: // entire rows added
548: for (int r = start; r <= end; ++r) {
549: if (m_rowFilter
550: .getBoolean(m_parent.getTuple(r))) {
551: if (rowman.getChildRow(r) < 0)
552: addCascadedRow(r);
553: }
554: }
555: } else {
556: // column added
557: filterColumns();
558: }
559: break;
560: }
561: }
562:
563: public void projectionChanged(ColumnProjection projection) {
564: if (projection == m_colFilter)
565: filterColumns();
566: }
567:
568: public void expressionChanged(Expression expr) {
569: if (expr == m_rowFilter)
570: filterRows();
571: }
572: }
573:
574: } // end of class CascadedTable
|