/*
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.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.biomart.builder.exceptions.ValidationException;
import org.biomart.builder.model.Key.ForeignKey;
import org.biomart.builder.model.Key.PrimaryKey;
import org.biomart.common.exceptions.AssociationException;
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;
/**
* A relation represents the association between two keys. Relations between two
* primary keys are always 1:1. Relations between two foreign keys are either
* 1:1 or M:M. Relations between a foreign key and a primary key can either be
* 1:1 or 1:M.
* <p>
* Both keys must have the same number of columns, and the related columns
* should appear in the same order in both keys. If they do not, then results
* may be unpredictable.
* <p>
* A {@link Relation} class forms the basic functionality outlined above.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.58 $, $Date: 2008-03-06 11:32:30 $, modified by
* $Author: rh4 $
* @since 0.5
*/
public class Relation implements Comparable, TransactionListener {
private static final long serialVersionUID = 1L;
private Cardinality cardinality;
private Cardinality originalCardinality;
private final Key firstKey;
private final Key secondKey;
private Key oneKey;
private Key manyKey;
private boolean oneToManyAAllowed;
private boolean oneToManyBAllowed;
private boolean oneToOne;
private boolean oneToManyA;
private boolean oneToManyB;
private boolean external;
private ComponentStatus status;
private boolean visibleModified = Transaction.getCurrentTransaction() == null ? false
: Transaction.getCurrentTransaction().isAllowVisModChange();
private boolean directModified = false;
private final Map mods = new HashMap();
private static final String DATASET_WIDE = "__DATASET_WIDE__";
/**
* Subclasses use this field to fire events of their own.
*/
protected final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport(
this);
// All changes to us make us modified.
private final PropertyChangeListener listener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
Relation.this.setDirectModified(true);
}
};
// Add listeners to keys such that if the number of columns
// no longer match, the relation will be removed.
private final PropertyChangeListener keyColListener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
if (Relation.this.firstKey.getColumns().length != Relation.this.secondKey
.getColumns().length) {
Relation.this.firstKey.getRelations().remove(Relation.this);
Relation.this.secondKey.getRelations().remove(Relation.this);
}
}
};
/**
* This constructor tests that both ends of the relation have keys with the
* same number of columns. The default constructor sets the status to
* {@link ComponentStatus#INFERRED}.
*
* @param firstKey
* the first key.
* @param secondKey
* the second key.
* @param cardinality
* the cardinality of the foreign key end of this relation. If
* both keys are primary keys, then this is ignored and defaults
* to 1 (meaning 1:1). If they are a mixture, then this
* differentiates between 1:1 and 1:M. If they are both foreign
* keys, then this differentiates between 1:1 and M:M. See
* {@link #setCardinality(Cardinality)}.
* @throws AssociationException
* if the number of columns in the keys don't match, or if the
* relation already exists.
*/
public Relation(Key firstKey, Key secondKey, final Cardinality cardinality)
throws AssociationException {
Log.debug("Creating relation between " + firstKey + " and " + secondKey
+ " with cardinality " + cardinality);
// Remember the keys etc.
this.firstKey = firstKey;
this.secondKey = secondKey;
this.setOriginalCardinality(cardinality);
this.setCardinality(cardinality);
this.setStatus(ComponentStatus.INFERRED);
// Check the keys have the same number of columns.
if (firstKey.getColumns().length != secondKey.getColumns().length)
throw new AssociationException(Resources
.get("keyColumnCountMismatch"));
// Check the relation doesn't already exist.
if (firstKey.getRelations().contains(this))
throw new AssociationException(Resources
.get("relationAlreadyExists"));
// Cannot place a relation on an FK to this table if it
// already has relations.
if (firstKey.getTable().equals(secondKey.getTable())
&& (firstKey instanceof ForeignKey
&& firstKey.getRelations().size() > 0 || secondKey instanceof ForeignKey
&& secondKey.getRelations().size() > 0))
throw new AssociationException(Resources
.get("fkToThisOnceOrOthers"));
// Cannot place a relation on an FK to another table if
// it already has a relation to this table (it will have
// only one due to previous check).
if (!firstKey.getTable().equals(secondKey.getTable())
&& ((firstKey instanceof ForeignKey
&& firstKey.getRelations().size() == 1 && ((Relation) firstKey
.getRelations().iterator().next())
.getOtherKey(firstKey).getTable().equals(
firstKey.getTable())) || (secondKey instanceof ForeignKey
&& secondKey.getRelations().size() == 1 && ((Relation) secondKey
.getRelations().iterator().next()).getOtherKey(
secondKey).getTable().equals(secondKey.getTable()))))
throw new AssociationException(Resources
.get("fkToThisOnceOrOthers"));
// Update flags.
this.oneToManyAAllowed = this.secondKey instanceof ForeignKey;
this.oneToManyBAllowed = this.firstKey instanceof ForeignKey;
this.external = !this.firstKey.getTable().getSchema().equals(
this.secondKey.getTable().getSchema());
Transaction.addTransactionListener(this);
this.firstKey.addPropertyChangeListener("columns", this.keyColListener);
this.secondKey
.addPropertyChangeListener("columns", this.keyColListener);
this.addPropertyChangeListener("cardinality", this.listener);
this.addPropertyChangeListener("originalCardinality", this.listener);
this.addPropertyChangeListener("status", this.listener);
this.addPropertyChangeListener("compoundRelation", this.listener);
this.addPropertyChangeListener("unrolledRelation", this.listener);
this.addPropertyChangeListener("forceRelation", this.listener);
this.addPropertyChangeListener("loopbackRelation", this.listener);
this.addPropertyChangeListener("maskRelation", this.listener);
this.addPropertyChangeListener("mergeRelation", this.listener);
this.addPropertyChangeListener("restrictRelation", this.listener);
this.addPropertyChangeListener("subclassRelation", this.listener);
this.addPropertyChangeListener("alternativeJoin", 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) {
if (modified == this.visibleModified)
return;
final boolean oldValue = this.visibleModified;
this.visibleModified = modified;
this.pcs.firePropertyChange("visibleModified", oldValue, modified);
this.setDirectModified(true);
}
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 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);
}
/**
* Returns the cardinality of the foreign key end of this relation, in a 1:M
* relation. In 1:1 relations this will always return 1, and in M:M
* relations it will always return M.
*
* @return the cardinality of the foreign key end of this relation, in 1:M
* relations only. Otherwise determined by the relation type.
*/
public Cardinality getCardinality() {
return this.cardinality;
}
/**
* Returns the original cardinality of the foreign key end of this relation,
* in a 1:M relation. In 1:1 relations this will always return 1, and in M:M
* relations it will always return M.
*
* @return the original cardinality of the foreign key end of this relation,
* in 1:M relations only. Otherwise determined by the relation type.
*/
public Cardinality getOriginalCardinality() {
return this.originalCardinality;
}
/**
* Returns the first key of this relation. The concept of which key is first
* and which is second depends merely on the order they were passed to the
* constructor.
*
* @return the first key.
*/
public Key getFirstKey() {
return this.firstKey;
}
/**
* In a 1:M relation, this will return the M end of the relation. In all
* other relation types, this will return <tt>null</tt>.
*
* @return the key at the many end of the relation, or <tt>null</tt> if
* this is not a 1:M relation.
*/
public Key getManyKey() {
return this.manyKey;
}
/**
* In a 1:M relation, this will return the 1 end of the relation. In all
* other relation types, this will return <tt>null</tt>.
*
* @return the key at the one end of the relation, or <tt>null</tt> if
* this is not a 1:M relation.
*/
public Key getOneKey() {
return this.oneKey;
}
/**
* Given a key that is in this relationship, return the other key.
*
* @param key
* the key we know is in this relationship.
* @return the other key in this relationship, or <tt>null</tt> if the key
* specified is not in this relationship.
*/
public Key getOtherKey(final Key key) {
return this.firstKey.equals(key) ? this.secondKey : this.firstKey;
}
/**
* Returns the second key of this relation. The concept of which key is
* first and which is second depends merely on the order they were passed to
* the constructor.
*
* @return the second key.
*/
public Key getSecondKey() {
return this.secondKey;
}
/**
* Returns the status of this relation. The default value, unless otherwise
* specified, is {@link ComponentStatus#INFERRED}.
*
* @return the status of this relation.
*/
public ComponentStatus getStatus() {
return this.status;
}
/**
* Returns <tt>true</tt> if this relation involves keys in two separate
* schemas. Those that do are external, those that don't are not.
*
* @return <tt>true</tt> if this is external, <tt>false</tt> otherwise.
*/
public boolean isExternal() {
return this.external;
}
/**
* Returns <tt>true</tt> if this is a 1:M(a) relation.
*
* @return <tt>true</tt> if this is a 1:M(a) relation, <tt>false</tt>
* otherwise.
*/
public boolean isOneToManyA() {
return this.oneToManyA;
}
/**
* Returns <tt>true</tt> if this is a 1:M(b) relation.
*
* @return <tt>true</tt> if this is a 1:M(b) relation, <tt>false</tt>
* otherwise.
*/
public boolean isOneToManyB() {
return this.oneToManyB;
}
/**
* Returns <tt>true</tt> if this is either kind of 1:M relation.
*
* @return <tt>true</tt> if this is either kind of 1:M relation,
* <tt>false</tt> otherwise.
*/
public boolean isOneToMany() {
return this.oneToManyA || this.oneToManyB;
}
/**
* Can this relation be 1:M(a)? Returns <tt>true</tt> in all cases where
* the two keys are of different types.
*
* @return <tt>true</tt> if this can be 1:M(a), <tt>false</tt> if not.
*/
public boolean isOneToManyAAllowed() {
return this.oneToManyAAllowed;
}
/**
* Can this relation be 1:M(b)? Returns <tt>true</tt> in all cases where
* the two keys are of different types.
*
* @return <tt>true</tt> if this can be 1:M(b), <tt>false</tt> if not.
*/
public boolean isOneToManyBAllowed() {
return this.oneToManyBAllowed;
}
/**
* Returns <tt>true</tt> if this is a 1:1 relation.
*
* @return <tt>true</tt> if this is a 1:1 relation, <tt>false</tt>
* otherwise.
*/
public boolean isOneToOne() {
return this.oneToOne;
}
/**
* Returns the key in this relation associated with the given table. If both
* keys are on that table, returns the one that is a PK, or the first one if
* both are FKs.
*
* @param table
* the table to get the key for.
* @return the key for that table. <tt>null</tt> if neither key is from
* that table.
*/
public Key getKeyForTable(final Table table) {
return this.firstKey.getTable().equals(table) ? this.firstKey
: this.secondKey;
}
/**
* Returns the key in this relation associated with the given schema. If
* both keys are on tables in that schema, returns the first one.
*
* @param schema
* the schema to get the key for.
* @return the key for that schema. <tt>null</tt> if neither key is from
* that schema.
*/
public Key getKeyForSchema(final Schema schema) {
return this.firstKey.getTable().getSchema().equals(schema) ? this.firstKey
: this.secondKey;
}
/**
* Sets the cardinality of the foreign key end of this relation, in a 1:M
* relation. If used on a 1:1 or M:M relation, then specifying M makes it
* M:M and specifying 1 makes it 1:1.
*
* @param cardinality
* the cardinality.
*/
public void setCardinality(Cardinality cardinality) {
Log.debug("Changing cardinality of " + this + " to " + cardinality);
if (this.firstKey instanceof PrimaryKey
&& this.secondKey instanceof PrimaryKey) {
Log.debug("Overriding cardinality change to ONE");
cardinality = Cardinality.ONE;
}
// TODO This is a backwards-compatibility clause that needs to
// stay in throughout the 0.7 release. It can be removed in 0.8.
if (cardinality == Cardinality.MANY
&& this.secondKey instanceof PrimaryKey) {
cardinality = Cardinality.MANY_B;
} else if (cardinality == Cardinality.MANY
&& this.firstKey instanceof PrimaryKey) {
cardinality = Cardinality.MANY_A;
}
// End fudge-mode.
final Cardinality oldValue = this.cardinality;
if (this.cardinality == cardinality || this.cardinality != null
&& this.cardinality.equals(cardinality))
return;
this.cardinality = cardinality;
if (this.cardinality.equals(Cardinality.ONE)) {
this.oneToOne = true;
this.oneToManyA = false;
this.oneToManyB = false;
this.oneKey = null;
this.manyKey = null;
} else if (this.cardinality.equals(Cardinality.MANY_A)) {
this.oneToOne = false;
this.oneToManyA = true;
this.oneToManyB = false;
this.oneKey = this.firstKey;
this.manyKey = this.secondKey;
} else if (this.cardinality.equals(Cardinality.MANY_B)) {
this.oneToOne = false;
this.oneToManyA = false;
this.oneToManyB = true;
this.oneKey = this.secondKey;
this.manyKey = this.firstKey;
} else {
// TODO This is a backwards-compatibility clause that needs to
// stay in throughout the 0.7 release. It can be removed in 0.8.
this.oneToOne = false;
this.oneToManyA = false;
this.oneToManyB = false;
this.oneKey = null;
this.manyKey = null;
// End fudge-mode.
}
if (Transaction.getCurrentTransaction() != null
&& Transaction.getCurrentTransaction().isAllowVisModChange())
this.setVisibleModified(true);
this.pcs.firePropertyChange("cardinality", oldValue, cardinality);
}
/**
* Sets the original cardinality of the foreign key end of this relation, in
* a 1:M relation. If used on a 1:1 or M:M relation, then specifying M makes
* it M:M and specifying 1 makes it 1:1.
*
* @param originalCardinality
* the originalCardinality.
*/
public void setOriginalCardinality(Cardinality originalCardinality) {
Log.debug("Changing original cardinality of " + this + " to "
+ originalCardinality);
final Cardinality oldValue = this.originalCardinality;
if (this.originalCardinality == originalCardinality
|| this.originalCardinality != null
&& this.originalCardinality.equals(originalCardinality))
return;
this.originalCardinality = originalCardinality;
// TODO This is a backwards-compatibility clause that needs to
// stay in throughout the 0.7 release. It can be removed in 0.8.
if (oldValue == Cardinality.MANY
&& !(this.firstKey instanceof PrimaryKey || this.secondKey instanceof PrimaryKey))
return;
// End fudge-mode.
this.pcs.firePropertyChange("originalCardinality", oldValue,
originalCardinality);
}
/**
* Sets the status of this relation.
*
* @param status
* the new status of this relation.
* @throws AssociationException
* if the keys at either end of the relation are incompatible
* upon attempting to mark an
* {@link ComponentStatus#INFERRED_INCORRECT} relation as
* anything else.
*/
public void setStatus(final ComponentStatus status)
throws AssociationException {
Log.debug("Changing status of " + this + " to " + status);
// If the new status is not incorrect, we need to make sure we
// can legally do this, ie. the two keys have the same number of
// columns each.
if (!status.equals(ComponentStatus.INFERRED_INCORRECT))
// Check both keys have same cardinality.
if (this.firstKey.getColumns().length != this.secondKey
.getColumns().length)
throw new AssociationException(Resources
.get("keyColumnCountMismatch"));
final ComponentStatus oldValue = this.status;
if (this.status == status || this.status != null
&& this.status.equals(status))
return;
// Make the change.
this.status = status;
this.pcs.firePropertyChange("status", oldValue, status);
}
/**
* 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 = Relation.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);
}
/**
* Is this relation subclassed?
*
* @param dataset
* the dataset to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isSubclassRelation(final DataSet dataset) {
return this.getMods(dataset, null).containsKey("subclassRelation");
}
/**
* Subclass this relation.
*
* @param dataset
* the dataset to set for.
* @param subclass
* <tt>true</tt> to subclass it, <tt>false</tt> to not.
* @throws ValidationException
* if it cannot make the change.
*/
public void setSubclassRelation(final DataSet dataset,
final boolean subclass) throws ValidationException {
final boolean oldValue = this.isSubclassRelation(dataset);
if (subclass == oldValue)
return;
if (subclass) {
// Work out the child end of the relation - the M end. The parent is
// the 1 end.
final Table parentTable = this.getOneKey().getTable();
final Table childTable = this.getManyKey().getTable();
if (parentTable.equals(childTable))
throw new ValidationException(Resources
.get("subclassNotBetweenTwoTables"));
if (parentTable.getPrimaryKey() == null
|| childTable.getPrimaryKey() == null)
throw new ValidationException(Resources
.get("subclassTargetNoPK"));
// We need to test if the selected relation links to
// a table which itself has subclass relations, or
// is the central table, and has not got an
// existing subclass relation in the direction we
// are working in.
boolean hasConflict = false;
final Set combinedRels = new HashSet();
combinedRels.addAll(parentTable.getRelations());
combinedRels.addAll(childTable.getRelations());
for (final Iterator i = combinedRels.iterator(); i.hasNext()
&& !hasConflict;) {
final Relation rel = (Relation) i.next();
if (!rel.isSubclassRelation(dataset))
continue;
else if (rel.getOneKey().getTable().equals(parentTable)
|| rel.getManyKey().getTable().equals(childTable))
hasConflict = true;
}
// If child has M:1 or parent has 1:M, we cannot do this.
if (hasConflict)
throw new ValidationException(Resources
.get("mixedCardinalitySubclasses"));
// Now do it.
this.getMods(dataset, null).put("subclassRelation", null);
this.pcs.firePropertyChange("subclassRelation", null, dataset);
} else {
// Break the chain first.
final Key key = this.getManyKey();
if (key != null) {
final Table target = key.getTable();
if (!target.equals(dataset.getCentralTable()))
if (target.getPrimaryKey() != null)
for (final Iterator i = target.getPrimaryKey()
.getRelations().iterator(); i.hasNext();) {
final Relation rel = (Relation) i.next();
if (rel.isOneToMany())
rel.setSubclassRelation(dataset, false);
}
}
// Now do it.
this.getMods(dataset, null).remove("subclassRelation");
this.pcs.firePropertyChange("subclassRelation", dataset, null);
}
}
/**
* Is this relation merged?
*
* @param dataset
* the dataset to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isMergeRelation(final DataSet dataset) {
return this.getMods(dataset, null).containsKey("mergeRelation");
}
/**
* Merge this relation.
*
* @param dataset
* the dataset to set for.
* @param merge
* <tt>true</tt> to merge it, <tt>false</tt> to not.
*/
public void setMergeRelation(final DataSet dataset, final boolean merge) {
final boolean oldValue = this.isMergeRelation(dataset);
if (merge == oldValue)
return;
if (merge) {
this.getMods(dataset, null).put("mergeRelation", null);
this.pcs.firePropertyChange("mergeRelation", null, dataset);
} else {
this.getMods(dataset, null).remove("mergeRelation");
this.pcs.firePropertyChange("mergeRelation", dataset, null);
}
}
/**
* Is this relation masked?
*
* @param dataset
* the dataset to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isMaskRelation(final DataSet dataset) {
return this.getMods(dataset, null).containsKey("maskRelation");
}
/**
* Is this relation masked?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isMaskRelation(final DataSet dataset, final String tableKey) {
return this.isMaskRelation(dataset)
|| this.getMods(dataset, tableKey).containsKey("maskRelation");
}
/**
* Mask this relation.
*
* @param dataset
* the dataset to set for.
* @param mask
* <tt>true</tt> to mask it, <tt>false</tt> to not.
*/
public void setMaskRelation(final DataSet dataset, final boolean mask) {
final boolean oldValue = this.isMaskRelation(dataset);
if (mask == oldValue)
return;
if (mask) {
this.getMods(dataset, null).put("maskRelation", null);
this.pcs.firePropertyChange("maskRelation", null, dataset);
} else {
this.getMods(dataset, null).remove("maskRelation");
this.pcs.firePropertyChange("maskRelation", dataset, null);
}
}
/**
* Mask this relation.
*
* @param dataset
* the dataset to set for.
* @param tableKey
* the dataset table to set for.
* @param mask
* <tt>true</tt> to mask it, <tt>false</tt> to not.
*/
public void setMaskRelation(final DataSet dataset, final String tableKey,
final boolean mask) {
final boolean oldValue = this.isMaskRelation(dataset, tableKey);
if (mask == oldValue)
return;
if (mask) {
this.getMods(dataset, tableKey).put("maskRelation", null);
this.pcs.firePropertyChange("maskRelation", null, tableKey);
} else {
this.getMods(dataset, tableKey).remove("maskRelation");
this.pcs.firePropertyChange("maskRelation", tableKey, null);
}
}
/**
* Is this relation alternative-joined?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isAlternativeJoin(final DataSet dataset,
final String tableKey) {
return this.getMods(dataset, tableKey).containsKey("alternativeJoin");
}
/**
* Alternative-join this relation.
*
* @param dataset
* the dataset to set for.
* @param tableKey
* the dataset table to set for.
* @param join
* <tt>true</tt> to do it, <tt>false</tt> to not.
*/
public void setAlternativeJoin(final DataSet dataset,
final String tableKey, final boolean join) {
final boolean oldValue = this.isAlternativeJoin(dataset, tableKey);
if (join == oldValue)
return;
if (join) {
this.getMods(dataset, tableKey).put("alternativeJoin", null);
this.pcs.firePropertyChange("alternativeJoin", null, tableKey);
} else {
this.getMods(dataset, tableKey).remove("alternativeJoin");
this.pcs.firePropertyChange("alternativeJoin", tableKey, null);
}
}
/**
* Is this relation forced?
*
* @param dataset
* the dataset to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isForceRelation(final DataSet dataset) {
return this.getMods(dataset, null).containsKey("forceRelation");
}
/**
* Is this relation forced?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isForceRelation(final DataSet dataset, final String tableKey) {
return this.isForceRelation(dataset)
|| this.getMods(dataset, tableKey).containsKey("forceRelation");
}
/**
* Force this relation.
*
* @param dataset
* the dataset to set for.
* @param forced
* <tt>true</tt> to force it, <tt>false</tt> to not.
*/
public void setForceRelation(final DataSet dataset, final boolean forced) {
final boolean oldValue = this.isForceRelation(dataset);
if (forced == oldValue)
return;
if (forced) {
this.getMods(dataset, null).put("forceRelation", null);
this.pcs.firePropertyChange("forceRelation", null, dataset);
} else {
this.getMods(dataset, null).remove("forceRelation");
this.pcs.firePropertyChange("forceRelation", dataset, null);
}
}
/**
* Force this relation.
*
* @param dataset
* the dataset to set for.
* @param tableKey
* the dataset table to set for.
* @param forced
* <tt>true</tt> to force it, <tt>false</tt> to not.
*/
public void setForceRelation(final DataSet dataset, final String tableKey,
final boolean forced) {
final boolean oldValue = this.isForceRelation(dataset, tableKey);
if (forced == oldValue)
return;
if (forced) {
this.getMods(dataset, tableKey).put("forceRelation", null);
this.pcs.firePropertyChange("forceRelation", null, tableKey);
} else {
this.getMods(dataset, tableKey).remove("forceRelation");
this.pcs.firePropertyChange("forceRelation", tableKey, null);
}
}
/**
* Is this relation loopbacked?
*
* @param dataset
* the dataset to check for.
* @return true if it is, false otherwise.
*/
public boolean isLoopbackRelation(final DataSet dataset) {
return this.getMods(dataset, null).containsKey("loopbackRelation");
}
/**
* Is this relation loopbacked?
*
* @param dataset
* the dataset to check for.
* @return the column to use if it is, null otherwise.
*/
public Column getLoopbackRelation(final DataSet dataset) {
return (Column) this.getMods(dataset, null).get("loopbackRelation");
}
/**
* Is this relation loopbacked?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return true if it is, false otherwise.
*/
public boolean isLoopbackRelation(final DataSet dataset,
final String tableKey) {
return this.getMods(dataset, tableKey).containsKey("loopbackRelation");
}
/**
* Is this relation loopbacked?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return the column to use if it is, null otherwise.
*/
public Column getLoopbackRelation(final DataSet dataset,
final String tableKey) {
Column result = (Column) this.getMods(dataset, tableKey).get(
"loopbackRelation");
if (result == null)
result = this.getLoopbackRelation(dataset);
return result;
}
/**
* Loopback this relation.
*
* @param dataset
* the dataset to set for.
* @param loopback
* the column to set - if null, it undoes it.
* @throws ValidationException
* if it cannot be done.
*/
public void setLoopbackRelation(final DataSet dataset, final Column loopback)
throws ValidationException {
final Column oldValue = this.getLoopbackRelation(dataset);
if (loopback == oldValue || oldValue != null
&& oldValue.equals(loopback))
return;
// Check that the relation is a 1:M relation.
if (!this.isOneToMany())
throw new ValidationException(Resources.get("loopbackNotOneMany"));
if (loopback != null) {
this.getMods(dataset, null).put("loopbackRelation", loopback);
this.pcs.firePropertyChange("loopbackRelation", null, dataset);
} else {
this.getMods(dataset, null).remove("loopbackRelation");
this.pcs.firePropertyChange("loopbackRelation", dataset, null);
}
}
/**
* Loopback this relation.
*
* @param dataset
* the dataset to set for.
* @param tableKey
* the dataset table to set for.
* @param loopback
* the column to set - if null, it undoes it.
* @throws ValidationException
* if it cannot be done.
*/
public void setLoopbackRelation(final DataSet dataset,
final String tableKey, final Column loopback)
throws ValidationException {
final Column oldValue = this.getLoopbackRelation(dataset, tableKey);
if (loopback == oldValue || oldValue != null
&& oldValue.equals(loopback))
return;
// Check that the relation is a 1:M relation.
if (!this.isOneToMany())
throw new ValidationException(Resources.get("loopbackNotOneMany"));
if (loopback != null) {
this.getMods(dataset, tableKey).put("loopbackRelation", loopback);
this.pcs.firePropertyChange("loopbackRelation", null, tableKey);
} else {
this.getMods(dataset, tableKey).remove("loopbackRelation");
this.pcs.firePropertyChange("loopbackRelation", tableKey, null);
}
}
/**
* Is this relation restricted, anywhere (regardless of iteration)?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return <tt>true</tt> if it is.
*/
public boolean isRestrictRelation(final DataSet dataset,
final String tableKey) {
return this.getMods(dataset, tableKey).containsKey("restrictRelation")
&& !((Map) this.getMods(dataset, tableKey).get(
"restrictRelation")).isEmpty();
}
/**
* Is this relation compounded?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @param n
* the index to lookup. 0 is first.
* @return the def to use if it is, null otherwise.
*/
public RestrictedRelationDefinition getRestrictRelation(
final DataSet dataset, final String tableKey, final int n) {
return !this.getMods(dataset, tableKey).containsKey("restrictRelation") ? null
: (RestrictedRelationDefinition) ((Map) this.getMods(dataset,
tableKey).get("restrictRelation")).get(new Integer(n));
}
/**
* Restrict this relation.
*
* @param dataset
* the dataset to set for.
* @param tableKey
* the dataset table to set for.
* @param n
* the index of the relation to restrict - 0 is first.
* @param def
* the definition to set - if null, it undoes it.
*/
public void setRestrictRelation(final DataSet dataset,
final String tableKey, final RestrictedRelationDefinition def,
final int n) {
final RestrictedRelationDefinition oldValue = this.getRestrictRelation(
dataset, tableKey, n);
if (def == oldValue || oldValue != null && oldValue.equals(def))
return;
if (def != null) {
if (!this.getMods(dataset, tableKey)
.containsKey("restrictRelation"))
this.getMods(dataset, tableKey).put("restrictRelation",
new HashMap());
((Map) this.getMods(dataset, tableKey).get("restrictRelation"))
.put(new Integer(n), def);
def.addPropertyChangeListener("directModified", this.listener);
this.pcs.firePropertyChange("restrictRelation", null, tableKey);
} else {
if (this.getMods(dataset, tableKey).containsKey("restrictRelation"))
((Map) this.getMods(dataset, tableKey).get("restrictRelation"))
.remove(new Integer(n));
this.pcs.firePropertyChange("restrictRelation", tableKey, null);
}
}
/**
* Is this relation unrolled?
*
* @param dataset
* the dataset to check for.
* @return the def to use if it is, null otherwise.
*/
public UnrolledRelationDefinition getUnrolledRelation(final DataSet dataset) {
return (UnrolledRelationDefinition) this.getMods(dataset, null).get(
"unrolledRelation");
}
/**
* Unroll this relation.
*
* @param dataset
* the dataset to set for.
* @param def
* the definition to set - if null, it undoes it.
*/
public void setUnrolledRelation(final DataSet dataset,
final UnrolledRelationDefinition def) {
final UnrolledRelationDefinition oldValue = this
.getUnrolledRelation(dataset);
if (def == oldValue || oldValue != null && oldValue.equals(def))
return;
if (def != null) {
this.getMods(dataset, null).put("unrolledRelation", def);
def.addPropertyChangeListener("directModified", this.listener);
this.pcs.firePropertyChange("unrolledRelation", null, dataset);
} else {
this.getMods(dataset, null).remove("unrolledRelation");
this.pcs.firePropertyChange("unrolledRelation", dataset, null);
}
}
/**
* Is this relation compounded?
*
* @param dataset
* the dataset to check for.
* @return the def to use if it is, null otherwise.
*/
public CompoundRelationDefinition getCompoundRelation(final DataSet dataset) {
return (CompoundRelationDefinition) this.getMods(dataset, null).get(
"compoundRelation");
}
/**
* Is this relation compounded?
*
* @param dataset
* the dataset to check for.
* @return true if it is, false otherwise.
*/
public boolean isCompoundRelation(final DataSet dataset) {
return this.getMods(dataset, null).containsKey("compoundRelation");
}
/**
* Is this relation compounded?
*
* @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 CompoundRelationDefinition getCompoundRelation(
final DataSet dataset, final String tableKey) {
CompoundRelationDefinition result = (CompoundRelationDefinition) this
.getMods(dataset, tableKey).get("compoundRelation");
if (result == null)
result = this.getCompoundRelation(dataset);
return result;
}
/**
* Is this relation compounded?
*
* @param dataset
* the dataset to check for.
* @param tableKey
* the table to check for.
* @return true if it is, false otherwise.
*/
public boolean isCompoundRelation(final DataSet dataset,
final String tableKey) {
return this.getMods(dataset, tableKey).containsKey("compoundRelation");
}
/**
* Compound this relation.
*
* @param dataset
* the dataset to set for.
* @param def
* the definition to set - if null, it undoes it.
*/
public void setCompoundRelation(final DataSet dataset,
final CompoundRelationDefinition def) {
final CompoundRelationDefinition oldValue = this
.getCompoundRelation(dataset);
if (def == oldValue || oldValue != null && oldValue.equals(def))
return;
if (def != null) {
this.getMods(dataset, null).put("compoundRelation", def);
def.addPropertyChangeListener("directModified", this.listener);
this.pcs.firePropertyChange("compoundRelation", null, dataset);
} else {
this.getMods(dataset, null).remove("compoundRelation");
this.pcs.firePropertyChange("compoundRelation", dataset, null);
}
}
/**
* Compound this relation.
*
* @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 setCompoundRelation(final DataSet dataset,
final String tableKey, final CompoundRelationDefinition def) {
final CompoundRelationDefinition oldValue = this.getCompoundRelation(
dataset, tableKey);
if (def == oldValue || oldValue != null && oldValue.equals(def))
return;
if (def != null) {
this.getMods(dataset, tableKey).put("compoundRelation", def);
def.addPropertyChangeListener("directModified", this.listener);
this.pcs.firePropertyChange("compoundRelation", null, tableKey);
} else {
this.getMods(dataset, tableKey).remove("compoundRelation");
this.pcs.firePropertyChange("compoundRelation", tableKey, null);
}
}
public int compareTo(final Object o) throws ClassCastException {
final Relation r = (Relation) o;
if (this.firstKey.equals(r.firstKey))
return this.secondKey.compareTo(r.secondKey);
else if (this.firstKey.equals(r.secondKey))
return this.secondKey.compareTo(r.firstKey);
else if (this.secondKey.equals(r.firstKey))
return this.firstKey.compareTo(r.secondKey);
else
return this.firstKey.compareTo(r.firstKey);
}
public int hashCode() {
final int firstHash = this.firstKey.hashCode();
final int secondHash = this.secondKey.hashCode();
// So that two rels between same keys always match.
return (Math.min(firstHash, secondHash) + "_" + Math.max(firstHash,
secondHash)).hashCode();
}
public boolean equals(final Object o) {
if (o == this)
return true;
else if (o == null)
return false;
else if (o instanceof Relation) {
final Relation r = (Relation) o;
// Check that the same keys are involved.
return r.firstKey.equals(this.secondKey)
&& r.secondKey.equals(this.firstKey)
|| r.firstKey.equals(this.firstKey)
&& r.secondKey.equals(this.secondKey);
} else
return false;
}
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(this.firstKey == null ? "<undef>" : this.firstKey.toString());
sb.append(" -> ");
sb.append(this.secondKey == null ? "<undef>" : this.secondKey
.toString());
return sb.toString();
}
/**
* This internal singleton class represents the cardinality of a relation.
* Note that the names of cardinality objects are case-sensitive.
*/
public static class Cardinality implements Comparable {
private static final long serialVersionUID = 1L;
private static final Map singletons = new HashMap();
/**
* Use this constant to refer to a relation with many values at the
* second key end.
*/
public static final Cardinality MANY_B = Cardinality.get("M(b)");
/**
* Use this constant to refer to a relation with many values at the
* first key end.
*/
public static final Cardinality MANY_A = Cardinality.get("M(a)");
// TODO This is a backwards-compatibility clause that needs to
// stay in throughout the 0.7 release. It can be removed in 0.8.
/**
* Use this constant to refer to a relation with many values at the
* first key end.
*/
public static final Cardinality MANY = Cardinality.get("M");
// End fudge-mode.
/**
* Use this constant to refer to a 1:1 relation.
*/
public static final Cardinality ONE = Cardinality.get("1");
/**
* The static factory method creates and returns a cardinality with the
* given name. It ensures the object returned is a singleton. Note that
* the names of cardinality objects are case-sensitive.
*
* @param name
* the name of the cardinality object.
* @return the cardinality object or null if null was passed in.
*/
public static Cardinality get(String name) {
// Return null for null name.
if (name == null)
return null;
// Do we already have this one?
// If so, then return it.
if (Cardinality.singletons.containsKey(name))
return (Cardinality) Cardinality.singletons.get(name);
// Otherwise, create it, remember it.
final Cardinality c = new Cardinality(name);
Cardinality.singletons.put(name, c);
// Return it.
return c;
}
private final String name;
/**
* The private constructor takes a single parameter, which defines the
* name this cardinality object will display when printed.
*
* @param name
* the name of the cardinality.
*/
private Cardinality(final String name) {
this.name = name;
}
public int compareTo(final Object o) throws ClassCastException {
final Cardinality c = (Cardinality) o;
return this.toString().compareTo(c.toString());
}
public boolean equals(final Object o) {
// We are dealing with singletons so can use == happily.
return o == this;
}
/**
* Displays the name of this cardinality object.
*
* @return the name of this cardinality object.
*/
public String getName() {
return this.name;
}
public int hashCode() {
return this.toString().hashCode();
}
/**
* {@inheritDoc}
* <p>
* Always returns the name of this cardinality.
*/
public String toString() {
return this.name;
}
}
/**
* Defines a compound relation.
*/
public static class CompoundRelationDefinition implements
TransactionListener {
private static final long serialVersionUID = 1L;
private int n;
private boolean parallel;
private boolean directModified = false;
private final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport(
this);
private final PropertyChangeListener listener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent e) {
CompoundRelationDefinition.this.setDirectModified(true);
}
};
/**
* This constructor gives the compound relation an arity and a flag
* indicating whether to follow the multiple copies in parallel or
* serial.
*
* @param n
* the number of times this relation has been compounded (the
* arity).
* @param parallel
* whether this is a parallel (<tt>true</tt>) or serial (<tt>false</tt>)
* compounding.
*/
public CompoundRelationDefinition(final int n, final boolean parallel) {
// Remember the settings.
this.n = n;
this.parallel = parallel;
Transaction.addTransactionListener(this);
this.addPropertyChangeListener("n", this.listener);
this.addPropertyChangeListener("parallel", this.listener);
}
/**
* Replicate ourselves.
*
* @return the copy.
*/
public CompoundRelationDefinition replicate() {
return new CompoundRelationDefinition(this.n, this.parallel);
}
/**
* 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.
}
/**
* Get the arity of this compound relation.
*
* @return the arity.
*/
public int getN() {
return this.n;
}
/**
* Set the arity of this compound relation.
*
* @param n
* the new arity.
*/
public void setN(final int n) {
if (n == this.n)
return;
final int oldValue = this.n;
this.n = n;
this.pcs.firePropertyChange("n", oldValue, n);
}
/**
* Is this compound relation parallel?
*
* @return <tt>true</tt> if it is.
*/
public boolean isParallel() {
return this.parallel;
}
/**
* Is this compound relation parallel?
*
* @param parallel
* <tt>true</tt> if it is.
*/
public void setParallel(final boolean parallel) {
if (parallel == this.parallel)
return;
final boolean oldValue = this.parallel;
this.parallel = parallel;
this.pcs.firePropertyChange("parallel", oldValue, parallel);
}
}
/**
* Defines an unrolled relation.
*/
public static class UnrolledRelationDefinition implements
TransactionListener {
private static final long serialVersionUID = 1L;
private Column nameColumn;
private boolean reversed;
private boolean directModified = false;
private final WeakPropertyChangeSupport pcs = new WeakPropertyChangeSupport(
this);
private final PropertyChangeListener listener = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent e) {
UnrolledRelationDefinition.this.setDirectModified(true);
}
};
/**
* This constructor gives the unrolled relation a name column and a flag
* indicating whether to reverse the sense of the unrolling.
*
* @param nameColumn
* the column to name each unrolled record with.
* @param reversed
* whether this is a reversed (<tt>true</tt>) unrolling.
*/
public UnrolledRelationDefinition(final Column nameColumn,
final boolean reversed) {
// Remember the settings.
this.nameColumn = nameColumn;
this.reversed = reversed;
Transaction.addTransactionListener(this);
this.addPropertyChangeListener("nameColumn", this.listener);
this.addPropertyChangeListener("reversed", this.listener);
}
/**
* Replicate ourselves.
*
* @return the copy.
*/
public UnrolledRelationDefinition replicate() {
return new UnrolledRelationDefinition(this.nameColumn,
this.reversed);
}
/**
* 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.
}
/**
* Get the name column of this unrolled relation.
*
* @return the column.
*/
public Column getNameColumn() {
return this.nameColumn;
}
/**
* Set the name column of this unrolled relation.
*
* @param nameColumn
* the column.
*/
public void setNameColumn(final Column nameColumn) {
final Column oldValue = this.nameColumn;
if (nameColumn == oldValue || oldValue != null
&& oldValue.equals(nameColumn))
return;
this.nameColumn = nameColumn;
this.pcs.firePropertyChange("nameColumn", oldValue, nameColumn);
}
/**
* Is this unrolled relation reversed?
*
* @return <tt>true</tt> if it is.
*/
public boolean isReversed() {
return this.reversed;
}
/**
* Is this unrolled relation reversed?
*
* @param reversed
* <tt>true</tt> if it is.
*/
public void setReversed(final boolean reversed) {
if (reversed == this.reversed)
return;
final boolean oldValue = this.reversed;
this.reversed = reversed;
this.pcs.firePropertyChange("reversed", oldValue, reversed);
}
}
/**
* Defines the restriction on a table, ie. a where-clause.
*/
public static class RestrictedRelationDefinition implements
TransactionListener {
private static final long serialVersionUID = 1L;
private BeanMap leftAliases;
private BeanMap rightAliases;
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) {
RestrictedRelationDefinition.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 leftAliases
* the aliases to use for columns on the LHS of the join.
* @param rightAliases
* the aliases to use for columns on the RHS of the join.
*/
public RestrictedRelationDefinition(final String expr, Map leftAliases,
Map rightAliases) {
// Test for good arguments.
if (expr == null || expr.trim().length() == 0)
throw new IllegalArgumentException(Resources
.get("relRestrictMissingExpression"));
if (leftAliases == null)
leftAliases = new HashMap();
if (rightAliases == null)
rightAliases = new HashMap();
if (leftAliases.isEmpty() && rightAliases.isEmpty())
throw new IllegalArgumentException(Resources
.get("relRestrictMissingAliases"));
// Remember the settings.
this.leftAliases = new BeanMap(new HashMap(leftAliases));
this.rightAliases = new BeanMap(new HashMap(rightAliases));
this.expr = expr;
Transaction.addTransactionListener(this);
this.addPropertyChangeListener(this.listener);
this.leftAliases.addPropertyChangeListener(this.listener);
this.rightAliases.addPropertyChangeListener(this.listener);
}
/**
* Replicate ourselves.
*
* @return the copy.
*/
public RestrictedRelationDefinition replicate() {
return new RestrictedRelationDefinition(this.expr,
this.leftAliases, this.rightAliases);
}
/**
* 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 getLeftAliases() {
return this.leftAliases;
}
/**
* 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 getRightAliases() {
return this.rightAliases;
}
/**
* Returns the expression, <i>without</i> substitution. This value is
* RDBMS-specific.
*
* @return the unsubstituted expression.
*/
public String getExpression() {
return this.expr;
}
/**
* 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);
}
/**
* Returns the expression, <i>with</i> substitution. This value is
* RDBMS-specific. The prefix map must contain two entries. Each entry
* relates to one of the keys of a relation. The key of the map is the
* key of the relation, and the value is the prefix to use in the
* substituion, eg. "a" if columns for the table for that key should be
* prefixed as "a.mycolumn".
*
* @param schemaPrefix
* the value to substitute for ':schemaPrefix'.
* @param leftTablePrefix
* the prefix to use for the LHS table in the expression.
* @param rightTablePrefix
* the prefix to use for the LHS table in the expression.
* @param leftIsDataSet
* if the LHS side is a dataset table.
* @param rightIsDataSet
* if the RHS side is a dataset table.
* @param mappingUnit
* the transformation unit this restriction will use to
* translate columns into dataset column equivalents.
* @return the substituted expression.
*/
public String getSubstitutedExpression(final String schemaPrefix,
final String leftTablePrefix, final String rightTablePrefix,
final boolean leftIsDataSet, final boolean rightIsDataSet,
final TransformationUnit mappingUnit) {
Log.debug("Calculating restricted table expression");
String sub = this.expr;
for (final Iterator i = this.leftAliases.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, leftTablePrefix
+ "."
+ (leftIsDataSet ? mappingUnit.getDataSetColumnFor(col)
.getModifiedName() : col.getName()));
}
for (final Iterator i = this.rightAliases.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, rightTablePrefix
+ "."
+ (rightIsDataSet ? mappingUnit
.getDataSetColumnFor(col).getModifiedName()
: col.getName()));
}
sub = sub.replaceAll(":" + Resources.get("schemaPrefix"),
schemaPrefix == null ? "null" : schemaPrefix);
Log.debug("Expression is: " + sub);
return sub;
}
}
}