/*
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.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.biomart.builder.model.DataSet.DataSetColumn;
import org.biomart.common.resources.Log;
import org.biomart.common.resources.Resources;
import org.biomart.common.utils.BeanCollection;
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 key class is core to the way tables get associated. They are involved in
* relations which link tables together in various ways, and provide information
* about which columns at each end correspond.
* <p>
* The {@link Key} implementation provides the basis for the other types of
* keys, eg. keeping track of relations etc.
* <p>
* Unless otherwise specified, all keys are created with a default status of
* {@link ComponentStatus#INFERRED}.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.43 $, $Date: 2007-11-02 16:22:37 $, modified by
* $Author: rh4 $
* @since 0.5
*/
public abstract class Key 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 Column[] columns;
private final BeanCollection relations;
private ComponentStatus status;
private boolean directModified = false;
private Collection relationCache;
private final PropertyChangeListener listener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
Key.this.setDirectModified(true);
}
};
private final PropertyChangeListener dropListener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
if (!Key.this.getTable().getSchema().getTables().containsValue(
Key.this.getTable())) {
final List relations = new ArrayList(Key.this.getRelations());
for (final Iterator i = relations.iterator(); i.hasNext();) {
final Relation rel = (Relation) i.next();
rel.getFirstKey().getRelations().remove(rel);
rel.getSecondKey().getRelations().remove(rel);
}
}
}
};
private final PropertyChangeListener relationCacheBuilder = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
Key.this.setDirectModified(true);
// Mass change.
final Collection addedRels = new HashSet(Key.this.relations);
addedRels.removeAll(Key.this.relationCache);
for (final Iterator i = addedRels.iterator(); i.hasNext();) {
final Relation rel = (Relation) i.next();
rel.addPropertyChangeListener("directModified",
Key.this.listener);
}
Key.this.relationCache.clear();
Key.this.relationCache.addAll(Key.this.relations);
}
};
/**
* The constructor constructs a key over a set of columns. It doesn't check
* to make sure they all come from the same table, nor does it check to see
* if they are in a sensible order. The order they are specified in here is
* the order in which the key will refer to them in future. The key will
* have a status of {@link ComponentStatus#INFERRED}.
*
* @param columns
* the set of columns to form the key over.
*/
public Key(final Column[] columns) {
Log.debug("Creating key over " + columns);
this.status = ComponentStatus.INFERRED;
this.relations = new BeanSet(new HashSet());
this.setColumns(columns);
Transaction.addTransactionListener(this);
// All changes to us make us modified.
this.addPropertyChangeListener(this.listener);
// Check to see if our table goes AWOL.
this.getTable().getSchema().getTables().addPropertyChangeListener(
this.dropListener);
// Changes on relations.
this.relationCache = new HashSet();
this.relations.addPropertyChangeListener(this.relationCacheBuilder);
}
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.
}
public void transactionResetVisibleModified() {
this.setVisibleModified(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);
}
/**
* The constructor constructs a key over a single column. Otherwise, it is
* identical to the multi-column constructor.
*
* @param column
* the column to form the key over.
*/
public Key(final Column column) {
this(new Column[] { column });
}
/**
* Returns the list of columns this key is formed over. It may return an
* empty set.
*
* @return the list of columns this key involves.
*/
public Column[] getColumns() {
return this.columns;
}
/**
* Returns all relations this key is involved in. The set may be empty but
* it will never be <tt>null</tt>.
*
* @return the set of all relations this key is involved in.
*/
public BeanCollection getRelations() {
return this.relations;
}
/**
* Returns the status of this key. The default value, unless otherwise
* specified, is {@link ComponentStatus#INFERRED}.
*
* @return the status of this key.
*/
public ComponentStatus getStatus() {
return this.status;
}
/**
* Returns the table this key is formed over.
*
* @return the table this key involves.
*/
public Table getTable() {
return this.columns[0].getTable();
}
/**
* Replaces the set of columns this key is formed over with a new set.
*
* @param columns
* the replacement columns, in order.
*/
public void setColumns(final Column[] columns) {
Log.debug("Creating key over " + columns);
final Column[] oldValue = this.columns;
if (this.columns == columns || this.columns != null
&& this.columns.equals(columns))
return;
this.columns = columns;
this.pcs.firePropertyChange("columns", oldValue, columns);
}
/**
* Sets the status of this key.
*
* @param status
* the new status of this key.
*/
public void setStatus(final ComponentStatus status) {
Log.debug("Changing status for " + this + " to " + status);
final ComponentStatus oldValue = this.status;
if (this.status == status || this.status != null
&& this.status.equals(status))
return;
this.status = status;
this.pcs.firePropertyChange("status", oldValue, status);
}
private String getName() {
final StringBuffer sb = new StringBuffer();
sb.append(this.getTable() == null ? "<undef>" : this.getTable()
.toString());
sb.append(" [");
for (int i = 0; i < this.columns.length; i++) {
if (i > 0)
sb.append(',');
sb
.append(this.columns[i] instanceof DataSetColumn ? ((DataSetColumn) this.columns[i])
.getModifiedName()
: this.columns[i].getName());
}
sb.append(']');
return sb.toString();
}
public int compareTo(final Object o) throws ClassCastException {
final Key k = (Key) o;
return (this.getTable().getSchema().getMart().getUniqueId() + "_" + this
.toString()).compareTo(k.getTable().getSchema().getMart()
.getUniqueId()
+ "_" + k.toString());
}
public boolean equals(final Object o) {
if (o == this)
return true;
else if (o == null)
return false;
else if (o instanceof Key) {
final Key k = (Key) o;
return k.getClass().equals(this.getClass())
&& (k.getTable().getSchema().getMart().getUniqueId() + "_" + k
.toString()).equals(this.getTable().getSchema()
.getMart().getUniqueId()
+ "_" + this.toString());
} else
return false;
}
public int hashCode() {
// The hash code is only against the table itself, in
// case our column names change. This is to ensure
// that we stay in the same hash buckets.
return this.getTable().hashCode();
}
public String toString() {
return this.getName();
}
/**
* This implementation is a simple primary key.
*/
public static class PrimaryKey extends Key {
private static final long serialVersionUID = 1L;
private final PropertyChangeListener listener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
if (!PrimaryKey.this.equals(PrimaryKey.this.getTable()
.getPrimaryKey())) {
final List deadRels = new ArrayList(PrimaryKey.this
.getRelations());
for (final Iterator i = deadRels.iterator(); i.hasNext();) {
final Relation rel = (Relation) i.next();
PrimaryKey.this.getRelations().remove(rel);
rel.getOtherKey(PrimaryKey.this).getRelations().remove(
rel);
}
}
}
};
/**
* The constructor passes on all its work to the {@link Key}
* constructor.
*
* @param columns
* the list of columns to form the key over.
*/
public PrimaryKey(final Column[] columns) {
super(columns);
// If we are removed from the table, remove all our relations.
this.getTable().addPropertyChangeListener("primaryKey",
this.listener);
}
public String toString() {
return super.toString() + " {" + Resources.get("pkPrefix") + "}";
}
}
/**
* This implementation is a simple foreign key.
*/
public static class ForeignKey extends Key {
private static final long serialVersionUID = 1L;
private final PropertyChangeListener listener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
if (!ForeignKey.this.getTable().getForeignKeys().contains(
ForeignKey.this)) {
final List deadRels = new ArrayList(ForeignKey.this
.getRelations());
for (final Iterator i = deadRels.iterator(); i.hasNext();) {
final Relation rel = (Relation) i.next();
ForeignKey.this.getRelations().remove(rel);
rel.getOtherKey(ForeignKey.this).getRelations().remove(
rel);
}
}
}
};
/**
* The constructor passes on all its work to the {@link Key}
* constructor. It then adds itself to the set of foreign keys on the
* parent table.
*
* @param columns
* the list of columns to form the key over.
*/
public ForeignKey(final Column[] columns) {
super(columns);
// If we are removed from the table, remove all our relations.
this.getTable().getForeignKeys().addPropertyChangeListener(
this.listener);
}
public String toString() {
return super.toString() + " {" + Resources.get("fkPrefix") + "}";
}
}
}