/* Copyright (C) 2006 EBI This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the itmplied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.biomart.builder.model; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.WeakHashMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.biomart.builder.exceptions.PartitionException; import org.biomart.builder.model.DataSet.DataSetTable; import org.biomart.builder.model.Schema.JDBCSchema; import org.biomart.common.exceptions.BioMartError; import org.biomart.common.resources.Log; import org.biomart.common.resources.Resources; import org.biomart.common.utils.BeanList; import org.biomart.common.utils.BeanMap; import org.biomart.common.utils.Transaction; import org.biomart.common.utils.WeakPropertyChangeSupport; import org.biomart.common.utils.Transaction.TransactionEvent; import org.biomart.common.utils.Transaction.TransactionListener; /** * The partition table interface allows lists of values to be stored, with those * lists broken into sub-lists if required. Each entry in the list can consist * of multiple columns each labelled with a unique name. The partition table * itself also has a unique name. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.32 $, $Date: 2007-12-17 09:34:01 $, modified by * $Author: rh4 $ * @since 0.7 */ public abstract class PartitionTable implements TransactionListener, Comparable { /** * Subclasses use this field to fire events of their own. */ protected final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private boolean visibleModified = true; private boolean directModified = false; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { PartitionTable.this.setDirectModified(true); } }; /** * Use this constant to pass to methods which require a number of rows as a * parameter. */ public static final int UNLIMITED_ROWS = -1; /** * Use this marker in the selected column list to indicate the start of a * subdivision. */ public static final String DIV_COLUMN = "__SUBDIVISION_BOUNDARY__"; /** * Use this marker to indicate that the partitoin is applied to the whole * dataset, not just a dimension in it. */ public static final String NO_DIMENSION = ""; /** * Internal use only, by anonymous subclass. Sorted by column name. */ protected BeanMap columnMap = new BeanMap(new TreeMap()); private int rowIterator = -1; private PartitionRow currentRow = null; private List rows = null; /** * Internal use only, for subdivision tables only. */ protected final List subRows = new ArrayList(); /** * Internal use only, by anonymous subclass. */ protected List selectedColumnNames = new ArrayList(); /** * Internal use only, by anonymous subclass. */ protected List groupCols = new ArrayList(); private PartitionTable subdivision = null; private final Map dmApplications = new WeakHashMap(); // Unique IDs for avoiding listener probs. private static int ID_SERIES = 0; private final int uniqueId = PartitionTable.ID_SERIES++; /** * Create a new, empty, partition table. */ public PartitionTable() { // Set up listening for property changes. Transaction.addTransactionListener(this); // All changes to us make us modified. this.addPropertyChangeListener(this.listener); } public boolean isDirectModified() { return this.directModified; } public void setDirectModified(final boolean modified) { if (modified == this.directModified) return; final boolean oldValue = this.directModified; this.directModified = modified; this.pcs.firePropertyChange("directModified", oldValue, modified); } public boolean isVisibleModified() { return this.visibleModified; } public void setVisibleModified(final boolean modified) { // We don't care as this gets set internally. } public void transactionResetVisibleModified() { this.visibleModified = false; } public void transactionResetDirectModified() { this.directModified = false; } public void transactionStarted(final TransactionEvent evt) { // Don't really care for now. } public void transactionEnded(final TransactionEvent evt) { // Do nothing. } /** * Adds a property change listener. * * @param listener * the listener to add. */ public void addPropertyChangeListener(final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(listener); } /** * Adds a property change listener. * * @param property * the property to listen to. * @param listener * the listener to add. */ public void addPropertyChangeListener(final String property, final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(property, listener); } /** * Return all applications of this table. Keys are datasets, values are * nested maps of dimension names to applications. Note that the * applications are wrapped in WeakReferences and when resolved may be null. * * @return the map. */ public Map getAllApplications() { return this.dmApplications; } /** * What is our name? * * @return the name. */ public abstract String getName(); /** * What is our original name? * * @return the original name. */ public abstract String getOriginalName(); /** * What columns can we list and include? * * @return the list of includable columns. */ public abstract Collection getAvailableColumnNames(); /** * Get ready to iterate over the rows in this table. After calling this, a * call to {@link #nextRow()} will return the first row. * * @param schemaPrefix * the partition of the schema we are getting rows from, if the * table needs it (<tt>null</tt> otherwise). This value is * used when establishing a connection to the schema. See * {@link JDBCSchema#getConnection(String)}. If <tt>null</tt> * is passed when it needs a non-null value then it should use a * sensible default. * @param limit * the maximum number of rows to return, or * {@link #UNLIMITED_ROWS} for no limit. * @throws PartitionException * if anything went wrong. */ public void prepareRows(final String schemaPrefix, final int limit) throws PartitionException { Log.debug("Preparing rows"); this.currentRow = null; this.rows = new ArrayList(this.getRows(schemaPrefix)); // Iterate over rows, apply transforms, drop duplicates. final Set seen = new HashSet(); for (final Iterator i = this.rows.iterator(); i.hasNext();) { final PartitionRow row = (PartitionRow) i.next(); final StringBuffer buf = new StringBuffer(); for (final Iterator j = this.columnMap.values().iterator(); j .hasNext();) { final PartitionColumn pcol = (PartitionColumn) j.next(); buf.append(pcol.getValueForRow(row)); buf.append(','); } final String result = buf.toString(); if (!seen.contains(result)) seen.add(result); else i.remove(); } Collections.sort(this.rows); if (limit != PartitionTable.UNLIMITED_ROWS && this.rows.size() > limit) this.rows = this.rows.subList(0, limit); this.rowIterator = 0; } /** * How many rows do we have? * * @return the number of rows. */ public int countRows() { return this.rows.size(); } /** * Move to the next row, or the first row if not yet called. This will skip * over all rows with an identical set of values used to define any * subdivision. * * @return <tt>true</tt> if it could, or <tt>false</tt> if there are no * more. * @throws PartitionException * if anything went wrong. */ public boolean nextRow() throws PartitionException { return this.getNextRow(false); } /** * Move to the next row, or the first row if not yet called. This will not * skip over multiple rows with the same subdivision-defining values. * * @return <tt>true</tt> if it could, or <tt>false</tt> if there are no * more. * @throws PartitionException * if anything went wrong. */ public boolean nudgeRow() throws PartitionException { return this.getNextRow(true); } /** * Return the current row. If {@link #nextRow()} has not been called since * {@link #prepareRows(String, int)} was called, or you are calling this * after a failed call to {@link #nextRow()} then you will get an exception. * * @return the current row. * @throws PartitionException * if anything went wrong, or there is no current row. */ public PartitionRow currentRow() throws PartitionException { // Exception if currentRow is null. if (this.currentRow == null) throw new PartitionException(Resources .get("partitionCurrentBeforeNext")); return this.currentRow; } /** * What columns do we have? * * @return the column names in keys and the columns themselves in values. */ public BeanMap getColumns() { return this.columnMap; } /** * What columns did the user select? (This may contain entries which are * equal to {@link #DIV_COLUMN} which indicate the location of subdivision * boundaries.) * * @return the ordered list of selected columns. */ public List getSelectedColumnNames() { return this.selectedColumnNames; } /** * What columns did the user select? (This may contain entries which are * equal to {@link #DIV_COLUMN} which indicate the location of subdivision * boundaries.) * * @param selectedColumnNames * the ordered list of selected column names. * @throws PartitionException * if any of them are invalid. */ public void setSelectedColumnNames(final List selectedColumnNames) throws PartitionException { final List oldValue = this.selectedColumnNames; if (oldValue.equals(selectedColumnNames)) return; // Preserve any existing regexes. final Map regexStore = new HashMap(this.columnMap); // Clear-out. this.selectedColumnNames.clear(); this.groupCols.clear(); this.columnMap.clear(); // Construct new table hierarchy. String previous = ""; for (final Iterator i = selectedColumnNames.iterator(); i.hasNext();) { final String col = (String) i.next(); if (col.equals(PartitionTable.DIV_COLUMN)) // Don't allow back-to-back divs. if (previous.equals(col)) continue; // Don't allow div-at-end. else if (!i.hasNext()) continue; // Don't allow div-at-start. else if ("".equals(previous)) continue; this.selectedColumnNames.add(col); previous = col; } // Column groupings into subdivisions. final List currentGroupCols = new ArrayList(); int groupPos = 0; while (groupPos < this.selectedColumnNames.size() && !this.selectedColumnNames.get(groupPos).equals( PartitionTable.DIV_COLUMN)) currentGroupCols.add(this.selectedColumnNames.get(groupPos++)); // Set up initial table. for (final Iterator i = currentGroupCols.iterator(); i.hasNext();) { final String col = (String) i.next(); this.groupCols.add(col); final PartitionColumn pcol = new PartitionColumn(this, col); pcol.addPropertyChangeListener(this.listener); final PartitionColumn regexStored = (PartitionColumn) regexStore .get(pcol.getName()); if (regexStored != null) { pcol.setRegexMatch(regexStored.getRegexMatch()); pcol.setRegexReplace(regexStored.getRegexReplace()); } this.columnMap.put(col, pcol); } PartitionTable currentPT = this; while (groupPos < this.selectedColumnNames.size()) { // Skip DIV itself. if (groupPos < this.selectedColumnNames.size()) groupPos++; // Extend group cols to next DIV final List newGroupCols = new ArrayList(); while (groupPos < this.selectedColumnNames.size() && !this.selectedColumnNames.get(groupPos).equals( PartitionTable.DIV_COLUMN)) { final String col = (String) this.selectedColumnNames .get(groupPos++); currentGroupCols.add(col); newGroupCols.add(col); } // Create subdiv PT with extended group cols final PartitionTable parent = this; final PartitionTable subdiv = new PartitionTable() { { // Subdiv column map = pointer this column map. this.columnMap = parent.columnMap; // Subdiv selected cols = pointer this selected cols. this.selectedColumnNames = parent.selectedColumnNames; // Subdiv group cols = copy currentGroupCols this.groupCols = new ArrayList(currentGroupCols); } protected List getRows(String schemaPartition) throws PartitionException { return this.subRows; } public Collection getAvailableColumnNames() { return parent.getAvailableColumnNames(); } public String getName() { return parent.getName(); } public String getOriginalName() { return parent.getOriginalName(); } }; // Assign subdiv to current PT currentPT.subdivision = subdiv; // Create column objects for each new group col for (final Iterator i = newGroupCols.iterator(); i.hasNext();) { final String col = (String) i.next(); // Assign column objects to new subdiv final PartitionColumn pcol = new PartitionColumn(subdiv, col); pcol.addPropertyChangeListener(this.listener); final PartitionColumn regexStored = (PartitionColumn) regexStore .get(pcol.getName()); if (regexStored != null) { pcol.setRegexMatch(regexStored.getRegexMatch()); pcol.setRegexReplace(regexStored.getRegexReplace()); } this.columnMap.put(col, pcol); } // Set current PT = new subdiv currentPT = subdiv; } this.pcs.firePropertyChange("selectedColumnNames", oldValue, selectedColumnNames); } /** * Apply this partition table to the given dimension using the given * definition. If the definition is null, apply using defaults. * * @param ds * the dataset. * @param dimension * the dimension. * @param appl * the application definition (null for default). */ public void applyTo(final DataSet ds, String dimension, PartitionTableApplication appl) { if (dimension == null || dimension.equals(PartitionTable.NO_DIMENSION)) dimension = PartitionTable.NO_DIMENSION; if (!this.dmApplications.containsKey(ds)) this.dmApplications.put(ds, new HashMap()); if (appl == null) appl = PartitionTableApplication.createDefault(this, ds, dimension); ((Map) this.dmApplications.get(ds)).put(dimension, new WeakReference( appl)); if (!dimension.equals(PartitionTable.NO_DIMENSION)) ((DataSetTable) ds.getTables().get(dimension)) .setPartitionTableApplication(appl); else ds.setPartitionTableApplication(appl); // Listen to the applied rows. if (appl != null) appl.addPropertyChangeListener("directModified", this.listener); // Fire event - we have no before/after, so a simple event will do. this.pcs.firePropertyChange("partitionTableApplication", null, appl); } /** * Remove this partition table from a dimension. * * @param ds * the dataset. * @param dimension * the dimension. */ public void removeFrom(final DataSet ds, String dimension) { if (dimension == null || dimension.equals(PartitionTable.NO_DIMENSION)) dimension = PartitionTable.NO_DIMENSION; if (!this.dmApplications.containsKey(ds)) return; ((Map) this.dmApplications.get(ds)).remove(dimension); if (((Map) this.dmApplications.get(ds)).isEmpty()) this.dmApplications.remove(ds); if (!dimension.equals(PartitionTable.NO_DIMENSION)) { if (ds.getTables().containsKey(dimension)) ((DataSetTable) ds.getTables().get(dimension)) .setPartitionTableApplication(null); } else ds.setPartitionTableApplication(null); // Fire event - we have no before/after, so a simple event will do. this.pcs.firePropertyChange("partitionTableApplication", null, null); } private boolean getNextRow(final boolean nudge) throws PartitionException { // If row iterator is negative, throw exception. if (this.rowIterator < 0) throw new PartitionException(Resources .get("partitionIterateBeforePopulate")); // Exception if doesn't have a next, and set currentRow to // null. if (this.rowIterator >= this.rows.size()) return false; // Update current row. this.currentRow = (PartitionRow) this.rows.get(this.rowIterator++); // Set up the sub-division tables. if (this.subdivision != null) { this.subdivision.subRows.clear(); this.subdivision.subRows.add(this.currentRow); // Keep adding rows till find one not same. boolean keepGoing = !nudge; while (keepGoing && this.rowIterator < this.rows.size()) { final PartitionRow subRow = (PartitionRow) this.rows .get(this.rowIterator); for (final Iterator i = this.groupCols.iterator(); i.hasNext() && keepGoing;) { final String subColName = (String) i.next(); final PartitionColumn pcol = (PartitionColumn) this .getColumns().get(subColName); final String parentValue = pcol .getValueForRow(this.currentRow); final String subValue = pcol.getValueForRow(subRow); keepGoing &= parentValue.equals(subValue); } if (keepGoing) { this.subdivision.subRows.add(subRow); this.rowIterator++; } } } return true; } /** * Implementing methods should use this to build a list of rows and return * it. Iteration will be handled by the parent. Duplicated rows, if any, * will be handled by the parent, as will any regexing or special row * manipulation. * * @param schemaPrefix * the partition to get rows for, or <tt>null</tt> if not to * bother. * @return the rows. Never <tt>null</tt> but may be empty. * @throws PartitionException * if the rows couldn't be obtained. */ protected abstract List getRows(final String schemaPrefix) throws PartitionException; public boolean equals(final Object obj) { if (obj == this) return true; else if (obj == null) return false; else if (obj instanceof PartitionTable) { final PartitionTable pt = (PartitionTable) obj; return (this.uniqueId + "_" + this.getOriginalName()) .equals(pt.uniqueId + "_" + pt.getOriginalName()); } else return false; } public int compareTo(final Object obj) { final PartitionTable pt = (PartitionTable) obj; return (this.uniqueId + "_" + this.getOriginalName()) .compareTo(pt.uniqueId + "_" + pt.getOriginalName()); } public String toString() { return this.getName(); } /** * A column knows its name. */ public static class PartitionColumn implements TransactionListener { private final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private boolean visibleModified = true; private boolean directModified = false; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { PartitionColumn.this.setDirectModified(true); } }; private final PartitionTable table; private String name; private String regexMatch = null; private String regexReplace = null; private Pattern compiled = null; /** * Construct a new column that is going to be added to this table (but * don't actually add it yet). * * @param table * the table. * @param name * the name. */ public PartitionColumn(final PartitionTable table, final String name) { // Set up listening for property changes. Transaction.addTransactionListener(this); // All changes to us make us modified. this.addPropertyChangeListener(this.listener); this.table = table; this.name = name; } public boolean isDirectModified() { return this.directModified; } public void setDirectModified(final boolean modified) { if (modified == this.directModified) return; final boolean oldValue = this.directModified; this.directModified = modified; this.pcs.firePropertyChange("directModified", oldValue, modified); } public boolean isVisibleModified() { return this.visibleModified; } public void setVisibleModified(final boolean modified) { // We don't care as this gets set internally. } public void transactionResetVisibleModified() { this.visibleModified = false; } public void transactionResetDirectModified() { this.directModified = false; } public void transactionStarted(final TransactionEvent evt) { // Don't really care for now. } public void transactionEnded(final TransactionEvent evt) { // Don't really care for now. } /** * Adds a property change listener. * * @param listener * the listener to add. */ public void addPropertyChangeListener( final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(listener); } /** * Adds a property change listener. * * @param property * the property to listen to. * @param listener * the listener to add. */ public void addPropertyChangeListener(final String property, final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(property, listener); } /** * Find out which table this column belongs to. * * @return the table. */ public PartitionTable getPartitionTable() { return this.table; } /** * Find out the column name. * * @return the column name. */ public String getName() { return this.name; } /** * Get the value in this column for the specified row. * * @param row * the row. * @return the value. * @throws PartitionException * if there were problems getting the value. */ public String getValueForRow(final PartitionRow row) throws PartitionException { if (this.compiled == null && this.regexMatch != null) try { this.compiled = Pattern.compile(this.regexMatch); } catch (final PatternSyntaxException pe) { this.compiled = null; } if (this.compiled != null && this.regexReplace != null) return this.compiled.matcher(row.getValue(this.getName())) .replaceAll(this.regexReplace); else return row.getValue(this.getName()); } /** * Get the value in this column for the specified row. * * @param row * the row. * @return the value. * @throws PartitionException * if there were problems getting the value. */ public String getRawValueForRow(final PartitionRow row) throws PartitionException { return row.getValue(this.getName()); } /** * Set the regex to use to match values. * * @param regexMatch * the regex. */ public void setRegexMatch(final String regexMatch) { final String oldValue = this.regexMatch; if (oldValue == regexMatch || oldValue != null && oldValue.equals(regexMatch)) return; this.regexMatch = regexMatch; this.pcs.firePropertyChange("regexMatch", oldValue, regexMatch); this.compiled = null; } /** * What regex are we using to match values? * * @return the regex. */ public String getRegexMatch() { return this.regexMatch; } /** * Set the regex to use to replace values. * * @param regexReplace * the regex. */ public void setRegexReplace(final String regexReplace) { final String oldValue = this.regexReplace; if (oldValue == regexReplace || oldValue != null && oldValue.equals(regexReplace)) return; this.regexReplace = regexReplace; this.pcs.firePropertyChange("regexReplace", oldValue, regexReplace); } /** * What regex are we using to replace values? * * @return the regex. */ public String getRegexReplace() { return this.regexReplace; } /** * A fake column for use when partitioning is not applied, but the * algorithm requires a partition column object. */ public static class FakeColumn extends PartitionColumn { /** * Construct a fake column that belongs to a fake table which only * has one row, with no columns. */ public FakeColumn() { super(new PartitionTable() { public String getName() { return "__FAKE__TABLE__"; } public String getOriginalName() { return "__FAKE__TABLE__"; } public Collection getAvailableColumnNames() { return Collections.EMPTY_SET; } public BeanMap getColumns() { return new BeanMap(Collections.EMPTY_MAP); } protected List getRows(final String schemaPartition) throws PartitionException { final List rows = new ArrayList(); rows.add(new PartitionRow(this) { public String getValue(final String columnName) throws PartitionException { // Should never get called. If it does, // then the empty string should suffice. return ""; } }); return rows; } }, null); } } } /** * This class defines how rows of the table will behave. */ public static abstract class PartitionRow implements Comparable { private final PartitionTable table; /** * Use this constructor to make a new numbered row. The numbers are not * checked so use with care. * * @param table * the table this row belongs to. */ protected PartitionRow(final PartitionTable table) { this.table = table; } /** * Find out which table this row belongs to. * * @return the table. */ public PartitionTable getPartitionTable() { return this.table; } /** * Return the value in the given column. If null, returns "null". * * @param columnName * the column. * @return the value. * @throws PartitionException * if there was a problem, or the column does not exist. */ public abstract String getValue(final String columnName) throws PartitionException; public String toString() { final StringBuffer sbuff = new StringBuffer(); for (final Iterator i = this.getPartitionTable() .getSelectedColumnNames().iterator(); i.hasNext();) { final String colName = (String) i.next(); if (colName.equals(PartitionTable.DIV_COLUMN)) continue; try { final PartitionColumn col = (PartitionColumn) this .getPartitionTable().getColumns().get(colName); sbuff.append(col.getValueForRow(this)); } catch (final PartitionException pe) { throw new BioMartError(pe); } } return sbuff.toString(); } public int compareTo(final Object obj) { final PartitionRow them = (PartitionRow) obj; for (final Iterator i = this.getPartitionTable() .getSelectedColumnNames().iterator(); i.hasNext();) { final String colName = (String) i.next(); if (colName.equals(PartitionTable.DIV_COLUMN)) continue; try { final PartitionColumn col = (PartitionColumn) this .getPartitionTable().getColumns().get(colName); if (!col.getValueForRow(this).equals( col.getValueForRow(them))) return col.getValueForRow(this).compareTo( col.getValueForRow(them)); } catch (final PartitionException pe) { throw new BioMartError(pe); } } return 0; } } /** * Defines how a partition table is applied in real life. */ public static class PartitionTableApplication implements TransactionListener { private final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private boolean visibleModified = true; private boolean directModified = false; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { PartitionTableApplication.this.setDirectModified(true); } }; private final PropertyChangeListener rowsListener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { final Collection newRows = new HashSet( PartitionTableApplication.this.partitionAppliedRows); newRows.removeAll(PartitionTableApplication.this.rowCache); for (final Iterator i = newRows.iterator(); i.hasNext();) { final PartitionAppliedRow row = (PartitionAppliedRow) i .next(); row .addPropertyChangeListener(PartitionTableApplication.this.listener); } PartitionTableApplication.this.rowCache.clear(); PartitionTableApplication.this.rowCache .addAll(PartitionTableApplication.this.partitionAppliedRows); PartitionTableApplication.this.setDirectModified(true); } }; private final PartitionTable pt; private final BeanList partitionAppliedRows = new BeanList( new ArrayList()); private final Collection rowCache = new HashSet(); /** * Construct a new, empty, partition table application. * * @param pt * the partition table. */ public PartitionTableApplication(final PartitionTable pt) { // Set up listening for property changes. Transaction.addTransactionListener(this); // All changes to us make us modified. this.addPropertyChangeListener(this.listener); this.pt = pt; // Listen to partitionAppliedRows // Also listen to each new row added. this.partitionAppliedRows.addPropertyChangeListener(this.listener); this.partitionAppliedRows .addPropertyChangeListener(this.rowsListener); } /** * Replicate ourselves. * * @return the copy. */ public PartitionTableApplication replicate() { final PartitionTableApplication appl = new PartitionTableApplication( this.pt); for (final Iterator i = this.partitionAppliedRows.iterator(); i .hasNext();) appl.partitionAppliedRows.add(((PartitionAppliedRow) i.next()) .replicate()); return appl; } public boolean isDirectModified() { return this.directModified; } public void setDirectModified(final boolean modified) { if (modified == this.directModified) return; final boolean oldValue = this.directModified; this.directModified = modified; this.pcs.firePropertyChange("directModified", oldValue, modified); } public boolean isVisibleModified() { return this.visibleModified; } public void setVisibleModified(final boolean modified) { // We don't care as this gets set internally. } public void transactionResetVisibleModified() { this.visibleModified = false; } public void transactionResetDirectModified() { this.directModified = false; } public void transactionStarted(final TransactionEvent evt) { // Don't really care for now. } public void transactionEnded(final TransactionEvent evt) { // Don't really care for now. } /** * Adds a property change listener. * * @param listener * the listener to add. */ public void addPropertyChangeListener( final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(listener); } /** * Adds a property change listener. * * @param property * the property to listen to. * @param listener * the listener to add. */ public void addPropertyChangeListener(final String property, final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(property, listener); } /** * For a given relation, obtain the row that applies it. * * @param rel * the relation. * @return the applied row. */ public PartitionAppliedRow getAppliedRowForRelation(final Relation rel) { for (final Iterator i = this.partitionAppliedRows.iterator(); i .hasNext();) { final PartitionAppliedRow row = (PartitionAppliedRow) i.next(); if (row.getRelation() != null && row.getRelation().equals(rel)) return row; } return null; } /** * What table is this applying? * * @return the table. */ public PartitionTable getPartitionTable() { return this.pt; } /** * Obtain a list of all partition rows. * * @return the list. */ public BeanList getPartitionAppliedRows() { return this.partitionAppliedRows; } /** * Convenience method to get the column to use to provide the name for * the first entry in the applied rows. * * @return the real naming column. * @throws PartitionException * if it cannot. */ public PartitionColumn getNamePartitionCol() throws PartitionException { return (PartitionColumn) this.getPartitionTable().getColumns().get( ((PartitionAppliedRow) this.partitionAppliedRows.get(0)) .getNamePartitionCol()); } /** * Update the compound relation counts internally. * * @throws PartitionException * if it goes wrong. */ public void syncCounts() throws PartitionException { // Get real partition table for each alias and count rows. for (int i = 0; i < this.partitionAppliedRows.size(); i++) { final PartitionAppliedRow prow = (PartitionAppliedRow) this.partitionAppliedRows .get(i); final String partitionCol = prow.getPartitionCol(); final PartitionTable realPT = ((PartitionColumn) this.pt .getColumns().get(partitionCol)).getPartitionTable(); int compound = 0; realPT.prepareRows(null, PartitionTable.UNLIMITED_ROWS); while (realPT.nextRow()) compound++; prow.setCompound(compound); } } /** * Create a default application based on the given dataset. * * @param pt * the partition table. * @param ds * the dataset. * @return the default application. */ public static PartitionTableApplication createDefault( final PartitionTable pt, final DataSet ds) { final PartitionTableApplication pa = new PartitionTableApplication( pt); final String ptCol = (String) pt.getSelectedColumnNames() .iterator().next(); pa.getPartitionAppliedRows().add( new PartitionAppliedRow(ptCol, (String) ds.getMainTable() .getColumns().keySet().iterator().next(), ptCol, null)); return pa; } /** * Create a default application based on the given dimension. * * @param pt * the partition table. * @param ds * the dataset. * @param dimension * the dimension. * @return the default application. */ public static PartitionTableApplication createDefault( final PartitionTable pt, final DataSet ds, final String dimension) { final PartitionTableApplication pa = new PartitionTableApplication( pt); if (pt.getSelectedColumnNames().size() < 1) return pa; final String ptCol = (String) pt.getSelectedColumnNames() .iterator().next(); pa.getPartitionAppliedRows().add( new PartitionAppliedRow(ptCol, (String) ((DataSetTable) ds .getTables().get(dimension)).getColumns().keySet() .iterator().next(), ptCol, null)); return pa; } /** * Details of how a partition table is broken down into a particular * row. */ public static class PartitionAppliedRow implements TransactionListener { private final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private boolean visibleModified = true; private boolean directModified = false; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { PartitionAppliedRow.this.setDirectModified(true); } }; private int compound = 1; private String partitionCol; private String rootDataSetCol; private String namePartitionCol; private Relation relation; /** * Construct a row of data from a single partition table. * * @param partitionCol * the column providing unique values. * @param rootDataSetCol * the data set column the values are applied to. This is * a root name (not including the {0}*__ prefix). * @param namePartitionCol * the column providing data to be used in the prefix. * @param relation * the relation that provides the dataset column this * refers to. */ public PartitionAppliedRow(final String partitionCol, final String rootDataSetCol, final String namePartitionCol, final Relation relation) { this.compound = 1; this.partitionCol = partitionCol; this.rootDataSetCol = rootDataSetCol; this.namePartitionCol = namePartitionCol; this.relation = relation; // Set up listening for property changes. Transaction.addTransactionListener(this); // All changes to us make us modified. this.addPropertyChangeListener(this.listener); } /** * Replicate ourselves. * * @return the replica. */ public PartitionAppliedRow replicate() { final PartitionAppliedRow row = new PartitionAppliedRow( this.partitionCol, this.rootDataSetCol, this.namePartitionCol, this.relation); row.compound = this.compound; return row; } public boolean isDirectModified() { return this.directModified; } public void setDirectModified(final boolean modified) { if (modified == this.directModified) return; final boolean oldValue = this.directModified; this.directModified = modified; this.pcs.firePropertyChange("directModified", oldValue, modified); } public boolean isVisibleModified() { return this.visibleModified; } public void setVisibleModified(final boolean modified) { // We don't care as this gets set internally. } public void transactionResetVisibleModified() { this.visibleModified = false; } public void transactionResetDirectModified() { this.directModified = false; } public void transactionStarted(final TransactionEvent evt) { // Don't really care for now. } public void transactionEnded(final TransactionEvent evt) { // Don't really care for now. } /** * Adds a property change listener. * * @param listener * the listener to add. */ public void addPropertyChangeListener( final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(listener); } /** * Adds a property change listener. * * @param property * the property to listen to. * @param listener * the listener to add. */ public void addPropertyChangeListener(final String property, final PropertyChangeListener listener) { this.pcs.addPropertyChangeListener(property, listener); } /** * @return the compound */ public int getCompound() { return this.compound; } /** * @param compound * the compound to set */ public void setCompound(final int compound) { final int oldValue = this.compound; if (oldValue == compound) return; this.compound = compound; this.pcs.firePropertyChange("compound", oldValue, compound); } /** * @return the namePartitionCol */ public String getNamePartitionCol() { return this.namePartitionCol; } /** * @return the partitionCol */ public String getPartitionCol() { return this.partitionCol; } /** * @return the rootDataSetCol */ public String getRootDataSetCol() { return this.rootDataSetCol; } /** * @return the relation */ public Relation getRelation() { return this.relation; } /** * @param namePartitionCol * the namePartitionCol to set */ public void setNamePartitionCol(final String namePartitionCol) { final String oldValue = this.namePartitionCol; if (oldValue == namePartitionCol || oldValue != null && oldValue.equals(namePartitionCol)) return; this.namePartitionCol = namePartitionCol; this.pcs.firePropertyChange("namePartitionCol", oldValue, namePartitionCol); } /** * @param partitionCol * the partitionCol to set */ public void setPartitionCol(final String partitionCol) { final String oldValue = this.partitionCol; if (oldValue == partitionCol || oldValue != null && oldValue.equals(partitionCol)) return; this.partitionCol = partitionCol; this.pcs.firePropertyChange("partitionCol", oldValue, partitionCol); } /** * @param rootDataSetCol * the rootDataSetCol to set */ public void setRootDataSetCol(final String rootDataSetCol) { final String oldValue = this.rootDataSetCol; if (oldValue == rootDataSetCol || oldValue != null && oldValue.equals(rootDataSetCol)) return; this.rootDataSetCol = rootDataSetCol; this.pcs.firePropertyChange("rootDataSetCol", oldValue, rootDataSetCol); } /** * @param relation * the relation to set */ public void setRelation(final Relation relation) { final Relation oldValue = this.relation; if (oldValue == relation || oldValue != null && oldValue.equals(relation)) return; this.relation = relation; this.pcs.firePropertyChange("relation", oldValue, relation); } public int hashCode() { return (this.namePartitionCol == null ? 1 : this.namePartitionCol.hashCode()) * (this.partitionCol == null ? 1 : this.partitionCol .hashCode()) * (this.rootDataSetCol == null ? 1 : this.rootDataSetCol.hashCode()) * (this.relation == null ? 1 : this.relation.hashCode()); } public boolean equals(final Object o) { if (!(o instanceof PartitionAppliedRow)) return false; final PartitionAppliedRow them = (PartitionAppliedRow) o; return (this.namePartitionCol == them.namePartitionCol || this.namePartitionCol != null && this.namePartitionCol.equals(them.namePartitionCol)) && (this.partitionCol == them.partitionCol || this.partitionCol != null && this.partitionCol.equals(them.partitionCol)) && (this.rootDataSetCol == them.rootDataSetCol || this.rootDataSetCol != null && this.rootDataSetCol .equals(them.rootDataSetCol)) && (this.relation == them.relation || this.relation != null && this.relation.equals(them.relation)); } } } }