/* * Table.java * Created on 23 March 2006, 14:34 */ /* 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.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import org.biomart.builder.model.Key.PrimaryKey; import org.biomart.common.resources.Log; import org.biomart.common.resources.Resources; import org.biomart.common.utils.BeanCollection; import org.biomart.common.utils.BeanMap; import org.biomart.common.utils.BeanSet; 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 table class provides the basic idea of what constitutes a database table * or an XML document entity. It has an optional primary key, zero or more * foreign keys, and one or more columns. * <p> * The {@link Table} class is provided as a template from which to build up more * complex implementations. It is able to keep track of keys and columns but it * does not provide any methods that process or analyse these. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.56 $, $Date: 2008-02-25 12:28:26 $, modified by * $Author: rh4 $ * @since 0.5 */ public class Table implements Comparable, TransactionListener { /** * Subclasses use this field to fire events of their own. */ protected final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private static final long serialVersionUID = 1L; private int uniqueId; private final BeanMap columns; private final BeanCollection foreignKeys; private final String name; private final BeanCollection schemaPartitions = new BeanSet(new HashSet()); private PrimaryKey primaryKey; private final Schema schema; private boolean masked = false; private final BeanCollection keyCache; private final BeanCollection relationCache; private final Collection columnCache; private boolean directModified = false; private final Map mods = new HashMap(); private static final String DATASET_WIDE = "__DATASET_WIDE__"; private final PropertyChangeListener relationCacheBuilder = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { Table.this.recalculateCaches(); } }; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { Table.this.setDirectModified(true); } }; /** * Make a new table in the given schema. It won't add itself, so you'll need * to do that separately. * * @param schema * the schema this table should belong to. * @param name * the name of the table. It will make this unique if there is a * clash with some other table already in the same schema. */ public Table(final Schema schema, String name) { Log.debug("Creating table " + name + " in " + schema); this.schema = schema; this.uniqueId = this.schema.getNextUniqueId(); this.columns = new BeanMap(new HashMap()); this.foreignKeys = new BeanCollection(new HashSet()); // Make the name unique. final String baseName = name; for (int i = 1; schema.getTables().containsKey(name); name = baseName + "_" + i++) ; Log.debug("Unique name is " + name); this.name = name; Transaction.addTransactionListener(this); // Listen to own PK and FKs and update key+relation caches. this.keyCache = new BeanSet(new HashSet()); this.relationCache = new BeanSet(new HashSet()); this.columnCache = new HashSet(); this.addPropertyChangeListener("primaryKey", this.relationCacheBuilder); this.getForeignKeys().addPropertyChangeListener( this.relationCacheBuilder); this.getColumns().addPropertyChangeListener(this.relationCacheBuilder); // All changes to us make us modified. this.addPropertyChangeListener("masked", this.listener); this.addPropertyChangeListener("restrictTable", this.listener); this.addPropertyChangeListener("bigTable", this.listener); } /** * Change the unique ID for this table. * * @param uniqueId * the new one to use. */ public void setUniqueId(final int uniqueId) { this.uniqueId = uniqueId; } /** * Get the unique ID for this table. * * @return the unique ID. */ public int getUniqueId() { return this.uniqueId; } /** * 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); } /** * Does this exist for the given schema prefix? * * @param schemaPrefix * the prefix. * @return <tt>true</tt> if it does. */ public boolean existsForPartition(final String schemaPrefix) { return schemaPrefix == null || this.getSchemaPartitions().isEmpty() || this.getSchemaPartitions().contains(schemaPrefix); } 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() { // Compute this from all rels and cols - if any are vis // modified then we are too. for (final Iterator i = this.getRelations().iterator(); i.hasNext();) if (((Relation) i.next()).isVisibleModified()) return true; for (final Iterator i = this.getColumns().values().iterator(); i .hasNext();) if (((Column) i.next()).isVisibleModified()) return true; return false; } public void setVisibleModified(final boolean modified) { // We compute this on the fly so cannot set it. } public void transactionResetVisibleModified() { // We compute this on the fly so cannot set it. } 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. } /** * Drop modifications for the given dataset and optional table. * * @param dataset * dataset * @param tableKey * table key - <tt>null</tt> for all tables. */ public void dropMods(final DataSet dataset, final String tableKey) { // Drop all related mods. if (tableKey == null) this.mods.remove(dataset); else if (this.mods.containsKey(dataset)) ((Map) this.mods.get(dataset)).remove(tableKey); } /** * This contains the set of modifications to this schema that apply to a * particular dataset and table (null table means all tables in dataset). * * @param dataset * the dataset to lookup. * @param tableKey * the table to lookup. * @return the set of tables that the property currently applies to. This * set can be added to or removed from accordingly. The keys of the * map are names, the values are optional subsidiary objects. */ public Map getMods(final DataSet dataset, String tableKey) { if (tableKey == null) tableKey = Table.DATASET_WIDE; if (!this.mods.containsKey(dataset)) this.mods.put(dataset, new HashMap()); final Map dsMap = (Map) this.mods.get(dataset); if (!dsMap.containsKey(tableKey)) dsMap.put(tableKey.intern(), new HashMap()); return (Map) dsMap.get(tableKey); } private synchronized void recalculateCaches() { final Collection newCols = new HashSet(this.getColumns().values()); if (!newCols.equals(this.columnCache)) { this.setDirectModified(true); // Identify dropped ones. final Collection dropped = new HashSet(this.columnCache); dropped.removeAll(newCols); // Identify new ones. newCols.removeAll(this.columnCache); // Drop dropped ones. for (final Iterator i = dropped.iterator(); i.hasNext();) this.columnCache.remove(i.next()); // Add added ones. for (final Iterator i = newCols.iterator(); i.hasNext();) { final Column column = (Column) i.next(); column.addPropertyChangeListener("directModified", this.listener); } this.columnCache.clear(); this.columnCache.addAll(this.getColumns().values()); } final Collection newKeys = new HashSet(); if (this.primaryKey != null) newKeys.add(this.primaryKey); newKeys.addAll(this.foreignKeys); if (!newKeys.equals(this.keyCache)) { this.setDirectModified(true); // Identify dropped ones. final Collection dropped = new HashSet(this.keyCache); dropped.removeAll(newKeys); // Identify new ones. newKeys.removeAll(this.keyCache); // Drop dropped ones. for (final Iterator i = dropped.iterator(); i.hasNext();) this.keyCache.remove(i.next()); // Add added ones. for (final Iterator i = newKeys.iterator(); i.hasNext();) { final Key key = (Key) i.next(); key.getRelations().addPropertyChangeListener( this.relationCacheBuilder); key.addPropertyChangeListener("directModified", this.relationCacheBuilder); key.addPropertyChangeListener("directModified", this.listener); } this.keyCache.clear(); if (this.primaryKey != null) this.keyCache.add(this.primaryKey); this.keyCache.addAll(this.foreignKeys); } final Collection newRels = new HashSet(); for (final Iterator i = this.keyCache.iterator(); i.hasNext();) { final Key key = (Key) i.next(); newRels.addAll(key.getRelations()); } if (!newRels.equals(this.relationCache)) { this.setDirectModified(true); this.relationCache.clear(); this.relationCache.addAll(newRels); } } /** * Obtain all keys on this table. * * @return the unmodifiable collection of keys. */ public BeanCollection getKeys() { return this.keyCache; } /** * Obtain all relations on this table. * * @return the unmodifiable collection of relations. */ public BeanCollection getRelations() { return this.relationCache; } /** * Returns a set of the columns of this table. It may be empty, indicating * that the table has no columns, however this is highly unlikely! It will * never return <tt>null</tt>. * * @return the set of columns for this table. */ public BeanMap getColumns() { return this.columns; } /** * Returns a set of the foreign keys of this table. It may be empty, * indicating that the table has no foreign keys. It will never return * <tt>null</tt>. * * @return the set of foreign keys for this table. */ public BeanCollection getForeignKeys() { return this.foreignKeys; } /** * Returns the name of this table. * * @return the name of this table. */ public String getName() { return this.name; } /** * Retrieve the set of schema partition names this column applies to. May be * empty, in which case it applies to the default schema only. * * @return the set of schema partition names. */ public BeanCollection getSchemaPartitions() { return this.schemaPartitions; } /** * Checks whether this table is masked or not. * * @return <tt>true</tt> if it is, <tt>false</tt> if it isn't. */ public boolean isMasked() { return this.masked; } /** * Sets whether this table is masked or not. * * @param masked * <tt>true</tt> if it is, <tt>false</tt> if it isn't. */ public void setMasked(final boolean masked) { Log.debug("Setting masked table on " + this + " to " + masked); final boolean oldValue = this.masked; if (this.masked == masked) return; this.masked = masked; this.pcs.firePropertyChange("masked", oldValue, masked); } /** * Returns a reference to the primary key of this table. It may be * <tt>null</tt>, indicating that the table has no primary key. * * @return the primary key of this table. */ public PrimaryKey getPrimaryKey() { return this.primaryKey; } /** * Sets the primary key of this table. It may be <tt>null</tt>, * indicating that the table has no primary key. * * @param pk * the primary key of this table. */ public void setPrimaryKey(final PrimaryKey pk) { Log.debug("Changing PK on table " + this + " to " + pk); final PrimaryKey oldValue = this.primaryKey; if (this.primaryKey == pk || this.primaryKey != null && this.primaryKey.equals(pk)) return; this.primaryKey = pk; this.pcs.firePropertyChange("primaryKey", oldValue, pk); } /** * Returns the schema for this table. * * @return the schema for this table. */ public Schema getSchema() { return this.schema; } /** * Is this table restricted? * * @param dataset * the dataset to check for. * @param tableKey * the table to check for. * @return the def to use if it is, null otherwise. */ public RestrictedTableDefinition getRestrictTable(final DataSet dataset, final String tableKey) { return (RestrictedTableDefinition) this.getMods(dataset, tableKey).get( "restrictTable"); } /** * Restrict this table. * * @param dataset * the dataset to set for. * @param tableKey * the dataset table to set for. * @param def * the definition to set - if null, it undoes it. */ public void setRestrictTable(final DataSet dataset, final String tableKey, final RestrictedTableDefinition def) { final RestrictedTableDefinition oldValue = this.getRestrictTable( dataset, tableKey); if (def == oldValue || oldValue != null && oldValue.equals(def)) return; if (def != null) { this.getMods(dataset, tableKey).put("restrictTable", def); def.addPropertyChangeListener("directModified", this.listener); this.pcs.firePropertyChange("restrictTable", null, tableKey); } else { this.getMods(dataset, tableKey).remove("restrictTable"); this.pcs.firePropertyChange("restrictTable", tableKey, null); } } /** * Is this table big? * * @param dataset * the dataset to check for. * @return the big-ness to use if it is, 0 otherwise. */ public int getBigTable(final DataSet dataset) { final Integer val = (Integer) this.getMods(dataset, null).get( "bigTable"); return val == null ? 0 : val.intValue(); } /** * Is this table big? * * @param dataset * the dataset to check for. * @param tableKey * the table to check for. * @return the big-ness to use if it is, 0 otherwise. */ public int getBigTable(final DataSet dataset, final String tableKey) { Integer val = (Integer) this.getMods(dataset, tableKey).get("bigTable"); if (val == null) return this.getBigTable(dataset); else return val.intValue(); } /** * Big-up this table. * * @param dataset * the dataset to set for. * @param bigness * the bigness to set - if 0, it undoes it. */ public void setBigTable(final DataSet dataset, final int bigness) { final int oldValue = this.getBigTable(dataset); if (bigness == oldValue) return; if (bigness > 0) { this.getMods(dataset, null).put("bigTable", new Integer(bigness)); this.pcs.firePropertyChange("bigTable", null, dataset); } else { this.getMods(dataset, null).remove("bigTable"); this.pcs.firePropertyChange("bigTable", dataset, null); } } /** * Big-up this table. * * @param dataset * the dataset to set for. * @param tableKey * the dataset table to set for. * @param bigness * the bigness to set - if 0, it undoes it. */ public void setBigTable(final DataSet dataset, final String tableKey, final int bigness) { final int oldValue = this.getBigTable(dataset, tableKey); if (bigness == oldValue) return; if (bigness > 0) { this.getMods(dataset, tableKey).put("bigTable", new Integer(bigness)); this.pcs.firePropertyChange("bigTable", null, tableKey); } else { this.getMods(dataset, tableKey).remove("bigTable"); this.pcs.firePropertyChange("bigTable", tableKey, null); } } /** * Is this table a transform start? * * @param dataset * the dataset to check for. * @param tableKey * the table to check for. * @return true if it is, false otherwise. */ public boolean isTransformStart(final DataSet dataset, final String tableKey) { return this.getMods(dataset, tableKey).containsKey("transformStart"); } /** * Transform-start this table. * * @param dataset * the dataset to set for. * @param tableKey * the dataset table to set for. * @param doIt * if false, it undoes it. */ public void setTransformStart(final DataSet dataset, final String tableKey, final boolean doIt) { final boolean oldValue = this.isTransformStart(dataset, tableKey); if (doIt == oldValue) return; if (doIt) { this.getMods(dataset, tableKey).put("transformStart", null); this.pcs.firePropertyChange("transformStart", null, tableKey); } else { this.getMods(dataset, tableKey).remove("transformStart"); this.pcs.firePropertyChange("transformStart", tableKey, null); } } public int compareTo(final Object o) throws ClassCastException { final Table t = (Table) o; return (this.schema.getMart().getUniqueId() + "_" + this.toString()) .compareTo(t.schema.getMart().getUniqueId() + "_" + t.toString()); } public boolean equals(final Object o) { if (o == this) return true; else if (o == null) return false; else if (o instanceof Table) { final Table t = (Table) o; return (t.schema.getMart().getUniqueId() + "_" + t.toString()) .equals(this.schema.getMart().getUniqueId() + "_" + this.toString()); } else return false; } public int hashCode() { return this.name.hashCode(); } public String toString() { return "(" + this.schema + ") " + this.name; } /** * Defines the restriction on a table, ie. a where-clause. */ public static class RestrictedTableDefinition implements TransactionListener { private static final long serialVersionUID = 1L; private BeanMap aliases; private String expr; private boolean directModified = false; private final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport( this); private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent e) { RestrictedTableDefinition.this.setDirectModified(true); } }; /** * This constructor gives the restriction an initial expression and a * set of aliases. The expression may not be empty, and neither can the * alias map. * * @param expr * the expression to define for this restriction. * @param aliases * the aliases to use for columns. */ public RestrictedTableDefinition(final String expr, final Map aliases) { // Test for good arguments. if (expr == null || expr.trim().length() == 0) throw new IllegalArgumentException(Resources .get("tblRestrictMissingExpression")); if (aliases == null || aliases.isEmpty()) throw new IllegalArgumentException(Resources .get("tblRestrictMissingAliases")); // Remember the settings. this.aliases = new BeanMap(new HashMap(aliases)); this.expr = expr; Transaction.addTransactionListener(this); this.addPropertyChangeListener(this.listener); this.aliases.addPropertyChangeListener(this.listener); } /** * Replicate ourselves. * * @return the copy. */ public RestrictedTableDefinition replicate() { return new RestrictedTableDefinition(this.expr, this.aliases); } /** * 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); } 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. } /** * Retrieves the map used for setting up aliases. * * @return the aliases map. Keys must be {@link Column} instances, and * values are aliases used in the expression. */ public BeanMap getAliases() { return this.aliases; } /** * Returns the expression, <i>without</i> substitution. This value is * RDBMS-specific. * * @return the unsubstituted expression. */ public String getExpression() { return this.expr; } /** * Returns the expression, <i>with</i> substitution. This value is * RDBMS-specific. * * @param schemaPrefix * the value to substitute for ':schemaPrefix'. * @param tablePrefix * the prefix to use for the table in the expression. * @return the substituted expression. */ public String getSubstitutedExpression(final String schemaPrefix, final String tablePrefix) { Log.debug("Calculating restricted table expression"); String sub = this.expr; for (final Iterator i = this.aliases.entrySet().iterator(); i .hasNext();) { final Map.Entry entry = (Map.Entry) i.next(); final Column col = (Column) entry.getKey(); final String alias = ":" + (String) entry.getValue(); sub = sub.replaceAll(alias, tablePrefix + "." + col.getName()); } sub = sub.replaceAll(":" + Resources.get("schemaPrefix"), schemaPrefix == null ? "null" : schemaPrefix); Log.debug("Expression is: " + sub); return sub; } /** * The actual expression. The values from the alias maps will be used to * refer to various columns. This value is RDBMS-specific. * * @param expr * the actual expression to use. */ public void setExpression(final String expr) { if (expr == this.expr || expr.equals(this.expr)) return; final String oldValue = this.expr; this.expr = expr; this.pcs.firePropertyChange("expression", oldValue, expr); } } }