/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * SQL Power Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.sqlobject; import java.beans.PropertyChangeEvent; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import ca.sqlpower.object.AbstractSPListener; import ca.sqlpower.object.ObjectDependentException; import ca.sqlpower.object.SPChildEvent; import ca.sqlpower.object.SPListener; import ca.sqlpower.object.SPObject; import ca.sqlpower.object.annotation.Accessor; import ca.sqlpower.object.annotation.Constructor; import ca.sqlpower.object.annotation.ConstructorParameter; import ca.sqlpower.object.annotation.ConstructorParameter.ParameterType; import ca.sqlpower.object.annotation.Mutator; import ca.sqlpower.object.annotation.NonProperty; import ca.sqlpower.object.annotation.Transient; import ca.sqlpower.sql.CachedRowSet; import ca.sqlpower.sqlobject.SQLIndex.Column; import ca.sqlpower.util.SQLPowerUtils; import ca.sqlpower.util.SessionNotFoundException; /** * The SQLRelationship class represents a foreign key relationship between * two SQLTable objects or two groups of columns within the same table. */ public class SQLRelationship extends SQLObject implements java.io.Serializable { /** * Defines an absolute ordering of the child types of this class. */ public static final List<Class<? extends SPObject>> allowedChildTypes = Collections.<Class<? extends SPObject>>singletonList(ColumnMapping.class); /** * Comparator that orders ColumnMapping objects by FK column position. */ public static class ColumnMappingFKColumnOrderComparator implements Comparator<ColumnMapping> { public int compare(ColumnMapping o1, ColumnMapping o2) { int fkPos1 = o1.getFkColumn().getParent().getChildren().indexOf(o1.getFkColumn()); int fkPos2 = o2.getFkColumn().getParent().getChildren().indexOf(o2.getFkColumn()); if (fkPos1 == fkPos2) return 0; if (fkPos1 < fkPos2) return -1; return 1; } } private static Logger logger = Logger.getLogger(SQLRelationship.class); private List<ColumnMapping> mappings = new ArrayList<ColumnMapping>(); /** * If true then one side of this relationship is being disconnected from * its parent table and the manager is making a call to remove the * relationship from the other parent. Then this relationship should not * go back to the first table and try to remove the relationship again * or else it will bounce back and forth between the tables like a bad * game of pong. */ private boolean isDisconnecting = false; /** * This is the imported side of this SQLRelationship. The imported key holds * a reference to this relationship. The imported key is added as a child to * the child table to let the relationship exist as a child of both the * parent and child tables. * <p> * It is important that the foreign key be final otherwise you can very * quickly and easily break the relationship as the two ends float apart or * you end up with two ends on one side pointing to the same object on the * other side. If you feel like you need to make this value non-final or * make a setter for it think hard about your reasoning or if there is a * different way. */ private SQLImportedKey foreignKey; /** * The enumeration of all referential integrity constraint checking * policies. */ public static enum Deferrability { /** * Indicates the constrain is deferrable, and checking is deferred by * default unless the current transaction has been set for immediate * constraint checking. */ INITIALLY_DEFERRED(5), /** * Indicates the constrain is deferrable, and checking is performed * immediately unless the current transaction has been set for deferred * constraint checking. */ INITIALLY_IMMEDIATE(6), /** * Indicates that the checking for this constraint must always be immediate * regardless of the current transaction setting. */ NOT_DEFERRABLE(7); /** * The JDBC code number for this deferrability policy. */ private final int code; private Deferrability(int code) { this.code = code; } /** * Returns the enumeration value associated with the given code number. * The code numbers are defined in the JDBC specification. * * @throws IllegalArgumentException if the given code number is not valid. */ public static Deferrability ruleForCode(int code) { for (Deferrability d : values()) { if (d.code == code) return d; } throw new IllegalArgumentException("No such deferrability code " + code); } /** * Returns the enumeration value associated with the given code number, * or the given default value if the given code number is not valid. * This method exists mainly for backward compatibility with old projects * where all the deferrability rules were defaulted to 0, which is an * invalid code. New code should normally be written to use {@link #ruleForCode(int)}, * which throws an exception when asked for an invalid code. */ public static Deferrability ruleForCode(int code, Deferrability defaultValue) { for (Deferrability d : values()) { if (d.code == code) return d; } return defaultValue; } /** * Returns the JDBC code number for this deferrability rule. */ public int getCode() { return code; } } /** * Enumeration of the various rules allowed for (foreign/imported/child) * columns when their parent value is updated or deleted. */ public static enum UpdateDeleteRule { /** * When parent value changes, child value should be modified to * match new parent value. */ CASCADE(DatabaseMetaData.importedKeyCascade), /** * Modifying or deleting the parent value should fail if * there are child records. This is different from {@link #NO_ACTION} * in that the constraint check will not be deferrable on some * platforms. */ RESTRICT(DatabaseMetaData.importedKeyRestrict), /** * The child value will be set to SQL NULL if the parent value * is modified or deleted. */ SET_NULL(DatabaseMetaData.importedKeySetNull), /** * Modifying or deleting the parent value should fail if * there are child records. This is different from {@link #RESTRICT} * in that the constraint checking will be deferrable on some platforms. * This is the default update and delete rule on most database platforms. */ NO_ACTION(DatabaseMetaData.importedKeyNoAction), /** * Modifying or deleting the parent value should cause the child * value to be set to its default. */ SET_DEFAULT(DatabaseMetaData.importedKeySetDefault); /** * The JDBC code for this update/delete rule. */ private final int code; private UpdateDeleteRule(int code) { this.code = code; } /** * Returns the update/delete rule associated with the given code number. * The code numbers are defined in the JDBC specification. * * @throws IllegalArgumentException if the given code number is not valid. */ public static UpdateDeleteRule ruleForCode(int code) { for (UpdateDeleteRule r : values()) { if (r.code == code) return r; } throw new IllegalArgumentException("No such update/delete rule code " + code); } /** * Returns the JDBC code number for this update/delete rule. */ public int getCode() { return code; } } /** * A convenience method for turning a list of SQLImported keys into a list * of their corresponding SQLRelationships */ public static List<SQLRelationship> getExportedKeys(List<SQLImportedKey> foreignKeys) { List<SQLRelationship> primaryKeys = new ArrayList<SQLRelationship>(); for (SQLImportedKey k : foreignKeys) { primaryKeys.add(k.getRelationship()); } return primaryKeys; } public static final int ZERO = 1; public static final int ONE = 2; public static final int MANY = 4; public static final int PKCOLUMN = 4; public static final int FKCOLUMN = 5; /** * The rule for what the DBMS should do to the child (imported) key value when its * parent table (exported) key value changes. */ protected UpdateDeleteRule updateRule = UpdateDeleteRule.NO_ACTION; /** * The rule for what the DBMS should do to the child (imported) key value when its * parent table (exported) row is deleted. */ protected UpdateDeleteRule deleteRule = UpdateDeleteRule.NO_ACTION; /** * The deferrability rule for constraint checking on this relationship. * Defaults to NOT_DEFERRABLE. */ protected Deferrability deferrability = Deferrability.NOT_DEFERRABLE; protected int pkCardinality; protected int fkCardinality; /** * Value should be true if this relationship is identifying, and false if * otherwise. * <p> * Here is our definition of identifying relationships and non-identifying * relationships (as discussed in the <a * href="http://groups.google.com/group/architect-developers/browse_thread/thread/d70e3e3ee3353f1"/> * Architect Developer's mailing list</a>). * <p> * An 'identifying' relationship is: A foreign key relationship in which the * whole primary key of the parent table is entirely contained in the * primary key of the child table. * <p> * A 'non-identifying' relationship is: A foreign key relationship in which * the whole primary key of the parent table is NOT entirely contained in * the primary key of the child table. */ protected boolean identifying; /** * This is the text for parent label of relationship. */ private String textForParentLabel = ""; /** * This is the text for child label of relationship. */ private String textForChildLabel = ""; public SQLRelationship() { pkCardinality = ONE; fkCardinality = ZERO | ONE | MANY; setName("New SQL Relationship"); setPopulated(true); foreignKey = new SQLImportedKey(this); } /** * NOTE: Magic MUST be disabled before calling this constructor. If not the * relationship will try to be attached between the parent table and a null * FK table throwing a NPE. * <p> * NOTE 2: No foreign key will be created with this constructor. After * calling this constructor you MUST set the SQLImportedKey before using it * or else it will only be half a relationship. */ @Constructor public SQLRelationship(@ConstructorParameter(parameterType=ParameterType.PROPERTY, propertyName="parent") SQLTable pkTable) { pkCardinality = ONE; fkCardinality = ZERO | ONE | MANY; setName("New SQL Relationship"); setPopulated(true); setParent(pkTable); } /** * A copy constructor that returns a copy of the provided SQLRelationship * with the following properties copied: * <li> Name </li> * <li> Identifying status </li> * <li> Update rule </li> * <li> Delete rule </li> * <li> Deferrability </li> * * @param relationshipToCopy * The SQLRelationship object to copy */ public SQLRelationship(SQLRelationship relationshipToCopy) throws SQLObjectException { this(); updateToMatch(relationshipToCopy); } @Override @Mutator public void setName(String name) { super.setName(name); setPhysicalName(name); } @Override public final void updateToMatch(SQLObject source) throws SQLObjectException { updateToMatch(source, false); } public final void updateToMatch(SQLObject source, boolean ignoreColumnMappings) throws SQLObjectException { SQLRelationship relationshipToCopy = (SQLRelationship) source; setName(relationshipToCopy.getName()); setIdentifying(relationshipToCopy.determineIdentifyingStatus()); setUpdateRule(relationshipToCopy.getUpdateRule()); setDeleteRule(relationshipToCopy.getDeleteRule()); setDeferrability(relationshipToCopy.getDeferrability()); for (Map.Entry<Class<? extends SQLObject>, Throwable> inaccessibleReason : source.getChildrenInaccessibleReasons().entrySet()) { setChildrenInaccessibleReason(inaccessibleReason.getValue(), inaccessibleReason.getKey(), false); } setTextForChildLabel(relationshipToCopy.getTextForChildLabel()); setTextForParentLabel(relationshipToCopy.getTextForParentLabel()); if (ignoreColumnMappings) return; List<ColumnMapping> columnsToRemove = new ArrayList<ColumnMapping>(getChildrenWithoutPopulating()); for (ColumnMapping newColMapping : relationshipToCopy.getChildrenWithoutPopulating()) { boolean foundColumn = false; for (int i = columnsToRemove.size() - 1; i >= 0; i--) { ColumnMapping existingMapping = columnsToRemove.get(i); SQLTable existingFKTable; String existingColName; if (existingMapping.getFkColumn() == null) { existingFKTable = existingMapping.getFkTable(); existingColName = existingMapping.getFkColName(); } else { existingFKTable = existingMapping.getFkColumn().getParent(); existingColName = existingMapping.getFkColumn().getName(); } SQLTable newColFKTable; String newColFKColName; if (newColMapping.getFkColumn() == null) { newColFKTable = newColMapping.getFkTable(); newColFKColName = newColMapping.getFkColName(); } else { newColFKTable = newColMapping.getFkColumn().getParent(); newColFKColName = newColMapping.getFkColumn().getName(); } if (existingMapping.getPkColumn().equals(newColMapping.getPkColumn()) && ((existingMapping.getFkColumn() != null && newColMapping.getFkColumn() != null && existingMapping.getFkColumn().equals(newColMapping.getFkColumn()) || (existingFKTable.equals(newColFKTable) && existingColName.equals(newColFKColName))))) { columnsToRemove.remove(existingMapping); foundColumn = true; break; } } if (!foundColumn) { addChild(newColMapping); } } for (ColumnMapping removeMe : columnsToRemove) { try { removeChild(removeMe); } catch (Exception e) { throw new SQLObjectException(e); } } } /** * Adds a counter to the end of the default column name until * it is unique in the given table. */ private static String generateUniqueColumnName(String colName, SQLTable table) throws SQLObjectException { if (table.getColumnByName(colName) == null) return colName; int count = 1; String uniqueName; do { uniqueName = colName + "_" + count; count++; } while (table.getColumnByName(uniqueName) != null); return uniqueName; } public void attachListeners() throws SQLObjectException { SQLPowerUtils.listenToHierarchy(getParent(), fkColumnUpdater); } private void detachListeners(){ if (getParent() != null) { SQLPowerUtils.unlistenToHierarchy(getParent(), fkColumnUpdater); } } @Mutator(constructorMutator=true) public void setPkTable(SQLTable pkTable) { if (getPkTable() != null && !getPkTable().equals(pkTable)) throw new IllegalArgumentException("Cannot set the parent table of relationship " + getName() + " to " + pkTable.getName() + " as it is already attached to " + getParent().getName()); SQLTable oldTable = getPkTable(); setParent(pkTable); firePropertyChange("pkTable", oldTable, pkTable); } @Transient @Mutator public void setFkTable(SQLTable fkTable) { if (getFkTable() != null && !getFkTable().equals(fkTable)) throw new IllegalArgumentException("Cannot set the child table of relationship " + getName() + " to " + fkTable.getName() + " as it is already attached to " + getFkTable().getName()); SQLTable oldTable = getFkTable(); foreignKey.setParent(fkTable); firePropertyChange("fkTable", oldTable, fkTable); } /** * Associates an {@link SQLRelationship} with the given {@link SQLTable} * objects. Also automatically generates the PK to FK column mapping if * autoGenerateMapping is set to true. * * @param pkTable * The parent table in this relationship. * @param fkTable * The child table in this relationship that contains the foreign * key. * @param autoGenerateMapping * Automatically generates the PK to FK column mapping if true * @throws SQLObjectException */ public void attachRelationship(SQLTable pkTable, SQLTable fkTable, boolean autoGenerateMapping) throws SQLObjectException { attachRelationship(pkTable, fkTable, autoGenerateMapping, true); } /** * Associates an {@link SQLRelationship} with the given {@link SQLTable} * objects. Also automatically generates the PK to FK column mapping if * autoGenerateMapping is set to true. * * @param pkTable * The parent table in this relationship. * @param fkTable * The child table in this relationship that contains the foreign * key. * @param autoGenerateMapping * Automatically generates the PK to FK column mapping if true * @param addToParents * If true the relationship and its imported key will be added to * their corresponding tables as children by this method. If * false the relationship will be fully setup but will not be an * actual child of the table and will have to be added after this * method call. * @throws SQLObjectException */ public void attachRelationship(SQLTable pkTable, SQLTable fkTable, boolean autoGenerateMapping, boolean addToParents) throws SQLObjectException { setFkTable(fkTable); try { setMagicEnabled(false); setParent(pkTable); } finally { setMagicEnabled(true); } attachRelationship(autoGenerateMapping, addToParents); } /** * Associates an {@link SQLRelationship} with the given {@link SQLTable} * objects. Also automatically generates the PK to FK column mapping if * autoGenerateMapping is set to true. The PK and FK tables must be set * already before calling this method. * * @param autoGenerateMapping * Automatically generates the PK to FK column mapping if true * @throws SQLObjectException */ private void attachRelationship(boolean autoGenerateMapping) throws SQLObjectException { attachRelationship(autoGenerateMapping, true); } /** * Associates an {@link SQLRelationship} with the given {@link SQLTable} * objects. Also automatically generates the PK to FK column mapping if * autoGenerateMapping is set to true. The PK and FK tables must be set * already before calling this method. * * @param autoGenerateMapping * Automatically generates the PK to FK column mapping if true * @param addToParents * If true the relationship and its imported key will be added to * their corresponding tables as children by this method. If * false the relationship will be fully setup but will not be an * actual child of the table and will have to be added after this * method call. You may not want to attach a relationship as a child * of tables to prevent events from being pushed to the object model * while the relationship is being setup. * @throws SQLObjectException */ private void attachRelationship(boolean autoGenerateMapping, boolean addToParents) throws SQLObjectException { if(getParent() == null) throw new NullPointerException("Null pkTable not allowed"); SQLTable fkTable = getFkTable(); if(fkTable == null) throw new NullPointerException("Null fkTable not allowed"); //Listeners detached so they don't get added twice when adding the //relationship to parent tables? detachListeners(); boolean alreadyExists = false; for (SQLRelationship r : getParent().getExportedKeysWithoutPopulating()) { if (r.getFkTable().equals(fkTable)) { alreadyExists = true; break; } } if (addToParents) { getParent().addChild(this); fkTable.addChild(foreignKey); } try { fkTable.setMagicEnabled(false); if (autoGenerateMapping) { // iterate over a copy of pktable's column list to avoid comodification // when creating a self-referencing table java.util.List<SQLColumn> pkColListCopy = new ArrayList<SQLColumn>(getParent().getColumns().size()); pkColListCopy.addAll(getParent().getColumns()); for (SQLColumn pkCol : pkColListCopy) { if (!pkCol.isPrimaryKey()) break; SQLColumn match = fkTable.getColumnByName(pkCol.getName()); SQLColumn fkCol = new SQLColumn(pkCol); if (getParent() == fkTable) { // self-reference should never hijack the PK! String colName = "Parent_" + fkCol.getName(); fkCol.setName(generateUniqueColumnName(colName, fkTable)); setIdentifying(false); } else if (match == null) { // no match, so we need to import this column from PK table fkCol.setName(generateUniqueColumnName(pkCol.getName(),fkTable)); } else { // does the matching column have a compatible data type? if (!alreadyExists && match.getType() == pkCol.getType() && match.getPrecision() == pkCol.getPrecision() && match.getScale() == pkCol.getScale()) { // column is an exact match, so we don't have to recreate it fkCol = match; } else { String colName = pkCol.getParent().getName() + "_" + pkCol.getName(); fkCol.setName(generateUniqueColumnName(colName,fkTable)); } } this.addMapping(pkCol, fkCol); } } realizeMapping(); this.attachListeners(); } finally { if ( fkTable != null ) { fkTable.setMagicEnabled(true); } } } /** * Takes the existing ColumnMapping children of this relationship, and ensures * that the FK Columns exist in the FK Table, and that they are in/out of the FK * table's primary key depending on whether or not this is an identifying relationship. * * @throws SQLObjectException If something goes terribly wrong */ private void realizeMapping() throws SQLObjectException { for (ColumnMapping m : getChildren(ColumnMapping.class)) { if (m.getFkColumn() == null) continue; //fk col has not yet been populated. if (logger.isDebugEnabled()) { logger.debug("realizeMapping: processing " + m); } SQLColumn fkCol = m.getFkColumn(); try { fkCol.setMagicEnabled(false); if (fkCol.getReferenceCount() == 0) fkCol.addReference(); // since we turned magic off, we have to insert the PK cols in // the correct position int insertIdx; if (identifying) { if (fkCol.getParent() == null || !fkCol.isPrimaryKey()) { logger.debug("realizeMapping: fkCol PK seq is null. Inserting at end of PK."); insertIdx = getFkTable().getPkSize(); } else { insertIdx = getFkTable().getColumnIndex(fkCol); logger.debug("realizeMapping: using existing fkCol PK seq " + insertIdx); } } else { if (fkCol.getParent() != null && fkCol.isPrimaryKey()) { insertIdx = getFkTable().getColumnIndex(fkCol); } else { insertIdx = getFkTable().getColumns().size(); } } fkCol.setAutoIncrement(false); // This might bump up the reference count (which would be // correct) getFkTable().addColumn(fkCol, insertIdx); logger.debug("realizeMapping: Added column '" + fkCol.getName() + "' at index " + insertIdx); if (fkCol.getReferenceCount() <= 0) throw new IllegalStateException("Created a column with 0 references!"); if (identifying && !fkCol.isPrimaryKey()) { getFkTable().addToPK(fkCol); } } finally { fkCol.setMagicEnabled(true); } } } /** * Fetches imported keys for the given table. (Imported keys are the * uniquely-indexed columns of other tables that are referenced by (imported * from) the given table). Nothing is added to the given table or any other * objects, although the returned SQLRelationship column mapping objects * will likely have references to columns in that table and others. It is * the caller's option whether or not to attach some, all, or none of the * returned SQLRelationship objects to the object model. All returned * objects can be safely thrown away simply by ignoring them, or kept by * invoking their {@link #attachRelationship(SQLTable, SQLTable, boolean)} * method. * <p> * Note that <code>table</code>'s database must be fully populated up to the * table level (the tables themselves can be unpopulated) before you call * this method; it requires that all referenced tables are represented by * in-memory SQLTable objects. The tables this table imports its keys from * will be populated as a side effect of this call. * <p> * Also note that the table may not have had its children populated at this * point. If this method is being called in a background thread the columns * and indices will be added to the table in the foreground thread. Any * section that requires the table to have the correct children and indices * needs to be run on the foreground thread to ensure the objects exist. * * @throws SQLObjectException * if a database error occurs or if the given table's parent * database is not marked as populated. */ static List<SQLRelationship> fetchExportedKeys(final SQLTable table, final SQLTable originalFkTable) throws SQLObjectException { final SQLDatabase db = table.getParentDatabase(); if (!db.isPopulated()) { throw new SQLObjectException("relationship.unpopulatedTargetDatabase"); } CachedRowSet crs = new CachedRowSet(); ResultSet tempRS = null; // just a temporary place for the live result set. use crs instead. Connection con = null; try { con = table.getParentDatabase().getConnection(); DatabaseMetaData dbmd = con.getMetaData(); tempRS = dbmd.getExportedKeys(table.getCatalogName(), table.getSchemaName(), table.getName()); crs.populate(tempRS); } catch (SQLException e) { throw new SQLObjectException("relationship.populate", e); } finally { try { if (tempRS != null) tempRS.close(); } catch (SQLException e) { logger.warn("Couldn't close imported keys result set", e); } try { if (con != null) con.close(); } catch (SQLException e) { logger.warn("Couldn't close connection", e); } } try { int currentKeySeq; List<SQLRelationship> newKeys = new LinkedList<SQLRelationship>(); logger.debug("search relationship for table:"+table.getCatalogName()+"."+ table.getSchemaName()+"."+ table.getName()); SQLRelationship r = null; while (crs.next()) { currentKeySeq = crs.getInt(9); final String pkCat = crs.getString(1); final String pkSchema = crs.getString(2); final String pkTableName = crs.getString(3); final String pkColName = crs.getString(4); final SQLTable parentTable = db.getTableByName(pkCat, pkSchema, pkTableName); final String fkCat = crs.getString(5); final String fkSchema = crs.getString(6); final String fkTableName = crs.getString(7); final String fkColName = crs.getString(8); final SQLTable fkTable = db.getTableByName(fkCat,fkSchema,fkTableName); //For performance we only care about the relationships that are being searched for //on this fk table. if (originalFkTable != null && originalFkTable != fkTable) { continue; } final int updateRule = crs.getInt(10); final int deleteRule = crs.getInt(11); final String fkName = crs.getString(12); final int deferrability = crs.getInt(14); if (currentKeySeq == 1) { r = new SQLRelationship(); final SQLRelationship finalRelation = r; Runnable fkTableRunner = new Runnable() { public void run() { if (fkTable == null) { // In case of cross-schema relationship fkTable is null as related schema populated later // so get the foreignKey table again SQLTable foreignKeyTable; try { foreignKeyTable = db.getTableByName(fkCat,fkSchema,fkTableName); } catch (NullPointerException e) { logger.error("FK table "+fkTableName +" of "+fkSchema+" is not populated yet.", e); throw e; } catch (SQLObjectException e) { throw new RuntimeException(e); } finalRelation.setFkTable(foreignKeyTable); } else { finalRelation.setFkTable(fkTable); } } }; try { table.getRunnableDispatcher().runInForeground(fkTableRunner); } catch (SessionNotFoundException e) { fkTableRunner.run(); } //must be done on current thread newKeys.add(r); } if (parentTable == null) { logger.error("addImportedRelationshipsToTable: Couldn't find exporting table " +pkCat+"."+pkSchema+"."+pkTableName +" in target database!"); continue; } final SQLRelationship relToModify = r; Runnable runner = new Runnable() { public void run() { if (!parentTable.isColumnsPopulated()) { throw new IllegalStateException("FK table " + parentTable + " is missing columns, cannot populate relationships."); } try { relToModify.setMagicEnabled(false); ColumnMapping m = new ColumnMapping(); relToModify.addMapping(m); relToModify.setParent(parentTable); if (relToModify.getParent() != table) { throw new IllegalStateException("fkTable did not match requested table"); } logger.debug("Looking for pk column '"+pkColName+"' in table '"+relToModify.getParent()+"'"); m.pkColumn = relToModify.getParent().getColumnByName(pkColName); if (m.pkColumn == null) { throw new SQLObjectException("relationship.populate.nullPkColumn"); } m.fkColumn = relToModify.getFkTable().getColumnByName(fkColName, false, false); if (m.fkColumn == null) { m.setFkColName(fkColName); m.setFkTable(relToModify.getFkTable()); } // column 9 (currentKeySeq) handled above relToModify.updateRule = UpdateDeleteRule.ruleForCode(updateRule); relToModify.deleteRule = UpdateDeleteRule.ruleForCode(deleteRule); relToModify.setName(fkName); try { relToModify.deferrability = Deferrability.ruleForCode(deferrability); } catch (IllegalArgumentException ex) { logger.warn("Invalid code when reverse engineering" + " relationship. Defaulting to NOT_DEFERRABLE.", ex); relToModify.deferrability = Deferrability.NOT_DEFERRABLE; } } catch (SQLObjectException e) { throw new SQLObjectRuntimeException(e); } finally { relToModify.setMagicEnabled(true); } } }; try { table.getRunnableDispatcher().runInForeground(runner); } catch (SessionNotFoundException e) { runner.run(); } } return newKeys; } catch (SQLException e) { throw new SQLObjectException("relationship.populate", e); } finally { try { if (crs != null) crs.close(); } catch (SQLException e) { logger.warn("Couldn't close resultset", e); } } } @NonProperty public ColumnMapping getMappingByPkCol(SQLColumn pkcol) { for (ColumnMapping m : mappings) { if (m.pkColumn == pkcol) { return m; } } return null; } public void reassignMappingsByPkCol(SQLColumn pkCol) { for (ColumnMapping m : mappings) { if (m.pkColumn != null && m.pkColumn != pkCol && m.pkColumn.getUUID().equals(pkCol.getUUID())) { m.setFkColumn(pkCol); } } } public boolean containsPkColumn(SQLColumn col) { return getMappingByPkCol(col) != null; } @NonProperty public ColumnMapping getMappingByFkCol(SQLColumn fkcol) { for (ColumnMapping m : mappings) { if (m.fkColumn == fkcol) { return m; } } return null; } public boolean containsFkColumn(SQLColumn col) { return getMappingByFkCol(col) != null; } public void reassignMappingsByFkCol(SQLColumn fkCol) { for (ColumnMapping m : mappings) { if (m.fkColumn != null && m.fkColumn != fkCol && m.fkColumn.getUUID().equals(fkCol.getUUID())) { m.setFkColumn(fkCol); } } } public String printKeyColumns(int keyType) { StringBuffer s = new StringBuffer(); int i = 0; for (ColumnMapping cm : mappings) { if ( i++ > 0 ) s.append(","); if ( keyType == PKCOLUMN ) s.append(cm.getPkColumn().getName()); else s.append(cm.getFkColumn().getName()); } return s.toString(); } @Override protected void addChildImpl(SPObject child, int index) { if (child instanceof ColumnMapping) { addMapping((ColumnMapping) child, index); } else { throw new IllegalArgumentException("The child " + child.getName() + " of type " + child.getClass() + " is not a valid child type of " + getClass() + "."); } } /** * Convenience method for adding a SQLRelationship.ColumnMapping * child to this relationship. This will not increase the reference * count to the columns in the mapping. * @throws SQLObjectException */ public void addMapping(SQLColumn pkColumn, SQLColumn fkColumn) throws SQLObjectException { ColumnMapping cmap = new ColumnMapping(); cmap.setPkColumn(pkColumn); cmap.setFkColumn(fkColumn); logger.debug("add column mapping: "+pkColumn.getParent()+"." + pkColumn.getName() + " to " + fkColumn.getParent()+"."+fkColumn.getName() ); addMapping(cmap); } public void addMapping(ColumnMapping mapping) { addMapping(mapping, mappings.size()); } public void addMapping(ColumnMapping mapping, int index) { mappings.add(index, mapping); mapping.setParent(this); fireChildAdded(ColumnMapping.class, mapping, index); } public String toString() { return getShortDisplayName(); } // ------------------ SQLObject Listener --------------------- /** * This listener will update the fk columns of the fk table based on the * mappings in this relationship when there are property changes to the * columns in the pk table. This listener only needs to be attached to the * pk table. */ protected SPListener fkColumnUpdater = new ForeignKeyColumnUpdaterPoolingSPListener(this); // ---------------------- Former RelationshipManager ------------------------ /** * When a child is added to the parent table this method must be * called to fix the relationship mappings. * * @param col * The column just added to the parent table's relationship. */ public void fixMappingNewChildInParent(SQLColumn col) { if (!getParent().isMagicEnabled()){ logger.debug("Magic disabled; not fixing mapping for " + col); return; } try { if (col.isPrimaryKey()) { ensureInMapping(col); } else { ensureNotInMapping(col); } } catch (SQLObjectException ex) { logger.warn("Couldn't add/remove mapped FK columns", ex); } } /** * Must be called when this relationship or its foreignKey is being * removed from its parent table. * * @param isRelationship * If true the relationship is being removed from the pk * table and the associated {@link SQLImportedKey} must be * removed from the fk table. If false the * {@link SQLImportedKey} is being removed from the fk table * and the containing {@link SQLRelationship} must be removed * from the pk table. */ public void disconnectRelationship(boolean isRelationship) { if (!getParent().isMagicEnabled()){ logger.debug("Magic disabled; ignoring relationship remove " + SQLRelationship.this); return; } if (isDisconnecting) return; try { detachListeners(); try { isDisconnecting = true; if (isRelationship) { SQLImportedKey fk = foreignKey; fk.getParent().removeChild(fk); } else { getParent().removeChild(SQLRelationship.this); } } catch (ObjectDependentException e1) { throw new RuntimeException(e1); // This should not happen } logger.debug("Removing references for mappings: "+getChildren()); // references to fk columns are removed in reverse order in case // this relationship is reconnected in the future. (if not removed // in reverse order, the PK sequence numbers will change as each // mapping is removed and the subsequent column indexes shift down) List<ColumnMapping> mappings = new ArrayList<ColumnMapping>(getChildren(ColumnMapping.class)); Collections.sort(mappings, Collections.reverseOrder(new ColumnMappingFKColumnOrderComparator())); for (ColumnMapping cm : mappings) { logger.debug("Removing reference to fkcol "+ cm.getFkColumn()); if (cm.getFkColumn() != null) { cm.getFkColumn().removeReference(); } } } finally { isDisconnecting = false; } } /** * Must be called when a column is being removed from its parent table. * While this can be done from both pk and fk table it appears to only * have effect for the pk table. * * @param col * The column removed from the table. */ public void fixMappingChildRemoved(SQLColumn col) { if (!col.getParent().isMagicEnabled()){ logger.debug("Magic disabled; not fixing mapping for " + col); return; } try { ensureNotInMapping(col); } catch (SQLObjectException ex) { logger.warn("Couldn't remove mapped FK columns", ex); } } /** * To be called when the pk or fk table is removed from its parent. */ public void tableDisconnected() { if (!getParent().isMagicEnabled() || !getFkTable().isMagicEnabled()){ logger.debug("Magic disabled; ignoring table disconnect that would " + "clean up relationship " + SQLRelationship.this); return; } getParent().removeExportedKey(SQLRelationship.this); } // XXX this code serves essentially the same purpose as the loop in realizeMapping(). // We should refactor that method to use this one as a subroutine, and at that // time, ensure the special cases in both places are preserved. // (if there is a special case in there that's not here, it's probably a bug) protected void ensureInMapping(SQLColumn pkcol) throws SQLObjectException { if (!containsPkColumn(pkcol)) { if (logger.isDebugEnabled()) { logger.debug("ensureInMapping("+getName()+"): Adding " +pkcol.getParent().getName()+"."+pkcol.getName() +" to mapping"); } SQLColumn fkcol; if (pkcol.getParent().equals(getFkTable())) { // self-reference! must create new column! fkcol = new SQLColumn(pkcol); fkcol.setName(generateUniqueColumnName("Parent_"+pkcol.getName(), pkcol.getParent())); } else { fkcol = getFkTable().getColumnByName(pkcol.getName()); if (fkcol == null) fkcol = new SQLColumn(pkcol); } if (identifying && getParent() != getFkTable()) { //Each inserted column will be placed: //1: just above the next column in the fk index that we know about and can find. //2: just below the previous column in the fk index that we know about and can find. //3: at the bottom of the fk index. int index = -1; int pkIndex = getPkTable().getColumnIndex(pkcol); for (int i = pkIndex + 1; i < getPkTable().getPkSize(); i++) { for (int j = 0; j < getFkTable().getPkSize(); j++) { if (getFkTable().getColumn(j).getName().equals(getPkTable().getColumn(i).getName())) { index = j; break; } } if (index >= 0) break; } if (index == -1) { for (int i = pkIndex - 1; i >=0; i--) { for (int j = 0; j < getFkTable().getPkSize(); j++) { if (getFkTable().getColumn(j).getName().equals(getPkTable().getColumn(i).getName())) { index = j + 1; break; } } if (index >= 0) break; } } if (index == -1) { index = getFkTable().getPkSize(); } // this either adds the new column or bumps up the refcount on existing col getFkTable().addColumn(fkcol, index); getFkTable().addToPK(fkcol); } else { // this either adds the new column or bumps up the refcount on existing col getFkTable().addColumn(fkcol); } logger.debug("ensureInMapping("+getName()+"): added fkcol " + fkcol); fkcol.setAutoIncrement(false); addMapping(pkcol, fkcol); } } /** * Ensures there is no mapping for pkcol in this relationship. * If there was, it is removed along with the column that may * have been pushed into the relationship's fkTable. */ protected void ensureNotInMapping(SQLColumn pkcol) throws SQLObjectException { logger.debug("Removing "+pkcol.getParent()+"."+pkcol+" from mapping"); if (containsPkColumn(pkcol)) { ColumnMapping m = getMappingByPkCol(pkcol); try { removeChild(m); } catch (IllegalArgumentException e) { throw new SQLObjectException(e); } catch (ObjectDependentException e) { throw new SQLObjectException(e); } try { // XXX no magic here? this is suspect m.getFkColumn().setMagicEnabled(false); m.getFkColumn().removeReference(); } finally { m.getFkColumn().setMagicEnabled(true); } } } // ---------------------- SQLRelationship SQLObject support ------------------------ /** * Returns the foreign key name. */ @Transient @Accessor public String getShortDisplayName() { return getName(); } /** * This class is not a lazy-loading class. This call does nothing. */ protected void populateImpl() { // nothing to do. } /** * Returns true. */ @Transient @Accessor public boolean isPopulated() { return true; } // ----------------- accessors and mutators ------------------- @Accessor(isInteresting=true) public UpdateDeleteRule getUpdateRule() { return this.updateRule; } @Mutator public void setUpdateRule(UpdateDeleteRule rule) { UpdateDeleteRule oldRule = updateRule; updateRule = rule; firePropertyChange("updateRule", oldRule, rule); } @Accessor(isInteresting=true) public UpdateDeleteRule getDeleteRule() { return this.deleteRule; } @Mutator public void setDeleteRule(UpdateDeleteRule rule) { UpdateDeleteRule oldRule = deleteRule; deleteRule = rule; firePropertyChange("deleteRule", oldRule, rule); } @Accessor(isInteresting=true) public Deferrability getDeferrability() { return this.deferrability; } @Mutator public void setDeferrability(Deferrability argDeferrability) { if (argDeferrability == null) { throw new NullPointerException("Deferrability policy must not be null"); } Deferrability oldDefferability = this.deferrability; this.deferrability = argDeferrability; firePropertyChange("deferrability",oldDefferability,argDeferrability); } /** * Gets the value of pkCardinality * * @return the value of pkCardinality */ @Accessor public int getPkCardinality() { return this.pkCardinality; } /** * Sets the value of pkCardinality * * @param argPkCardinality Value to assign to this.pkCardinality */ @Mutator public void setPkCardinality(int argPkCardinality) { int oldPkCardinality = this.pkCardinality; this.pkCardinality = argPkCardinality; firePropertyChange("pkCardinality",oldPkCardinality,argPkCardinality); } /** * Gets the value of fkCardinality * * @return the value of fkCardinality */ @Accessor public int getFkCardinality() { return this.fkCardinality; } /** * Sets the value of fkCardinality * * @param argFkCardinality Value to assign to this.fkCardinality */ @Mutator public void setFkCardinality(int argFkCardinality) { int oldFkCardinality = this.fkCardinality; this.fkCardinality = argFkCardinality; firePropertyChange("fkCardinality",oldFkCardinality,argFkCardinality); } /** * Gets the value of identifying * * @return the value of identifying */ @Accessor(isInteresting=true) public boolean isIdentifying() { return this.identifying; } /** * Sets the value of identifying, and moves the FK columns into or * out of the FK Table's primary key as appropriate. * <p> * XXX Does anything in this method actually throw a SQLObjectException? * * @param argIdentifying Value to assign to this.identifying */ @Mutator public void setIdentifying(boolean argIdentifying) throws SQLObjectException { try { fireTransactionStarted("Setting " + getName() + " to be identifying: " + argIdentifying); boolean oldIdentifying = this.identifying; if (identifying != argIdentifying) { identifying = argIdentifying; if (identifying) { firePropertyChange("identifying", oldIdentifying, argIdentifying); if (isMagicEnabled()) { for (ColumnMapping m : getChildren(ColumnMapping.class)) { if (m.getFkColumn() != null && !m.getFkColumn().isPrimaryKey()) { getFkTable().addToPK(m.getFkColumn()); } } } } else { if (isMagicEnabled()) { for (ColumnMapping m : getChildren(ColumnMapping.class)) { if (m.getFkColumn() != null && m.getFkColumn().isPrimaryKey()) { getFkTable().moveAfterPK(m.getFkColumn()); } } } firePropertyChange("identifying", oldIdentifying, argIdentifying); } } fireTransactionEnded(); } catch (RuntimeException e) { fireTransactionRollback(e.getMessage()); throw e; } } @Accessor public SQLTable getParent() { return (SQLTable) super.getParent(); } /** * Because we constrained the return type on getParent there needs to be a * setter that has the same constraint otherwise the reflection in the undo * events will not find a setter to match the getter and won't be able to * undo parent property changes. */ @Mutator public void setParent(SQLTable parent) { setParentHelper(parent); } @Accessor public SQLTable getPkTable() { return getParent(); } @Transient @Accessor public SQLImportedKey getForeignKey() { return foreignKey; } @Transient @Mutator public void setForeignKey(SQLImportedKey newKey) { SQLImportedKey oldKey = foreignKey; this.foreignKey = newKey; firePropertyChange("foreignKey", oldKey, newKey); } @Mutator public void setParent(SPObject parent) { setParentHelper(parent); } /** * See the comment on {@link #setParent(SQLTable)} for why this method * exists if it seems goofy. */ private void setParentHelper(SPObject parent) { SPObject oldVal = getParent(); super.setParent(parent); if (parent != null) { if (isMagicEnabled() && parent != oldVal) { try { attachRelationship(true); } catch (SQLObjectException e) { throw new RuntimeException(e); } } else { //Column manager is removed first in case it has already been //added before. One case is when the relationship is being re-added //to the same table it was removed from by the undo system. SQLPowerUtils.unlistenToHierarchy(getParent(), fkColumnUpdater); SQLPowerUtils.listenToHierarchy(getParent(), fkColumnUpdater); } } } @Transient @Accessor public SQLTable getFkTable() { if (foreignKey != null) { return foreignKey.getParent(); } else { return null; } } /** * This class acts a wrapper around a SQLRelationship. It should be added to * the foreign key table as a child, and is depended on by the Relationship. */ public static class SQLImportedKey extends SQLObject { private final SQLRelationship relationship; /** * Listens for changes to the properties of the relationship this imported key * is attached to. This keeps the properties of the two relationship ends the * same where necessary. */ private final SPListener relationshipPropertyListener = new AbstractSPListener() { @Override public void propertyChanged(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("name")) { setName((String) evt.getNewValue()); } else if (evt.getPropertyName().equals("populated")) { setPopulated((Boolean) evt.getNewValue()); } } }; /** * This listener is for when an fk-column is moved from one index to * another. The reference of the column could change, so this will * reassign all column mappings to the new reference by comparing UUIDs. */ private final SPListener fkTableListener = new AbstractSPListener() { @Override /* * This is a cleaner/better fix than the one from commit 1466, but I * am not sure what the problem was, as the mappings seem to be * reassigned fine without this listener. */ public void childAdded(SPChildEvent e) { if (e.getChild() instanceof SQLColumn) { relationship.reassignMappingsByFkCol((SQLColumn) e.getChild()); } } }; @Constructor public SQLImportedKey( @ConstructorParameter(propertyName="relationship") SQLRelationship relationship) { super(); this.relationship = relationship; setName(relationship.getName()); setPopulated(relationship.isPopulated()); relationship.addSPListener(relationshipPropertyListener); relationship.setForeignKey(this); } @Override public SQLTable getParent() { return (SQLTable) super.getParent(); } /** * Because we constrained the return type on getParent there needs to be a * setter that has the same constraint otherwise the reflection in the undo * events will not find a setter to match the getter and won't be able to * undo parent property changes. */ @Mutator public void setParent(SQLTable parent) { if (getParent() != null) { getParent().removeSPListener(fkTableListener); } super.setParent(parent); if (parent != null) { parent.addSPListener(fkTableListener); } } @Override public List<? extends SQLObject> getChildrenWithoutPopulating() { return Collections.emptyList(); } @Override public String getShortDisplayName() { if (relationship != null) { return relationship.getShortDisplayName(); } else { return getName(); } } @Override protected void populateImpl() throws SQLObjectException { //no-op } @Override protected boolean removeChildImpl(SPObject child) { return false; } public List<Class<? extends SPObject>> getAllowedChildTypes() { return Collections.emptyList(); } public List<? extends SPObject> getDependencies() { return Collections.emptyList(); } public void removeDependency(SPObject dependency) { throw new UnsupportedOperationException("Need to decide the correct " + "dependency of this object."); } @Accessor public SQLRelationship getRelationship() { return relationship; } @Override public String toString() { return getShortDisplayName(); } @Override public final void updateToMatch(SQLObject source) throws SQLObjectException { //Do nothing, this is handled by the SQLRelationship } /** * The equals method for {@link SQLImportedKey} needs to keep in line * with {@link SQLRelationship}. */ @Override public boolean equals(Object obj) { if (obj instanceof SQLImportedKey) { SQLImportedKey key = (SQLImportedKey) obj; return (((getName() == null && key.getName() == null) || getName().equals(key.getName())) && relationship.equals(key.getRelationship())); } return false; } @Override public int hashCode() { int result = 17; result = 31 * result + (getName() == null? 0 : getName().hashCode()); result = 31 * relationship.hashCode(); return result; } } // -------------------------- COLUMN MAPPING ------------------------ public static class ColumnMapping extends SQLObject { /** * Defines an absolute ordering of the child types of this class. */ public static final List<Class<? extends SPObject>> allowedChildTypes = Collections.emptyList(); protected SQLColumn pkColumn; protected SQLColumn fkColumn; /** * If the fk table has not been populated the column mapping will point * to the column in the fk table by table reference and column name. * Then when the table is actually populated the fkColumn property will * be set. * <p> * If the fkColumn is already populated it should be used over this value. */ private SQLTable fkTable; /** * Holds the name of the fk table column this mapping should connect if * the table does not have columns created yet. * @see #fkTable */ private String fkColName; /** * A temporary hack to signal that the column mapping is being loaded * and it should add a reference to the fkColumn when it is set on this * object to properly complete the loading. It would be better if we * changed how columns signaled that they had multiple references * rather than properly maintaining the reference count. */ private boolean loading; /** * This listener will be attached to the fkTable if the fkTable has not * had its columns populated yet. This will let the column mapping update * correctly when the fkTable gets populated. */ private final SPListener fkTableListener = new AbstractSPListener() { public void childAdded(SPChildEvent e) { if (e.getChild() instanceof SQLColumn && e.getChild().getName().equals(fkColName)) { setFkColumn((SQLColumn) e.getChild()); } }; }; public ColumnMapping() { setName("Column Mapping"); setPopulated(true); } @Constructor public ColumnMapping(@ConstructorParameter(parameterType=ParameterType.PROPERTY, propertyName="pkColumn") SQLColumn pkColumn) { this(); setPkColumn(pkColumn); pkColumn.addReference(); loading = true; } /** * Gets the value of pkColumn * * @return the value of pkColumn */ @Accessor public SQLColumn getPkColumn() { return this.pkColumn; } /** * Sets the value of pkColumn * * @param argPkColumn Value to assign to this.pkColumn */ @Mutator public void setPkColumn(SQLColumn argPkColumn) { SQLColumn oldPK = pkColumn; this.pkColumn = argPkColumn; firePropertyChange("pkColumn", oldPK, argPkColumn); } /** * Gets the value of fkColumn * * @return the value of fkColumn */ @Accessor public SQLColumn getFkColumn() { if (fkColumn == null && fkColName != null && fkTable != null) { try { setFkColumn(fkTable.getColumnByName(fkColName)); } catch (SQLObjectException e) { throw new RuntimeException(e); } } return this.fkColumn; } /** * Sets the value of fkColumn * * @param argFkColumn Value to assign to this.fkColumn */ @Mutator public void setFkColumn(SQLColumn argFkColumn) { try { begin("Setting column mapping fk column."); SQLColumn oldFK = this.fkColumn; this.fkColumn = argFkColumn; firePropertyChange("fkColumn", oldFK, argFkColumn); if (fkColumn != null) { setFkTable(null); setFkColName(null); } if (loading) { fkColumn.addReference(); loading = false; } commit(); } catch (RuntimeException e) { rollback(e.getMessage()); throw e; } } public String toString() { return getShortDisplayName(); } // ---------------------- ColumnMapping SQLObject support ------------------------ /** * Returns the table that holds the primary keys (the imported table). */ @Accessor public SQLRelationship getParent() { return (SQLRelationship) super.getParent(); } /** * Because we constrained the return type on getParent there needs to be a * setter that has the same constraint otherwise the reflection in the undo * events will not find a setter to match the getter and won't be able to * undo parent property changes. */ @Mutator public void setParent(SQLRelationship parent) { super.setParent(parent); } /** * Returns the table and column name of the pkColumn. */ @Transient @Accessor public String getShortDisplayName() { if (pkColumn == null || fkColumn == null) return "Incomplete mapping"; String fkTableName = null; if (fkColumn == null && fkTable != null) { fkTableName = fkTable.getName(); } else if (fkColumn.getParent() != null) { fkTableName = fkColumn.getParent().getName(); } String fkColumnName = null; if (fkColumn == null && fkColName != null) { fkColumnName = fkColName; } else if (fkColumn != null) { fkColumnName = fkColumn.getName(); } return pkColumn.getName() + " - " + fkTableName + "." + fkColumnName; } /** * This class is not a lazy-loading class. This call does nothing. */ protected void populateImpl() throws SQLObjectException { return; } /** * Returns true. */ @Transient @Accessor public boolean isPopulated() { return true; } @Override public boolean equals(Object obj) { if (obj instanceof ColumnMapping) { ColumnMapping cmap = (ColumnMapping) obj; return fkColumn == cmap.fkColumn && pkColumn == cmap.pkColumn && fkTable == cmap.fkTable && fkColName == cmap.fkColName; } return false; } @Override public int hashCode() { int result = 17; result = result * 31 + (fkColumn == null? 0 : fkColumn.hashCode()); result = result * 31 + (pkColumn == null? 0 :pkColumn.hashCode()); result = result * 31 + (fkTable == null? 0 : fkTable.hashCode()); result = result * 31 + (fkColName == null? 0 :fkColName.hashCode()); return result; } @Override public List<? extends SQLObject> getChildrenWithoutPopulating() { return Collections.emptyList(); } @Override protected boolean removeChildImpl(SPObject child) { return false; } public List<? extends SPObject> getDependencies() { List<SPObject> dependencies = new ArrayList<SPObject>(); dependencies.add(getFkColumn()); dependencies.add(getPkColumn()); return dependencies; } public void removeDependency(SPObject dependency) { if (dependency == getFkColumn() || dependency == getPkColumn()) { getParent().removeColumnMapping(this); } } public List<Class<? extends SPObject>> getAllowedChildTypes() { return allowedChildTypes; } @Mutator public void setFkTable(SQLTable fkTable) { SQLTable oldTable = this.fkTable; this.fkTable = fkTable; if (oldTable != null) { oldTable.removeSPListener(fkTableListener); } if (fkTable != null) { fkTable.addSPListener(fkTableListener); } firePropertyChange("fkTable", oldTable, fkTable); } @Accessor public SQLTable getFkTable() { return fkTable; } @Mutator public void setFkColName(String fkColName) { String oldName = this.fkColName; this.fkColName = fkColName; firePropertyChange("fkColName", oldName, fkColName); } @Accessor public String getFkColName() { return fkColName; } } /** * Throws a column locked exception if col is in a columnmapping of this relationship * * @param col * @throws LockedColumnException */ public void checkColumnLocked(SQLColumn col) throws LockedColumnException { for (SQLRelationship.ColumnMapping cm : getChildren(ColumnMapping.class)) { if (cm.getFkColumn() == col) { throw new LockedColumnException(this,col); } } } /** * Some SQLRelationship objects may not have their {@link #identifying} * property set properly which is particularly the case then creating * SQLRelationships for source database objects and then reverse * engineering, so this method will determine for certain if a relationship * is identifying or non-identifying. This is currently primarily being used * for determining the identifying status of reverse-engineered * relationships. * * @return True if this SQLRelationship is identifying. False if it is * non-identifying. */ public boolean determineIdentifyingStatus() throws SQLObjectException { if (getPkTable().getPkSize() > getFkTable().getPkSize()) return false; List<ColumnMapping> columnMappings = getChildren(ColumnMapping.class); SQLIndex pkTablePKIndex = getPkTable().getPrimaryKeyIndex(); if (pkTablePKIndex == null) return false; List<Column> pkColumns = pkTablePKIndex.getChildren(Column.class); for (Column col: pkColumns) { boolean colIsInFKTablePK = false; for (ColumnMapping mapping: columnMappings) { if (mapping.getPkColumn().equals(col.getColumn()) && mapping.getFkColumn().isPrimaryKey()) { colIsInFKTablePK = true; break; } } if (colIsInFKTablePK == false) return false; } return true; } public static SQLRelationship createRelationship(SQLTable pkTable, SQLTable fkTable, boolean identifying) throws SQLObjectException { SQLRelationship model = new SQLRelationship(); // XXX: need to ensure uniqueness of setName(), but // to_identifier should take care of this... StringBuilder sb = new StringBuilder(); if (pkTable.getPhysicalName() == null || pkTable.getPhysicalName().trim().equals("")) { sb.append(pkTable.getName()); } else { sb.append(pkTable.getPhysicalName()); } sb.append("_"); if (fkTable.getPhysicalName() == null || fkTable.getPhysicalName().trim().equals("")) { sb.append(fkTable.getName()); } else { sb.append(fkTable.getPhysicalName()); } sb.append("_fk"); Set<String> rel = new HashSet<String>(); SPObject tableParent = pkTable.getParent(); for(SQLTable tbl : tableParent.getChildren(SQLTable.class)) { for(SQLRelationship r : tbl.getChildren(SQLRelationship.class)) { rel.add(r.getPhysicalName()); } } if (rel.contains(sb.toString())) { int i = 1; while (rel.contains(sb.toString() + Integer.toString(i))) { i++; } sb.append(i); } model.setName(sb.toString()); model.getForeignKey().setName(sb.toString()); model.setIdentifying(identifying); model.attachRelationship(pkTable,fkTable,true); return model; } @Mutator public void setTextForParentLabel(String textForParentLabel) { String oldVal = this.textForParentLabel; this.textForParentLabel = textForParentLabel; firePropertyChange("textForParentLabel", oldVal, textForParentLabel); } @Accessor public String getTextForParentLabel() { return textForParentLabel; } @Mutator public void setTextForChildLabel(String textForChildLabel) { String oldVal = this.textForChildLabel; this.textForChildLabel = textForChildLabel; firePropertyChange("textForChildLabel", oldVal, textForChildLabel); } @Accessor public String getTextForChildLabel() { return textForChildLabel; } @Override public List<ColumnMapping> getChildrenWithoutPopulating() { return Collections.unmodifiableList(new ArrayList<ColumnMapping>(mappings)); } @Override protected boolean removeChildImpl(SPObject child) { if (child instanceof ColumnMapping) { return removeColumnMapping((ColumnMapping) child); } else { throw new IllegalArgumentException("Cannot remove children of type " + child.getClass() + " from " + getName()); } } public boolean removeColumnMapping(ColumnMapping child) { if (isMagicEnabled() && child.getParent() != this) { throw new IllegalStateException("Cannot remove child " + child.getName() + " of type " + child.getClass() + " as its parent is not " + getName() + "." + " The parent is " + child.getParent()); } int index = mappings.indexOf(child); if (index != -1) { mappings.remove(index); fireChildRemoved(SQLTable.class, child, index); child.setParent(null); return true; } return false; } public List<? extends SPObject> getDependencies() { return Collections.singletonList(foreignKey); } public void removeDependency(SPObject dependency) { for (SQLObject child : getChildren()) { child.removeDependency(dependency); } } public List<Class<? extends SPObject>> getAllowedChildTypes() { return allowedChildTypes; } }