/*******************************************************************************
* 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.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
import java.util.Vector;
import org.eclipse.persistence.tools.workbench.mappingsmodel.MWModel;
import org.eclipse.persistence.tools.workbench.mappingsmodel.MWNominative;
import org.eclipse.persistence.tools.workbench.mappingsmodel.ProblemConstants;
import org.eclipse.persistence.tools.workbench.mappingsmodel.spi.db.ExternalColumn;
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.mappingsmodel.spi.db.ExternalTable;
import org.eclipse.persistence.tools.workbench.mappingsmodel.spi.db.ExternalTableDescription;
import org.eclipse.persistence.tools.workbench.platformsmodel.DatabasePlatform;
import org.eclipse.persistence.tools.workbench.utility.NameTools;
import org.eclipse.persistence.tools.workbench.utility.io.FileTools;
import org.eclipse.persistence.tools.workbench.utility.iterators.CloneIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.CompositeIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.FilteringIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.TransformationIterator;
import org.eclipse.persistence.tools.workbench.utility.node.Node;
import org.eclipse.persistence.tools.workbench.utility.string.StringTools;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.mappings.DirectToFieldMapping;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.mappings.XMLCompositeCollectionMapping;
import org.eclipse.persistence.tools.schemaframework.TableDefinition;
public final class MWTable
extends MWModel
implements MWNominative
{
/** the catalog should never be empty - it can be null, but not empty */
private volatile String catalog;
public static final String CATALOG_PROPERTY = "catalog";
/** the schema should never be empty - it can be null, but not empty */
private volatile String schema;
public static final String SCHEMA_PROPERTY = "schema";
/** the short name should never be null OR empty */
private volatile String shortName;
public static final String SHORT_NAME_PROPERTY = "shortName";
public static final String QUALIFIED_NAME_PROPERTY = "qualifiedName";
/** this will be null if it is not known */
private Date lastRefreshTimestamp;
public static final String LAST_REFRESH_TIMESTAMP_PROPERTY = "lastRefreshTimestamp";
private Collection columns;
public static final String COLUMNS_COLLECTION = "columns";
private Collection references;
public static final String REFERENCES_COLLECTION = "references";
private boolean legacyIsFullyQualified;
// ********** constructors **********
/**
* Default constructor - for TopLink use only.
*/
private MWTable() {
super();
}
MWTable(MWDatabase database, String catalog, String schema, String shortName) {
super(database);
this.catalog = catalog;
this.schema = schema;
this.shortName = shortName;
}
// ********** initialization **********
/**
* initialize persistent state
*/
protected void initialize(Node parent) {
super.initialize(parent);
this.lastRefreshTimestamp = null; // if the table is built by hand, this is left null
this.columns = new Vector();
this.references = new Vector();
}
// ********** name: catalog, schema, shortName **********
public String getCatalog() {
return this.catalog;
}
/**
* private - @see #rename(String, String, String)
*/
private void setCatalog(String catalog) {
Object old = this.catalog;
this.catalog = catalog;
if (this.attributeValueHasChanged(old, catalog)) {
this.firePropertyChanged(CATALOG_PROPERTY, old, catalog);
this.qualifiedNameChanged();
}
}
public String getSchema() {
return this.schema;
}
/**
* private - @see #rename(String, String, String)
*/
private void setSchema(String schema) {
Object old = this.schema;
this.schema = schema;
if (this.attributeValueHasChanged(old, schema)) {
this.firePropertyChanged(SCHEMA_PROPERTY, old, schema);
this.qualifiedNameChanged();
}
}
public String getShortName() {
return this.shortName;
}
/**
* private - @see #rename(String, String, String)
*/
private void setShortName(String shortName) {
Object old = this.shortName;
this.shortName = shortName;
if (this.attributeValueHasChanged(old, shortName)) {
this.firePropertyChanged(SHORT_NAME_PROPERTY, old, shortName);
this.qualifiedNameChanged();
}
}
private void qualifiedNameChanged() {
this.firePropertyChanged(QUALIFIED_NAME_PROPERTY, this.qualifiedName());
this.getProject().nodeRenamed(this);
for (Iterator stream = this.columns(); stream.hasNext(); ) {
((MWColumn) stream.next()).qualifiedNameChanged();
}
}
// ********** columns **********
/**
* this will be null if it is not known
*/
public Date getLastRefreshTimestamp() {
return this.lastRefreshTimestamp;
}
/**
* PRIVATE - this can only be set internally
*/
private void setLastRefreshTimestamp(Date lastRefreshTimestamp) {
Object old = this.lastRefreshTimestamp;
this.lastRefreshTimestamp = lastRefreshTimestamp;
this.firePropertyChanged(LAST_REFRESH_TIMESTAMP_PROPERTY, old, lastRefreshTimestamp);
}
// ********** columns **********
public Iterator columns() {
return new CloneIterator(this.columns) {
protected void remove(Object current) {
MWTable.this.removeColumn((MWColumn) current);
}
};
}
public int columnsSize() {
return this.columns.size();
}
public MWColumn addColumn(String name) {
this.checkColumnName(name);
return this.addColumn(new MWColumn(this, name));
}
private MWColumn addColumn(MWColumn column) {
this.addItemToCollection(column, this.columns, COLUMNS_COLLECTION);
return column;
}
public void removeColumn(MWColumn column) {
this.removeNodeFromCollection(column, this.columns, COLUMNS_COLLECTION);
}
public void removeColumns(Iterator cols) {
while (cols.hasNext()) {
this.removeColumn((MWColumn) cols.next());
}
}
public void removeColumns(Collection cols) {
this.removeColumns(cols.iterator());
}
public boolean containsColumnNamed(String columnName) {
return this.columnNamed(columnName) != null;
}
/**
* only used for unqualified column names
* @see #columnWithQualifiedName(String qualifiedName)
*/
public MWColumn columnNamed(String unqualifiedColumnName) {
synchronized (this.columns) {
for (Iterator stream = this.columns.iterator(); stream.hasNext(); ) {
MWColumn column = (MWColumn) stream.next();
if (column.getName().equals(unqualifiedColumnName)) {
return column;
}
}
}
return null;
}
/**
* return the column with the specified "qualified" name
*/
public MWColumn columnWithQualifiedName(String name) {
if ( ! MWColumn.parseTableNameFromQualifiedName(name).equals(this.getName())) {
throw new IllegalArgumentException();
}
return this.columnNamed(MWColumn.parseColumnNameFromQualifiedName(name));
}
public Iterator columnNames() {
return new TransformationIterator(this.columns()) {
protected Object transform(Object next) {
return ((MWColumn) next).getName();
}
};
}
public int primaryKeyColumnsSize() {
int size = 0;
synchronized (this.columns) {
for (Iterator stream = this.columns.iterator(); stream.hasNext(); ) {
if (((MWColumn) stream.next()).isPrimaryKey()) {
size++;
}
}
}
return size;
}
public Iterator primaryKeyColumns() {
return new FilteringIterator(this.columns()) {
protected boolean accept(Object o) {
return ((MWColumn) o).isPrimaryKey();
}
};
}
public Iterator primaryKeyColumnNames() {
return new TransformationIterator(this.primaryKeyColumns()) {
protected Object transform(Object next) {
return ((MWColumn) next).getName();
}
};
}
public Iterator nonPrimaryKeyColumns() {
return new FilteringIterator(this.columns()) {
protected boolean accept(Object o) {
return ! ((MWColumn) o).isPrimaryKey();
}
};
}
/**
* used by table generation
*/
public MWColumn addColumnLike(MWColumn original) {
MWColumn copy = this.addColumn(original.getName());
copy.copySettingsFrom(original);
return copy;
}
// ********** references **********
public Iterator references() {
return new CloneIterator(this.references) {
protected void remove(Object current) {
MWTable.this.removeReference((MWReference) current);
}
};
}
public int referencesSize() {
return this.references.size();
}
public MWReference addReference(String name, MWTable targetTable) {
this.checkReferenceName(name);
return this.addReference(new MWReference(this, name, targetTable));
}
private MWReference addReference(MWReference reference) {
this.addItemToCollection(reference, this.references, REFERENCES_COLLECTION);
return reference;
}
public void removeReference(MWReference reference) {
this.removeNodeFromCollection(reference, this.references, REFERENCES_COLLECTION);
}
public void removeReferences(Iterator refs) {
while (refs.hasNext()) {
this.removeReference((MWReference) refs.next());
}
}
public void removeReferences(Collection refs) {
this.removeReferences(refs.iterator());
}
/**
* remove only the references among those specified
* that are defined on the database; leave any "virtual"
* user-defined references
*/
private void removeDatabaseReferences(Iterator refs) {
while (refs.hasNext()) {
MWReference ref = (MWReference) refs.next();
if (ref.isOnDatabase()) {
this.removeReference(ref);
}
}
}
/**
* remove only the references among those specified
* that are defined on the database; leave any "virtual"
* user-defined references
*/
private void removeDatabaseReferences(Collection refs) {
this.removeDatabaseReferences(refs.iterator());
}
public boolean containsReferenceNamed(String referenceName) {
return this.referenceNamed(referenceName) != null;
}
public MWReference referenceNamed(String referenceName) {
synchronized (this.references) {
for (Iterator stream = this.references.iterator(); stream.hasNext(); ) {
MWReference reference = (MWReference) stream.next();
if (reference.getName().equals(referenceName)) {
return reference;
}
}
return null;
}
}
public Iterator referenceNames(){
return new TransformationIterator(this.references()) {
protected Object transform(Object next) {
return ((MWReference) next).getName();
}
};
}
/**
* return the references that are actually present
* as constraints on the database
*/
public Iterator databaseReferences() {
return new FilteringIterator(this.references()) {
protected boolean accept(Object o) {
return ((MWReference) o).isOnDatabase();
}
};
}
/**
* return all the references between the table and the
* specified table (either table can be the source and/or target)
*/
public Iterator referencesBetween(MWTable table) {
return new CompositeIterator(this.referencesTo(table), table.referencesTo(this));
}
/**
* return all the references with the table as the source and the
* specified table as the target
*/
public Iterator referencesTo(final MWTable targetTable) {
return new FilteringIterator(this.references()) {
protected boolean accept(Object o) {
return ((MWReference) o).getTargetTable() == targetTable;
}
};
}
// ********** Nominative implementation **********
/**
* return the appropriately-qualified name
*/
public String getName() {
return this.qualifiedName();
}
// ********** queries **********
public DatabasePlatform databasePlatform() {
return this.getDatabase().getDatabasePlatform();
}
boolean nameMatches(String cat, String sch, String sn) {
return this.valuesAreEqual(this.catalog, cat) &&
this.valuesAreEqual(this.schema, sch) &&
this.valuesAreEqual(this.shortName, sn);
}
boolean nameMatchesIgnoreCase(String cat, String sch, String sn) {
return StringTools.stringsAreEqualIgnoreCase(this.catalog, cat) &&
StringTools.stringsAreEqualIgnoreCase(this.schema, sch) &&
StringTools.stringsAreEqualIgnoreCase(this.shortName, sn);
}
/**
* if either the 'catalog' or 'schema' are specified,
* the table's name is "qualified"
*/
public boolean nameIsQualified() {
if (this.catalog != null) {
return true;
}
if (this.schema != null) {
return true;
}
return false;
}
public boolean nameIsUnqualified() {
return ! this.nameIsQualified();
}
public String qualifiedName() {
return NameTools.buildQualifiedDatabaseObjectName(this.catalog, this.schema, this.shortName);
}
public String unqualifiedName() {
return this.shortName;
}
/**
* used for table generation:
* catalog.schema
* catalog.
* schema
*/
private String qualifier() {
if (this.nameIsUnqualified()) {
return "";
}
StringBuffer sb = new StringBuffer(100);
if (this.catalog != null) {
sb.append(this.catalog);
}
if (this.schema != null) {
if (this.catalog != null) {
sb.append('.');
}
sb.append(this.schema);
}
return sb.toString();
}
// ********** miscellaneous behavior **********
protected void addChildrenTo(List children) {
super.addChildrenTo(children);
synchronized (this.columns) { children.addAll(this.columns); }
synchronized (this.references) { children.addAll(this.references); }
}
public void rename(String newCatalog, String newSchema, String newShortName) {
if (this.nameMatches(newCatalog, newSchema, newShortName)) {
// if someone is tryng to rename a table to its existing name, ignore it
return;
}
this.getDatabase().checkTableName(newCatalog, newSchema, newShortName, this);
this.setCatalog(newCatalog);
this.setSchema(newSchema);
this.setShortName(newShortName);
this.getDatabase().tableRenamed(); // we must notify the database of a name change
}
/**
* disallow duplicate column names
*/
void checkColumnName(String columnName) {
if ((columnName == null) || (columnName.length() == 0)) {
throw new IllegalArgumentException();
}
if (this.containsColumnNamed(columnName)) {
throw new IllegalArgumentException("duplicate column name: " + columnName);
}
}
/**
* disallow duplicate reference names
*/
void checkReferenceName(String referenceName) {
if ((referenceName == null) || (referenceName.length() == 0)) {
throw new IllegalArgumentException();
}
if (this.containsReferenceNamed(referenceName)) {
throw new IllegalArgumentException("duplicate reference name: " + referenceName);
}
}
void databasePlatformChanged() {
synchronized (this.columns) {
for (Iterator stream = this.columns.iterator(); stream.hasNext(); ) {
((MWColumn) stream.next()).databasePlatformChanged();
}
}
}
// ********** problems **********
protected void addProblemsTo(List currentProblems) {
super.addProblemsTo(currentProblems);
int identityTypeCount = 0;
synchronized (this.columns) {
for (Iterator stream = this.columns.iterator(); stream.hasNext(); ) {
MWColumn column = (MWColumn) stream.next();
if (column.isIdentity()) {
identityTypeCount++;
}
}
}
if (identityTypeCount > 1) {
currentProblems.add(this.buildProblem(ProblemConstants.TABLE_TOO_MANY_IDENTITY_COLUMNS));
}
}
// ********** importing/refreshing **********
/**
* return the "external" table descriptions that share the table's name;
* typically this will return multiple entries only when the table's name
* is unqualified
*/
public Iterator matchingExternalTableDescriptions() {
return this.getDatabase().externalTableDescriptions(this.catalog, this.schema, this.shortName, null);
}
/**
* refresh the table's columns (but not the table's references - that
* must be performed separately);
*/
void refreshColumns(ExternalTable externalTable) {
// after we have looped through the external columns,
// 'removedColumns' will be left with the columns that need to be removed
Collection removedColumns;
synchronized (this.columns) {
removedColumns = new HashSet(this.columns);
}
ExternalColumn[] externalColumns = externalTable.getColumns();
for (int i = externalColumns.length; i-- > 0; ) {
this.refreshColumn(externalColumns[i], removedColumns);
}
this.removeColumns(removedColumns);
this.setLastRefreshTimestamp(new Date());
}
/**
* refresh the column corresponding to the specified "external" column
*/
private void refreshColumn(ExternalColumn externalColumn, Collection removedColumns) {
MWColumn existingColumn = this.columnNamed(externalColumn.getName());
if (existingColumn == null) {
// we have a new column
existingColumn = this.addColumn(externalColumn.getName());
} else {
// retain the existing column
removedColumns.remove(existingColumn);
}
existingColumn.refresh(externalColumn);
}
/**
* refresh the table's references - this should be called after
* the table's columns have been refreshed and any target tables
* have had their columns refreshed - this will allow us to build
* the references properly
*/
void refreshReferences(ExternalTable externalTable) {
// after we have looped through the foreign keys,
// 'removedReferences' will be left with the references that need to be removed
Collection removedReferences;
synchronized (this.references) {
removedReferences = new HashSet(this.references);
}
ExternalForeignKey[] externalForeignKeys = externalTable.getForeignKeys();
for (int i = externalForeignKeys.length; i-- > 0; ) {
this.refreshReference(externalForeignKeys[i], removedReferences);
}
// remove *only* the remaining references that were originally defined on the database;
// leave the "virtual" user-defined references intact
this.removeDatabaseReferences(removedReferences);
}
/**
* refresh the reference corresponding to the specified "external" foreign key;
* search the 'removedReferences' so that we don't ever
* refresh the same reference twice; first search for a match based
* on name, then search for a match based on the column pairs - this
* should prevent us from getting a match based on columns that has
* the same name as another reference with different columns that
* is further down the list
*/
private void refreshReference(ExternalForeignKey externalForeignKey, Collection removedReferences) {
// first, find the target table
ExternalTableDescription ttd = externalForeignKey.getTargetTableDescription();
MWTable targetTable = this.getDatabase().tableNamed(ttd.getCatalogName(), ttd.getSchemaName(), ttd.getName());
if (targetTable == null) {
// the target table may have been imported without
// a fully-qualified name, so try that also
targetTable = this.getDatabase().tableNamed(null, null, ttd.getName());
}
// if we don't have the target table, we can't build a reference to it
if (targetTable == null) {
return;
}
// look for a match based on name
for (Iterator stream = removedReferences.iterator(); stream.hasNext(); ) {
MWReference ref = (MWReference) stream.next();
if (ref.getName().equals(externalForeignKey.getName())) {
ref.setTargetTable(targetTable);
ref.refreshColumnPairs(externalForeignKey);
ref.setOnDatabase(true);
removedReferences.remove(ref);
return;
}
}
// look for a match based on column pairs
for (Iterator stream = removedReferences.iterator(); stream.hasNext(); ) {
MWReference ref = (MWReference) stream.next();
if (ref.matchesColumnPairs(externalForeignKey)) {
ref.setName(externalForeignKey.getName());
ref.setTargetTable(targetTable);
ref.setOnDatabase(true);
removedReferences.remove(ref);
return;
}
}
// no match - we have a new reference
MWReference ref = this.addReference(externalForeignKey.getName(), targetTable);
ExternalForeignKeyColumnPair[] pairs = externalForeignKey.getColumnPairs();
for (int i = pairs.length; i-- > 0; ) {
ref.addColumnPair(this.column(pairs[i].getSourceColumn()), targetTable.column(pairs[i].getTargetColumn()));
}
ref.setOnDatabase(true);
}
/**
* return the column with the same name as the specified "external" column
*/
MWColumn column(ExternalColumn externalColumn) {
return (externalColumn == null) ? null : this.columnNamed(externalColumn.getName());
}
// ********** runtime conversion ***********
/**
* return a run-time table definition corresponding to the table
*/
TableDefinition buildRuntimeTableDefinition() {
TableDefinition td = new TableDefinition();
td.setName(this.shortName);
td.setQualifier(this.qualifier());
synchronized (this.columns) {
for (Iterator stream = this.columns.iterator(); stream.hasNext(); ) {
td.addField(((MWColumn) stream.next()).buildRuntimeFieldDefinition());
}
}
// only add the references that are defined on the database
synchronized (this.references) {
for (Iterator stream = this.references.iterator(); stream.hasNext(); ) {
MWReference ref = (MWReference) stream.next();
if (ref.isOnDatabase()) {
td.addForeignKeyConstraint(ref.buildRuntimeConstraint());
}
}
}
return td;
}
// ********** printing and displaying **********
public void toString(StringBuffer sb) {
sb.append(this.qualifiedName());
}
public String displayString() {
return this.qualifiedName();
}
// ********** TopLink methods **********
public static XMLDescriptor buildDescriptor() {
XMLDescriptor descriptor = new XMLDescriptor();
descriptor.setJavaClass(MWTable.class);
descriptor.setDefaultRootElement("table");
descriptor.addDirectMapping("catalog", "catalog/text()");
descriptor.addDirectMapping("schema", "schema/text()");
descriptor.addDirectMapping("shortName", "short-name/text()");
descriptor.addDirectMapping("lastRefreshTimestamp", "last-refresh-timestamp/text()");
XMLCompositeCollectionMapping columnsMapping = new XMLCompositeCollectionMapping();
columnsMapping.setAttributeName("columns");
columnsMapping.setGetMethodName("getColumnsForTopLink");
columnsMapping.setSetMethodName("setColumnsForTopLink");
columnsMapping.setReferenceClass(MWColumn.class);
columnsMapping.setXPath("columns/column");
descriptor.addMapping(columnsMapping);
XMLCompositeCollectionMapping referencesMapping = new XMLCompositeCollectionMapping();
referencesMapping.setAttributeName("references");
referencesMapping.setGetMethodName("getReferencesForTopLink");
referencesMapping.setSetMethodName("setReferencesForTopLink");
referencesMapping.setReferenceClass(MWReference.class);
referencesMapping.setXPath("references/table-association");
descriptor.addMapping(referencesMapping);
return descriptor;
}
/**
* sort the columns for TopLink
*/
private Collection getColumnsForTopLink() {
synchronized (this.columns) {
return new TreeSet(this.columns);
}
}
private void setColumnsForTopLink(Collection columns) {
this.columns = columns;
}
/**
* sort the references for TopLink
*/
private Collection getReferencesForTopLink() {
synchronized (this.references) {
return new TreeSet(this.references);
}
}
private void setReferencesForTopLink(Collection references) {
this.references = references;
}
}