/* 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.sql.SQLException; 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.TreeSet; import org.biomart.builder.exceptions.PartitionException; import org.biomart.builder.exceptions.ValidationException; import org.biomart.builder.model.DataSet.DataSetColumn; import org.biomart.builder.model.DataSet.DataSetTable; import org.biomart.builder.model.DataSet.DataSetColumn.UnrolledColumn; import org.biomart.builder.model.DataSet.DataSetColumn.WrappedColumn; import org.biomart.builder.model.Key.ForeignKey; import org.biomart.builder.model.Key.PrimaryKey; import org.biomart.builder.model.PartitionTable.PartitionTableApplication; import org.biomart.builder.model.Relation.Cardinality; import org.biomart.builder.model.Relation.UnrolledRelationDefinition; import org.biomart.common.exceptions.AssociationException; import org.biomart.common.exceptions.DataModelException; import org.biomart.common.resources.Log; import org.biomart.common.resources.Resources; 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 mart contains the set of all schemas that are providing data to this * mart. It also has zero or more datasets based around these. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.90 $, $Date: 2008-03-03 12:16:15 $, modified by * $Author: rh4 $ * @since 0.5 */ public class Mart implements TransactionListener { private static final long serialVersionUID = 1L; /** * Subclasses use this field to fire events of their own. */ protected final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private final BeanMap datasets; private final BeanMap schemas; private String outputDatabase = null; private String outputSchema = null; private String outputHost = null; private String outputPort = null; private String overrideHost = null; private String overridePort = null; private boolean directModified = false; private boolean hideMaskedDataSets = false; private boolean hideMaskedSchemas = false; /** * Constant referring to table and column name conversion. */ public static final int USE_MIXED_CASE = 0; /** * Constant referring to table and column name conversion. */ public static final int USE_UPPER_CASE = 1; /** * Constant referring to table and column name conversion. */ public static final int USE_LOWER_CASE = 2; private int nameCase = Mart.USE_MIXED_CASE; // For use in hash code and equals to prevent dups in prop change. private static int ID_SERIES = 0; private final int uniqueID = Mart.ID_SERIES++; private Collection schemaCache; private Collection datasetCache; // All changes to us make us modified. private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { Mart.this.setDirectModified(true); } }; private final PropertyChangeListener schemaCacheBuilder = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { final Collection newSchs = new HashSet(Mart.this.schemas.values()); if (!newSchs.equals(Mart.this.schemaCache)) { Mart.this.setDirectModified(true); // Identify dropped ones. final Collection dropped = new HashSet(Mart.this.schemaCache); dropped.removeAll(newSchs); // Identify new ones. newSchs.removeAll(Mart.this.schemaCache); // Drop dropped ones. for (final Iterator i = dropped.iterator(); i.hasNext();) Mart.this.schemaCache.remove(i.next()); // Add added ones. for (final Iterator i = newSchs.iterator(); i.hasNext();) { final Schema sch = (Schema) i.next(); Mart.this.schemaCache.add(sch); sch.addPropertyChangeListener("directModified", Mart.this.listener); sch.addPropertyChangeListener("hideMasked", Mart.this.listener); } } } }; private final PropertyChangeListener datasetCacheBuilder = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { final Collection newDss = new HashSet(Mart.this.datasets.values()); if (!newDss.equals(Mart.this.datasetCache)) { Mart.this.setDirectModified(true); // Identify dropped ones. final Collection dropped = new HashSet(Mart.this.datasetCache); dropped.removeAll(newDss); // Identify new ones. newDss.removeAll(Mart.this.datasetCache); // Drop dropped ones. for (final Iterator i = dropped.iterator(); i.hasNext();) { final DataSet deadDS = (DataSet) i.next(); try { deadDS.setPartitionTable(false); } catch (final PartitionException pe) { // Ignore. } // Also remove all related mods in rels and tbls. for (final Iterator j = Mart.this.schemas.values() .iterator(); j.hasNext();) { final Schema sch = (Schema) j.next(); for (final Iterator k = sch.getTables().values() .iterator(); k.hasNext();) ((Table) k.next()).dropMods(deadDS, null); for (final Iterator k = sch.getRelations().iterator(); k .hasNext();) ((Relation) k.next()).dropMods(deadDS, null); } // Remove all partition table applications. PartitionTableApplication pta = deadDS .getPartitionTableApplication(); if (pta != null) pta.getPartitionTable().removeFrom(deadDS, null); for (final Iterator j = deadDS.getTables().values() .iterator(); j.hasNext();) { final DataSetTable dsTable = (DataSetTable) j.next(); pta = dsTable.getPartitionTableApplication(); if (pta != null) pta.getPartitionTable().removeFrom(deadDS, dsTable.getName()); } // Remove from cache. Mart.this.datasetCache.remove(deadDS); } // Add added ones. for (final Iterator i = newDss.iterator(); i.hasNext();) { final DataSet ds = (DataSet) i.next(); Mart.this.datasetCache.add(ds); ds.addPropertyChangeListener("directModified", Mart.this.listener); ds.addPropertyChangeListener("hideMasked", Mart.this.listener); } } } }; /** * Construct a new, empty, mart. */ public Mart() { Log.debug("Creating new mart"); this.datasets = new BeanMap(new TreeMap()); this.schemas = new BeanMap(new TreeMap()); Transaction.addTransactionListener(this); this.addPropertyChangeListener("case", this.listener); this.addPropertyChangeListener("outputHost", this.listener); this.addPropertyChangeListener("outputPort", this.listener); this.addPropertyChangeListener("outputSchema", this.listener); this.addPropertyChangeListener("overrideHost", this.listener); this.addPropertyChangeListener("hideMaskedSchemas", this.listener); this.addPropertyChangeListener("hideMaskedDataSets", this.listener); // Listeners on schema and dataset additions to spot // and handle renames. this.schemaCache = new HashSet(); this.schemas.addPropertyChangeListener(this.schemaCacheBuilder); this.datasetCache = new HashSet(); this.datasets.addPropertyChangeListener(this.datasetCacheBuilder); } /** * Obtain the next unique ID to use for a schema. * * @return the next ID. */ public int getNextUniqueId() { int x = 0; for (final Iterator i = this.schemaCache.iterator(); i.hasNext();) x = Math.max(x, ((Schema) i.next()).getUniqueId()); return x + 1; } /** * Obtain the unique series number for this mart. * * @return the unique Id. */ public int getUniqueId() { return this.uniqueID; } public int hashCode() { return 0; // All marts go in one big bucket! } public boolean equals(final Object obj) { if (obj == this) return true; else if (obj == null) return false; else if (obj instanceof Mart) return this.uniqueID == ((Mart) obj).uniqueID; else return false; } /** * Is this mart hiding masked datasets? * * @param hideMaskedDataSets * true if it is. */ public void setHideMaskedDataSets(final boolean hideMaskedDataSets) { final boolean oldValue = this.hideMaskedDataSets; if (this.hideMaskedDataSets == hideMaskedDataSets) return; this.hideMaskedDataSets = hideMaskedDataSets; this.pcs.firePropertyChange("hideMaskedDataSets", oldValue, hideMaskedDataSets); } /** * Is this mart hiding masked datasets? * * @return true if it is. */ public boolean isHideMaskedDataSets() { return this.hideMaskedDataSets; } /** * Is this mart hiding masked schemas? * * @param hideMaskedSchemas * true if it is. */ public void setHideMaskedSchemas(final boolean hideMaskedSchemas) { final boolean oldValue = this.hideMaskedSchemas; if (this.hideMaskedSchemas == hideMaskedSchemas) return; this.hideMaskedSchemas = hideMaskedSchemas; this.pcs.firePropertyChange("hideMaskedSchemas", oldValue, hideMaskedSchemas); } /** * Is this mart hiding masked schemas? * * @return true if it is. */ public boolean isHideMaskedSchemas() { return this.hideMaskedSchemas; } 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 false; } public void setVisibleModified(final boolean modified) { // Ignore, for now. } public void transactionResetVisibleModified() { // Ignore, for now. } 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); } /** * What case to use for table and column names? Mixed is default. * * @return one of {@link #USE_LOWER_CASE}, {@link #USE_UPPER_CASE}, or * {@link #USE_MIXED_CASE}. */ public int getCase() { return this.nameCase; } /** * What case to use for table and column names? Mixed is default. * * @param nameCase * one of {@link #USE_LOWER_CASE}, {@link #USE_UPPER_CASE}, or * {@link #USE_MIXED_CASE}. */ public void setCase(final int nameCase) { Log.debug("Changing case for " + this + " to " + nameCase); final int oldValue = this.nameCase; if (this.nameCase == nameCase) return; // Make the change. this.nameCase = nameCase; this.pcs.firePropertyChange("nameCase", oldValue, nameCase); } /** * Optional, sets the default target schema this mart will output dataset * DDL to later. * * @param outputSchema * the target schema. */ public void setOutputSchema(final String outputSchema) { Log.debug("Changing outputSchema for " + this + " to " + outputSchema); final String oldValue = this.outputSchema; if (this.outputSchema == outputSchema || this.outputSchema != null && this.outputSchema.equals(outputSchema)) return; // Make the change. this.outputSchema = outputSchema; this.pcs.firePropertyChange("outputSchema", oldValue, outputSchema); } /** * Optional, gets the default target schema this mart will output dataset * DDL to later. * * @return the target schema. */ public String getOutputSchema() { return this.outputSchema; } /** * Optional, sets the default target database this mart will output dataset * DDL to later. * * @param outputDatabase * the target database. */ public void setOutputDatabase(final String outputDatabase) { Log.debug("Changing outputDatabase for " + this + " to " + outputDatabase); final String oldValue = this.outputDatabase; if (this.outputDatabase == outputDatabase || this.outputDatabase != null && this.outputDatabase.equals(outputDatabase)) return; // Make the change. this.outputDatabase = outputDatabase; this.pcs.firePropertyChange("outputDatabase", oldValue, outputDatabase); } /** * Optional, gets the default target database this mart will output dataset * DDL to later. * * @return the target schema. */ public String getOutputDatabase() { return this.outputDatabase; } /** * Optional, sets the default target host this mart will output dataset DDL * to later. * * @param outputHost * the target host. */ public void setOutputHost(final String outputHost) { Log.debug("Changing outputHost for " + this + " to " + outputHost); final String oldValue = this.outputHost; if (this.outputHost == outputHost || this.outputHost != null && this.outputHost.equals(outputHost)) return; // Make the change. this.outputHost = outputHost; this.pcs.firePropertyChange("outputHost", oldValue, outputHost); } /** * Optional, gets the default target host this mart will output dataset DDL * to later. * * @return the target host. */ public String getOutputHost() { return this.outputHost; } /** * Optional, sets the default target port this mart will output dataset DDL * to later. * * @param outputPort * the target port. */ public void setOutputPort(final String outputPort) { Log.debug("Changing outputPort for " + this + " to " + outputPort); final String oldValue = this.outputPort; if (this.outputPort == outputPort || this.outputPort != null && this.outputPort.equals(outputPort)) return; // Make the change. this.outputPort = outputPort; this.pcs.firePropertyChange("outputPort", oldValue, outputPort); } /** * Optional, gets the default target port this mart will output dataset DDL * to later. * * @return the target port. */ public String getOutputPort() { return this.outputPort; } /** * Optional, sets the default target JDBC host this mart will output dataset * DDL to later. * * @param overrideHost * the target host. */ public void setOverrideHost(final String overrideHost) { Log.debug("Changing overrideHost for " + this + " to " + overrideHost); final String oldValue = this.overrideHost; if (this.overrideHost == overrideHost || this.overrideHost != null && this.overrideHost.equals(overrideHost)) return; // Make the change. this.overrideHost = overrideHost; this.pcs.firePropertyChange("overrideHost", oldValue, overrideHost); } /** * Optional, gets the default target JDBC host this mart will output dataset * DDL to later. * * @return the target host. */ public String getOverrideHost() { return this.overrideHost; } /** * Optional, sets the default target JDBC port this mart will output dataset * DDL to later. * * @param overridePort * the target port. */ public void setOverridePort(final String overridePort) { Log.debug("Changing overridePort for " + this + " to " + overridePort); final String oldValue = this.overridePort; if (this.overridePort == overridePort || this.overridePort != null && this.overridePort.equals(overridePort)) return; // Make the change. this.overridePort = overridePort; this.pcs.firePropertyChange("overridePort", oldValue, overridePort); } /** * Optional, gets the default target JDBC port this mart will output dataset * DDL to later. * * @return the target port. */ public String getOverridePort() { return this.overridePort; } /** * Returns the set of dataset objects which this mart includes. The set may * be empty but it is never <tt>null</tt>. * * @return a set of dataset objects. Keys are names, values are datasets. */ public BeanMap getDataSets() { return this.datasets; } /** * Returns the set of partition column names which this mart includes. The * set may be empty but it is never <tt>null</tt>. * * @return a set of partition column names (as strings). */ public Collection getPartitionColumnNames() { final List colNames = new ArrayList(); for (final Iterator i = this.getPartitionTables().iterator(); i .hasNext();) { final PartitionTable pt = (PartitionTable) i.next(); for (final Iterator j = pt.getSelectedColumnNames().iterator(); j .hasNext();) { final String col = (String) j.next(); if (!col.equals(PartitionTable.DIV_COLUMN)) colNames.add(pt.getName() + "." + col); } } Collections.sort(colNames); return Collections.unmodifiableCollection(colNames); } /** * Returns the set of partition table names which this mart includes. The * set may be empty but it is never <tt>null</tt>. * * @return a set of partition table names (as strings). */ public Collection getPartitionTables() { final List tbls = new ArrayList(); for (final Iterator i = this.datasets.values().iterator(); i.hasNext();) { final DataSet ds = (DataSet) i.next(); if (ds.isPartitionTable()) tbls.add(ds.asPartitionTable()); } Collections.sort(tbls); return Collections.unmodifiableCollection(tbls); } /** * Returns the set of schema objects which this mart includes. The set may * be empty but it is never <tt>null</tt>. * * @return a set of schema objects. Keys are names, values are actual * schemas. */ public BeanMap getSchemas() { return this.schemas; } /** * Given a set of tables, produce the minimal set of datasets which include * all the specified tables. Tables can be included in the same dataset if * they are linked by 1:M relations (1:M, 1:M in a chain), or if the table * is the last in the chain and is linked to the previous table by a pair of * 1:M and M:1 relations via a third table, simulating a M:M relation. * <p> * If the chains of tables fork, then one dataset is generated for each * branch of the fork. * <p> * Every suggested dataset is synchronised before being returned. * <p> * Datasets will be named after their central tables. If a dataset with that * name already exists, a '_' and sequence number will be appended to make * the new dataset name unique. * <p> * See also * {@link #continueSubclassing(Collection, Collection, DataSet, Table)}. * * @param includeTables * the tables that must appear in the final set of datasets. * @return the collection of datasets generated. * @throws SQLException * if there is any problem talking to the source database whilst * generating the dataset. * @throws DataModelException * if synchronisation fails. */ public Collection suggestDataSets(final Collection includeTables) throws SQLException, DataModelException { Log.debug("Suggesting datasets for " + includeTables); // The root tables are all those which do not have a M:1 relation // to another one of the initial set of tables. This means that // extra datasets will be created for each table at the end of // 1:M:1 relation, so that any further tables past it will still // be included. Log.debug("Finding root tables"); final Collection rootTables = new HashSet(includeTables); for (final Iterator i = includeTables.iterator(); i.hasNext();) { final Table candidate = (Table) i.next(); for (final Iterator j = candidate.getRelations().iterator(); j .hasNext();) { final Relation rel = (Relation) j.next(); if (rel.getStatus().equals(ComponentStatus.INFERRED_INCORRECT)) continue; if (!rel.isOneToMany()) continue; if (!rel.getManyKey().getTable().equals(candidate)) continue; if (includeTables.contains(rel.getOneKey().getTable())) rootTables.remove(candidate); } } // We construct one dataset per root table. final Set suggestedDataSets = new TreeSet(); for (final Iterator i = rootTables.iterator(); i.hasNext();) { final Table rootTable = (Table) i.next(); Log.debug("Constructing dataset for root table " + rootTable); final DataSet dataset; try { dataset = new DataSet(this, rootTable, rootTable.getName()); } catch (final ValidationException e) { // Skip this one. continue; } this.datasets.put(dataset.getOriginalName(), dataset); // Process it. final Collection tablesIncluded = new HashSet(); tablesIncluded.add(rootTable); Log.debug("Attempting to find subclass datasets"); suggestedDataSets.addAll(this.continueSubclassing(includeTables, tablesIncluded, dataset, rootTable)); } // Synchronise them all. Log.debug("Synchronising constructed datasets"); for (final Iterator i = suggestedDataSets.iterator(); i.hasNext();) ((DataSet) i.next()).synchronise(); // Do any of the resulting datasets contain all the tables // exactly with subclass relations between each? // If so, just use that one dataset and forget the rest. Log.debug("Finding perfect candidate"); DataSet perfectDS = null; for (final Iterator i = suggestedDataSets.iterator(); i.hasNext() && perfectDS == null;) { final DataSet candidate = (DataSet) i.next(); // A candidate is a perfect match if the set of tables // covered by the subclass relations is the same as the // original set of tables requested. final Collection scTables = new HashSet(); for (final Iterator j = candidate.getIncludedRelations().iterator(); j .hasNext();) { final Relation r = (Relation) j.next(); if (!r.isSubclassRelation(candidate)) continue; scTables.add(r.getFirstKey().getTable()); scTables.add(r.getSecondKey().getTable()); } // Finally perform the check to see if we have them all. if (scTables.containsAll(includeTables)) perfectDS = candidate; } if (perfectDS != null) { Log.debug("Perfect candidate found - dropping others"); // Drop the others. for (final Iterator i = suggestedDataSets.iterator(); i.hasNext();) { final DataSet candidate = (DataSet) i.next(); if (!candidate.equals(perfectDS)) { this.datasets.remove(candidate.getOriginalName()); i.remove(); } } // Rename it to lose any extension it may have gained. perfectDS.setName(perfectDS.getCentralTable().getName()); } else Log.debug("No perfect candidate found - retaining all"); // Return the final set of suggested datasets. return suggestedDataSets; } /** * This internal method takes a bunch of tables that the user would like to * see as subclass or main tables in a single dataset, and attempts to find * a subclass path between them. For each subclass path it can build, it * produces one dataset based on that path. Each path contains as many * tables as possible. The paths do not overlap. If there is a choice, the * one chosen is arbitrary. * * @param includeTables * the tables we want to include as main or subclass tables. * @param tablesIncluded * the tables we have managed to include in a path so far. * @param dataset * the dataset we started out from which contains just the main * table on its own with no subclassing. * @param table * the real table we are looking at to see if there is a subclass * path between any of the include tables and any of the existing * subclassed or main tables via this real table. * @return the datasets we have created - one per subclass path, or if there * were none, then a singleton collection containing the dataset * originally passed in. */ private Collection continueSubclassing(final Collection includeTables, final Collection tablesIncluded, final DataSet dataset, final Table table) { // Check table has a primary key. final Key pk = table.getPrimaryKey(); // Make a unique set to hold all the resulting datasets. It // is initially empty. final Collection suggestedDataSets = new HashSet(); // Make a set to contain relations to subclass. final Collection subclassedRelations = new HashSet(); // Make a map to hold tables included for each relation. final Map relationTablesIncluded = new HashMap(); // Make a list to hold all tables included at this level. final Collection localTablesIncluded = new HashSet(tablesIncluded); // Find all 1:M relations starting from the given table that point // to another interesting table. if (pk != null) for (final Iterator i = pk.getRelations().iterator(); i.hasNext();) { final Relation r = (Relation) i.next(); if (!r.isOneToMany()) continue; else if (r.getStatus().equals( ComponentStatus.INFERRED_INCORRECT)) continue; // For each relation, if it points to another included // table via 1:M we should subclass the relation. final Table target = r.getManyKey().getTable(); if (includeTables.contains(target) && !localTablesIncluded.contains(target)) { subclassedRelations.add(r); final Collection newRelationTablesIncluded = new HashSet( tablesIncluded); relationTablesIncluded.put(r, newRelationTablesIncluded); newRelationTablesIncluded.add(target); localTablesIncluded.add(target); } } // Find all 1:M:1 relations starting from the given table that point // to another interesting table. if (pk != null) for (final Iterator i = pk.getRelations().iterator(); i.hasNext();) { final Relation firstRel = (Relation) i.next(); if (!firstRel.isOneToMany()) continue; else if (firstRel.getStatus().equals( ComponentStatus.INFERRED_INCORRECT)) continue; final Table intermediate = firstRel.getManyKey().getTable(); for (final Iterator j = intermediate.getForeignKeys() .iterator(); j.hasNext();) { final Key fk = (Key) j.next(); if (fk.getStatus().equals( ComponentStatus.INFERRED_INCORRECT)) continue; for (final Iterator k = fk.getRelations().iterator(); k .hasNext();) { final Relation secondRel = (Relation) k.next(); if (secondRel.equals(firstRel)) continue; else if (!secondRel.isOneToMany()) continue; else if (secondRel.getStatus().equals( ComponentStatus.INFERRED_INCORRECT)) continue; // For each relation, if it points to another included // table via M:1 we should subclass the relation. final Table target = secondRel.getOneKey().getTable(); if (includeTables.contains(target) && !localTablesIncluded.contains(target)) { subclassedRelations.add(firstRel); final Collection newRelationTablesIncluded = new HashSet( tablesIncluded); relationTablesIncluded.put(firstRel, newRelationTablesIncluded); newRelationTablesIncluded.add(target); localTablesIncluded.add(target); } } } } // No subclassing? Return a singleton. if (subclassedRelations.isEmpty()) return Collections.singleton(dataset); // Iterate through the relations we found and recurse. // If not the last one, we copy the original dataset and // work on the copy, otherwise we work on the original. for (final Iterator i = subclassedRelations.iterator(); i.hasNext();) { final Relation r = (Relation) i.next(); DataSet suggestedDataSet = dataset; try { if (i.hasNext()) { suggestedDataSet = new DataSet(this, dataset .getCentralTable(), dataset.getCentralTable() .getName()); this.datasets.put(suggestedDataSet.getOriginalName(), suggestedDataSet); // Copy subclassed relations from existing dataset. for (final Iterator j = dataset.getIncludedRelations() .iterator(); j.hasNext();) ((Relation) j.next()).setSubclassRelation( suggestedDataSet, true); } r.setSubclassRelation(suggestedDataSet, true); } catch (final ValidationException e) { // Not valid? OK, ignore this one. continue; } suggestedDataSets.addAll(this.continueSubclassing(includeTables, (Collection) relationTablesIncluded.get(r), suggestedDataSet, r.getManyKey().getTable())); } // Return the resulting datasets. return suggestedDataSets; } /** * Given a dataset and a set of columns from one table upon which a table of * that dataset is based, find all other tables which have similar columns, * and create a new dataset for each one. * <p> * This method will not create datasets around tables which have already * been used as the underlying table in any dataset table in the existing * dataset. Neither will it create a dataset around the table from which the * original columns came. * <p> * There may be no datasets resulting from this, if the columns do not * appear elsewhere. * <p> * Datasets are synchronised before being returned. * <p> * Datasets will be named after their central tables. If a dataset with that * name already exists, a '_' and sequence number will be appended to make * the new dataset name unique. * * @param dataset * the dataset the columns were selected from. * @param columns * the columns to search across. * @return the resulting set of datasets. * @throws SQLException * if there is any problem talking to the source database whilst * generating the dataset. * @throws DataModelException * if synchronisation fails. */ public Collection suggestInvisibleDataSets(final DataSet dataset, final Collection columns) throws SQLException, DataModelException { Log.debug("Suggesting invisible datasets for " + dataset + " columns " + columns); final Collection invisibleDataSets = new HashSet(); final Table sourceTable = ((Column) columns.iterator().next()) .getTable(); // Find all tables which mention the columns specified. Log.debug("Finding candidate tables"); final Collection candidates = new HashSet(); for (final Iterator i = this.schemas.values().iterator(); i.hasNext();) for (final Iterator j = ((Schema) i.next()).getTables().values() .iterator(); j.hasNext();) { final Table table = (Table) j.next(); int matchingColCount = 0; for (final Iterator k = columns.iterator(); k.hasNext();) { final Column col = (Column) k.next(); if (table.getColumns().containsKey(col.getName()) || table .getColumns() .containsKey( col.getName() + Resources .get("foreignKeySuffix"))) matchingColCount++; } if (matchingColCount == columns.size()) candidates.add(table); } // Remove from the found tables all those which are already // used, and the one from which the original columns came. Log.debug("Removing candidates that are already used in this dataset"); candidates.remove(sourceTable); for (final Iterator i = dataset.getTables().values().iterator(); i .hasNext();) candidates.remove(((DataSetTable) i.next()).getFocusTable()); // Generate the dataset for each. Log.debug("Creating datasets for remaining candidates"); for (final Iterator i = candidates.iterator(); i.hasNext();) { final Table table = (Table) i.next(); final DataSet inv; try { inv = new DataSet(this, table, table.getName()); } catch (final ValidationException e) { // Skip this one. continue; } this.datasets.put(inv.getOriginalName(), inv); invisibleDataSets.add(inv); } // Synchronise them all and make them all invisible. Log.debug("Synchronising suggested datasets"); for (final Iterator i = invisibleDataSets.iterator(); i.hasNext();) { final DataSet ds = (DataSet) i.next(); ds.setInvisible(true); ds.synchronise(); } // Return the results. return invisibleDataSets; } /** * Given a pair of tables, construct an unrolled dataset with all defaults * in place for a useful ontology structure. * <p> * Datasets are synchronised before being returned. It will always return * exactly one dataset. * * @param nTable * the vocabulary definition table. * @param nIDCol * the unique ID column for each vocab term. * @param nNamingCol * the human readable version of each vocab term. * @param nrTable * the relationship table. * @param nrParentIDCol * the ID of the parent term in the relationship. * @param nrChildIDCol * the ID of the child term in the relationship. * @param reversed * <tt>true</tt> if the unrolling goes in the opposite sense of * the data in the table, e.g. the table goes parent to child but * we want to unroll child to parent. * @return the resulting dataset. * @throws SQLException * if there is any problem talking to the source database whilst * generating the dataset. * @throws AssociationException * if some logic problem occurs. * @throws ValidationException * if some logic problem occurs. * @throws DataModelException * if synchronisation fails. */ public DataSet suggestUnrolledDataSets(final Table nTable, final Column nIDCol, final Column nNamingCol, final Table nrTable, final Column nrParentIDCol, final Column nrChildIDCol, final boolean reversed) throws SQLException, DataModelException, AssociationException, ValidationException { // Create PK on nTable.nIDCol (or reuse). PrimaryKey pk = new PrimaryKey(new Column[] { nIDCol }); nTable.setPrimaryKey(pk); pk = nTable.getPrimaryKey(); pk.setStatus(ComponentStatus.HANDMADE); pk.getTable().setMasked(false); // Create FKs on nrTable.nrParent/ChildIDCol (or reuse). ForeignKey parentFk = new ForeignKey(new Column[] { nrParentIDCol }); ForeignKey childFk = new ForeignKey(new Column[] { nrChildIDCol }); if (!nrTable.getForeignKeys().add(parentFk)) { // Reuse. ForeignKey reuse = null; for (final Iterator i = nrTable.getForeignKeys().iterator(); i .hasNext() && reuse == null;) { final ForeignKey cand = (ForeignKey) i.next(); if (cand.equals(parentFk)) reuse = cand; } parentFk = reuse; } parentFk.setStatus(ComponentStatus.HANDMADE); parentFk.getTable().setMasked(false); if (!nrTable.getForeignKeys().add(childFk)) { // Reuse. ForeignKey reuse = null; for (final Iterator i = nrTable.getForeignKeys().iterator(); i .hasNext() && reuse == null;) { final ForeignKey cand = (ForeignKey) i.next(); if (cand.equals(childFk)) reuse = cand; } childFk = reuse; } childFk.setStatus(ComponentStatus.HANDMADE); childFk.getTable().setMasked(false); // Create or reuse relations between PK and each FK. Relation parentRel = null; try { parentRel = new Relation(pk, parentFk, Cardinality.MANY_A); pk.getRelations().add(parentRel); parentFk.getRelations().add(parentRel); } catch (final AssociationException e) { // Reuse. Relation reuse = null; for (final Iterator i = pk.getRelations().iterator(); i.hasNext() && reuse == null;) { final Relation cand = (Relation) i.next(); if (cand.getFirstKey().equals(parentFk) && cand.getSecondKey().equals(pk) || cand.getFirstKey().equals(pk) && cand.getSecondKey().equals(parentFk)) reuse = cand; } parentRel = reuse; } finally { parentRel.setStatus(ComponentStatus.HANDMADE); } Relation childRel = null; try { childRel = new Relation(pk, childFk, Cardinality.MANY_A); pk.getRelations().add(childRel); childFk.getRelations().add(childRel); } catch (final AssociationException e) { // Reuse. Relation reuse = null; for (final Iterator i = pk.getRelations().iterator(); i.hasNext() && reuse == null;) { final Relation cand = (Relation) i.next(); if (cand.getFirstKey().equals(childFk) && cand.getSecondKey().equals(pk) || cand.getFirstKey().equals(pk) && cand.getSecondKey().equals(childFk)) reuse = cand; } childRel = reuse; } finally { childRel.setStatus(ComponentStatus.HANDMADE); } // Now swap parent and child if reversed. if (reversed) { final Relation otherRel = parentRel; parentRel = childRel; childRel = otherRel; } // Don't make dataset itself green. if (Transaction.getCurrentTransaction() != null) Transaction.getCurrentTransaction().setAllowVisModChange(false); // Create a simple dataset based on the selected table. final DataSet ds = new DataSet(this, nTable, nTable.getName()); ds.synchronise(); // Must do now in order to locate dimensions. final DataSetTable mainTable = ds.getMainTable(); // Locate the merge dimension based on parent rel and merge it. // Locate the unroll dimension based on child rel and unroll it for (final Iterator i = ds.getTables().values().iterator(); i.hasNext();) { final DataSetTable dst = (DataSetTable) i.next(); if (dst.getFocusRelation() != null) if (dst.getFocusRelation().equals(parentRel)) dst.getFocusRelation().setMergeRelation(ds, true); else if (dst.getFocusRelation().equals(childRel)) dst.getFocusRelation() .setUnrolledRelation( ds, new UnrolledRelationDefinition(nNamingCol, reversed)); else dst.setDimensionMasked(true); } ds.synchronise(); // Must do again to update dimensions. // Locate all unimportant relations and mask them. // Force the child rel. for (final Iterator i = mainTable.getIncludedRelations().iterator(); i .hasNext();) { final Relation cand = (Relation) i.next(); if (cand.equals(parentRel)) continue; else if (cand.equals(childRel)) cand.setForceRelation(ds, mainTable.getName(), true); else cand.setMaskRelation(ds, mainTable.getName(), true); } ds.synchronise(); // Must do again to update relations. // Auto-mask all DS cols on main table which are not wrappers of // nIDCol or nNamingCol. for (final Iterator i = mainTable.getColumns().values().iterator(); i .hasNext();) { final DataSetColumn dsCol = (DataSetColumn) i.next(); if (dsCol instanceof UnrolledColumn) continue; else if (dsCol instanceof WrappedColumn) { final WrappedColumn wcol = (WrappedColumn) dsCol; if (wcol.getWrappedColumn().equals(nIDCol)) continue; else if (wcol.getWrappedColumn().equals(nNamingCol)) continue; } dsCol.setColumnMasked(true); } // Locate all unimportant dimensions and mask them. for (final Iterator i = ds.getTables().values().iterator(); i.hasNext();) { final DataSetTable dst = (DataSetTable) i.next(); if (dst.getFocusRelation() != null && !dst.getFocusRelation().equals(parentRel) && !dst.getFocusRelation().equals(childRel)) dst.setDimensionMasked(true); } // All done! return ds; } }