/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.mappingsmodel.db; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Vector; import org.eclipse.persistence.tools.workbench.mappingsmodel.MWModel; import org.eclipse.persistence.tools.workbench.mappingsmodel.ProblemConstants; import org.eclipse.persistence.tools.workbench.mappingsmodel.handles.MWHandle; import org.eclipse.persistence.tools.workbench.mappingsmodel.handles.MWTableHandle; import org.eclipse.persistence.tools.workbench.mappingsmodel.handles.MWHandle.NodeReferenceScrubber; import org.eclipse.persistence.tools.workbench.mappingsmodel.spi.db.ExternalForeignKey; import org.eclipse.persistence.tools.workbench.mappingsmodel.spi.db.ExternalForeignKeyColumnPair; import org.eclipse.persistence.tools.workbench.utility.CollectionTools; import org.eclipse.persistence.tools.workbench.utility.iterators.CloneIterator; import org.eclipse.persistence.tools.workbench.utility.node.Node; import org.eclipse.persistence.descriptors.ClassDescriptor; import org.eclipse.persistence.mappings.OneToOneMapping; import org.eclipse.persistence.oxm.XMLDescriptor; import org.eclipse.persistence.oxm.mappings.XMLCompositeCollectionMapping; import org.eclipse.persistence.oxm.mappings.XMLCompositeObjectMapping; import org.eclipse.persistence.oxm.mappings.XMLDirectMapping; import org.eclipse.persistence.tools.schemaframework.ForeignKeyConstraint; /** * A reference describes a foreign-key relationship from a "source" table * (the reference's "parent") to a "target" table. The foreign-key constraint * can either be real ("on database") or virtual (only implied by the joins * performed by TopLink). */ public final class MWReference extends MWModel { /** the name should never be null or empty */ private volatile String name; public static final String NAME_PROPERTY = "name"; private MWTableHandle targetTableHandle; public static final String TARGET_TABLE_PROPERTY = "targetTable"; private Collection columnPairs; public static final String COLUMN_PAIRS_COLLECTION = "columnPairs"; /** * indicate whether the reference is an actual database constraint or * only used by TopLink for joining */ private volatile boolean onDatabase; public static final String ON_DATABASE_PROPERTY = "onDatabase"; // ********** constructors ********** /** * Default constructor - for TopLink use only. */ private MWReference() { super(); } MWReference(MWTable parent, String name, MWTable targetTable) { super(parent); this.name = name; this.targetTableHandle.setTable(targetTable); } // ********** initialization ********** /** * initialize persistent state */ protected void initialize(Node parent) { super.initialize(parent); this.targetTableHandle = new MWTableHandle(this, this.buildTargetTableScrubber()); this.columnPairs = new Vector(); this.onDatabase = false; } // ********** accessors ********** public MWTable getSourceTable() { return (MWTable) this.getParent(); } public String getName() { return this.name; } public void setName(String name) { this.getSourceTable().checkReferenceName(name); Object old = this.name; this.name = name; this.firePropertyChanged(NAME_PROPERTY, old, name); if (this.attributeValueHasChanged(old, name)) { this.getProject().nodeRenamed(this); } } public MWTable getTargetTable() { return this.targetTableHandle.getTable(); } /** * if the target table changes, we clear out all the * column pairs, because they are now invalid */ public void setTargetTable(MWTable targetTable) { Object old = this.targetTableHandle.getTable(); this.targetTableHandle.setTable(targetTable); this.firePropertyChanged(TARGET_TABLE_PROPERTY, old, targetTable); if (this.attributeValueHasChanged(old, targetTable)) { this.clearColumnPairs(); } } public boolean isOnDatabase() { return this.onDatabase; } public void setOnDatabase(boolean onDatabase) { boolean old = this.onDatabase; this.onDatabase = onDatabase; this.firePropertyChanged(ON_DATABASE_PROPERTY, old, onDatabase); } // ********** column pairs public Iterator columnPairs() { return new CloneIterator(this.columnPairs) { protected void remove(Object current) { MWReference.this.removeColumnPair((MWColumnPair) current); } }; } public int columnPairsSize() { return this.columnPairs.size(); } public MWColumnPair addColumnPair(MWColumn sourceColumn, MWColumn targetColumn) { if (sourceColumn.getTable() != this.getSourceTable()) { throw new IllegalArgumentException("invalid source column: " + sourceColumn); } if (targetColumn.getTable() != this.getTargetTable()) { throw new IllegalArgumentException("invalid target column: " + targetColumn); } return this.addColumnPair(new MWColumnPair(this, sourceColumn, targetColumn)); } private MWColumnPair addColumnPair(MWColumnPair columnPair) { this.addItemToCollection(columnPair, this.columnPairs, COLUMN_PAIRS_COLLECTION); return columnPair; } public void removeColumnPair(MWColumnPair columnPair) { this.removeItemFromCollection(columnPair, this.columnPairs, COLUMN_PAIRS_COLLECTION); } public void removeColumnPairs(Iterator pairs) { while (pairs.hasNext()) { this.removeColumnPair((MWColumnPair) pairs.next()); } } public void removeColumnPairs(Collection pairs) { this.removeColumnPairs(pairs.iterator()); } private void clearColumnPairs() { for (Iterator stream = this.columnPairs(); stream.hasNext(); ) { stream.next(); stream.remove(); } } /** * used by MWColumnPairHandle */ public MWColumnPair columnPairNamed(String columnPairName) { synchronized (this.columnPairs) { for (Iterator stream = this.columnPairs.iterator(); stream.hasNext(); ) { MWColumnPair pair = (MWColumnPair) stream.next(); if (pair.getName().equals(columnPairName)) { return pair; } } } return null; } public MWColumnPair columnPairFor(MWColumn sourceColumn, MWColumn targetColumn) { synchronized (this.columnPairs) { for (Iterator stream = this.columnPairs.iterator(); stream.hasNext(); ) { MWColumnPair pair = (MWColumnPair) stream.next(); if (pair.pairs(sourceColumn, targetColumn)) { return pair; } } } return null; } // ********** queries ********** /** * this test is not entirely accurate, but it's close enough... */ public boolean isForeignKeyReference() { return ! this.isPrimaryKeyReference(); } /** * return whether the reference is a JOIN between the * primary keys on the source table with the primary keys * on the target table */ public boolean isPrimaryKeyReference() { Collection sourcePrimaryKeys = CollectionTools.collection(this.getSourceTable().primaryKeyColumns()); if (sourcePrimaryKeys.size() != this.columnPairs.size()) { return false; } Collection targetPrimaryKeys = CollectionTools.collection(this.getTargetTable().primaryKeyColumns()); if (targetPrimaryKeys.size() != this.columnPairs.size()) { return false; } synchronized (this.columnPairs) { for (Iterator stream = this.columnPairs.iterator(); stream.hasNext(); ) { MWColumnPair pair = (MWColumnPair) stream.next(); if ( ! sourcePrimaryKeys.remove(pair.getSourceColumn())) { return false; } if ( ! targetPrimaryKeys.remove(pair.getTargetColumn())) { return false; } } } return true; } // ********** model synchronization support ********** protected void addChildrenTo(List children) { super.addChildrenTo(children); children.add(this.targetTableHandle); synchronized (this.columnPairs) { children.addAll(this.columnPairs); } } private NodeReferenceScrubber buildTargetTableScrubber() { return new NodeReferenceScrubber() { public void nodeReferenceRemoved(Node node, MWHandle handle) { MWReference.this.setTargetTable(null); } public String toString() { return "MWReference.buildTargetTableScrubber()"; } }; } //************** problems *************** protected void addProblemsTo(List currentProblems) { super.addProblemsTo(currentProblems); if (this.columnPairs.isEmpty()) { currentProblems.add(this.buildProblem(ProblemConstants.REFERENCE_NO_COLUMN_PAIRS, this.getName())); } if (this.getTargetTable() == null) { currentProblems.add(this.buildProblem(ProblemConstants.REFERENCE_NO_TARGET_TABLE, this.getName())); } } // ********** importing/refreshing ********** /** * the reference has the same name as the specified "external" foreign key, * synchronize the reference's column pairs with the "external" foreign key's column pairs */ void refreshColumnPairs(ExternalForeignKey externalForeignKey) { // after we have looped through the "external" column pairs, // 'removedColumnPairs' will be left with the column pairs that need to be removed Collection removedColumnPairs; synchronized (this.columnPairs) { removedColumnPairs = new HashSet(this.columnPairs); } ExternalForeignKeyColumnPair[] externalPairs = externalForeignKey.getColumnPairs(); for (int i = externalPairs.length; i-- > 0; ) { this.refreshColumnPair(externalPairs[i], removedColumnPairs); } this.removeColumnPairs(removedColumnPairs); } private void refreshColumnPair(ExternalForeignKeyColumnPair externalPair, Collection removedColumnPairs) { for (Iterator stream = removedColumnPairs.iterator(); stream.hasNext(); ) { if (((MWColumnPair) stream.next()).matches(externalPair)) { stream.remove(); return; } } this.addColumnPair(this.sourceColumn(externalPair), this.targetColumn(externalPair)); } /** * return the column in the source table with the same name as the * "external" column pair's source column */ MWColumn sourceColumn(ExternalForeignKeyColumnPair externalPair) { return this.getSourceTable().column(externalPair.getSourceColumn()); } /** * return the column in the target table with the same name as the * "external" column pair's target column */ MWColumn targetColumn(ExternalForeignKeyColumnPair externalPair) { return this.getTargetTable().column(externalPair.getTargetColumn()); } /** * return whether the reference has the same column pairs as * the specified "external" foreign key; * we compare column pairs because some references have system- * generated names that can change over the life of the reference * and we don't want to remove a reference simply because its * name changes - this would force the user to rebuild any objects * that referenced the [mistakenly] removed reference, e.g. mappings */ boolean matchesColumnPairs(ExternalForeignKey externalForeignKey) { ExternalForeignKeyColumnPair[] externalPairs = externalForeignKey.getColumnPairs(); int externalPairsLength = externalPairs.length; if (this.columnPairs.size() != externalPairsLength) { return false; } Collection columnPairsCopy; synchronized (this.columnPairs) { columnPairsCopy = new HashSet(this.columnPairs); } for (int i = externalPairsLength; i-- > 0; ) { ExternalForeignKeyColumnPair externalPair = externalPairs[i]; boolean match = false; for (Iterator stream = columnPairsCopy.iterator(); stream.hasNext(); ) { if (((MWColumnPair) stream.next()).matches(externalPair)) { stream.remove(); match = true; break; } } if ( ! match) { return false; } } return true; } // ********** runtime conversion ********** ForeignKeyConstraint buildRuntimeConstraint() { ForeignKeyConstraint fkc = new ForeignKeyConstraint(); fkc.setName(this.getName()); if (this.getTargetTable() != null) { fkc.setTargetTable(this.getTargetTable().getName()); } synchronized (this.columnPairs) { for (Iterator stream = this.columnPairs.iterator(); stream.hasNext(); ) { ((MWColumnPair) stream.next()).configureRuntimeConstraint(fkc); } } return fkc; } // ********** displaying and printing ********** public String displayString() { return this.name; } public void toString(StringBuffer sb) { sb.append(this.name); sb.append(" : "); this.printTableNameOn(this.getSourceTable(), sb); sb.append("=>"); this.printTableNameOn(this.getTargetTable(), sb); } private void printTableNameOn(MWTable table, StringBuffer sb) { sb.append((table == null) ? "null" : table.getName()); } // ********** TopLink methods ********** public static XMLDescriptor buildDescriptor() { XMLDescriptor descriptor = new XMLDescriptor(); descriptor.setJavaClass(MWReference.class); descriptor.addDirectMapping("name", "name/text()"); XMLCompositeObjectMapping targetTableHandleMapping = new XMLCompositeObjectMapping(); targetTableHandleMapping.setAttributeName("targetTableHandle"); targetTableHandleMapping.setGetMethodName("getTargetTableHandleForTopLink"); targetTableHandleMapping.setSetMethodName("setTargetTableHandleForTopLink"); targetTableHandleMapping.setReferenceClass(MWTableHandle.class); targetTableHandleMapping.setXPath("target-table-handle"); descriptor.addMapping(targetTableHandleMapping); ((XMLDirectMapping) descriptor.addDirectMapping("onDatabase", "on-database/text()")).setNullValue(Boolean.FALSE); XMLCompositeCollectionMapping columnPairsMapping = new XMLCompositeCollectionMapping(); columnPairsMapping.setAttributeName("columnPairs"); columnPairsMapping.setReferenceClass(MWColumnPair.class); columnPairsMapping.setXPath("column-pairs/column-pair"); descriptor.addMapping(columnPairsMapping); return descriptor; } /** * check for null */ private MWTableHandle getTargetTableHandleForTopLink() { return (this.targetTableHandle.getTable() == null) ? null : this.targetTableHandle; } private void setTargetTableHandleForTopLink(MWTableHandle targetTableHandle) { NodeReferenceScrubber scrubber = this.buildTargetTableScrubber(); this.targetTableHandle = ((targetTableHandle == null) ? new MWTableHandle(this, scrubber) : targetTableHandle.setScrubber(scrubber)); } }