| /******************************************************************************* |
| * Copyright (c) 1998, 2012 Oracle. 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.db.relational; |
| |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Vector; |
| import org.eclipse.persistence.tools.db.relational.platformsmodel.DatabasePlatform; |
| import org.eclipse.persistence.tools.db.relational.spi.ExternalColumn; |
| import org.eclipse.persistence.tools.db.relational.spi.ExternalForeignKey; |
| import org.eclipse.persistence.tools.db.relational.spi.ExternalForeignKeyColumnPair; |
| import org.eclipse.persistence.tools.db.relational.spi.ExternalTable; |
| import org.eclipse.persistence.tools.db.relational.spi.ExternalTableDescription; |
| import org.eclipse.persistence.tools.utility.NameTools; |
| import org.eclipse.persistence.tools.utility.StringTools; |
| import org.eclipse.persistence.tools.utility.iterable.LiveCloneIterable; |
| import org.eclipse.persistence.tools.utility.iterator.CompositeIterator; |
| import org.eclipse.persistence.tools.utility.iterator.FilteringIterator; |
| import org.eclipse.persistence.tools.utility.iterator.TransformationIterator; |
| import org.eclipse.persistence.tools.utility.node.Node; |
| |
| /** |
| * @version 2.5 |
| */ |
| @SuppressWarnings("nls") |
| public final class ELTable extends ELModel { |
| |
| /** 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<ELColumn> columns; |
| public static final String COLUMNS_COLLECTION = "columns"; |
| |
| private Collection<ELReference> references; |
| public static final String REFERENCES_COLLECTION = "references"; |
| |
| private boolean legacyIsFullyQualified; |
| |
| // ********** constructors ********** |
| |
| ELTable(ELDatabase database, String catalog, String schema, String shortName) { |
| super(database); |
| this.catalog = catalog; |
| this.schema = schema; |
| this.shortName = shortName; |
| } |
| |
| |
| // ********** initialization ********** |
| |
| /** |
| * initialize persistent state |
| */ |
| @Override |
| 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<ELColumn>(); |
| this.references = new Vector<ELReference>(); |
| } |
| |
| |
| // ********** 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.getParent().nodeRenamed(this); |
| for (ELColumn column : this.columns()) { |
| column.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 Iterable<ELColumn> columns() { |
| return new LiveCloneIterable<ELColumn>(this.columns) { |
| @Override |
| protected void remove(ELColumn current) { |
| ELTable.this.removeColumn(current); |
| } |
| }; |
| } |
| |
| public int columnsSize() { |
| return this.columns.size(); |
| } |
| |
| public ELColumn addColumn(String name) { |
| this.checkColumnName(name); |
| return this.addColumn(new ELColumn(this, name)); |
| } |
| |
| private ELColumn addColumn(ELColumn column) { |
| this.addItemToCollection(column, this.columns, COLUMNS_COLLECTION); |
| return column; |
| } |
| |
| public void removeColumn(ELColumn column) { |
| this.removeNodeFromCollection(column, this.columns, COLUMNS_COLLECTION); |
| } |
| |
| public void removeColumns(Iterator<ELColumn> cols) { |
| while (cols.hasNext()) { |
| this.removeColumn(cols.next()); |
| } |
| } |
| |
| public void removeColumns(Collection<ELColumn> 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 ELColumn columnNamed(String unqualifiedColumnName) { |
| synchronized (this.columns) { |
| for (Iterator<ELColumn> stream = this.columns.iterator(); stream.hasNext(); ) { |
| ELColumn column = stream.next(); |
| if (column.getName().equals(unqualifiedColumnName)) { |
| return column; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the column with the specified "qualified" name |
| */ |
| public ELColumn columnWithQualifiedName(String name) { |
| if ( ! ELColumn.parseTableNameFromQualifiedName(name).equals(this.getName())) { |
| throw new IllegalArgumentException(); |
| } |
| return this.columnNamed(ELColumn.parseColumnNameFromQualifiedName(name)); |
| } |
| |
| public Iterator<String> columnNames() { |
| return new TransformationIterator<ELColumn, String>(this.columns()) { |
| @Override |
| protected String transform(ELColumn next) { |
| return next.getName(); |
| } |
| }; |
| } |
| |
| public int primaryKeyColumnsSize() { |
| int size = 0; |
| synchronized (this.columns) { |
| for (ELColumn column : this.columns) { |
| if (column.isPrimaryKey()) { |
| size++; |
| } |
| } |
| } |
| return size; |
| } |
| |
| public Iterator<ELColumn> primaryKeyColumns() { |
| return new FilteringIterator<ELColumn>(this.columns()) { |
| @Override |
| protected boolean accept(ELColumn o) { |
| return o.isPrimaryKey(); |
| } |
| }; |
| } |
| |
| public Iterator<String> primaryKeyColumnNames() { |
| return new TransformationIterator<ELColumn, String>(this.primaryKeyColumns()) { |
| @Override |
| protected String transform(ELColumn next) { |
| return next.getName(); |
| } |
| }; |
| } |
| |
| public Iterator<ELColumn> nonPrimaryKeyColumns() { |
| return new FilteringIterator<ELColumn>(this.columns()) { |
| @Override |
| protected boolean accept(ELColumn o) { |
| return ! o.isPrimaryKey(); |
| } |
| }; |
| } |
| |
| /** |
| * used by table generation |
| */ |
| public ELColumn addColumnLike(ELColumn original) { |
| ELColumn copy = this.addColumn(original.getName()); |
| copy.copySettingsFrom(original); |
| return copy; |
| } |
| |
| |
| // ********** references ********** |
| |
| public Iterable<ELReference> references() { |
| return new LiveCloneIterable<ELReference>(this.references) { |
| @Override |
| protected void remove(ELReference current) { |
| ELTable.this.removeReference(current); |
| } |
| }; |
| } |
| |
| public int referencesSize() { |
| return this.references.size(); |
| } |
| |
| public ELReference addReference(String name, ELTable targetTable) { |
| this.checkReferenceName(name); |
| return this.addReference(new ELReference(this, name, targetTable)); |
| } |
| |
| private ELReference addReference(ELReference reference) { |
| this.addItemToCollection(reference, this.references, REFERENCES_COLLECTION); |
| return reference; |
| } |
| |
| public void removeReference(ELReference reference) { |
| this.removeNodeFromCollection(reference, this.references, REFERENCES_COLLECTION); |
| } |
| |
| public void removeReferences(Iterator<ELReference> refs) { |
| while (refs.hasNext()) { |
| this.removeReference(refs.next()); |
| } |
| } |
| |
| public void removeReferences(Collection<ELReference> 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<ELReference> refs) { |
| while (refs.hasNext()) { |
| ELReference ref = 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<ELReference> refs) { |
| this.removeDatabaseReferences(refs.iterator()); |
| } |
| |
| public boolean containsReferenceNamed(String referenceName) { |
| return this.referenceNamed(referenceName) != null; |
| } |
| |
| public ELReference referenceNamed(String referenceName) { |
| synchronized (this.references) { |
| for (Iterator<ELReference> stream = this.references.iterator(); stream.hasNext(); ) { |
| ELReference reference = stream.next(); |
| if (reference.getName().equals(referenceName)) { |
| return reference; |
| } |
| } |
| return null; |
| } |
| } |
| |
| public Iterator<String> referenceNames(){ |
| return new TransformationIterator<ELReference, String>(this.references()) { |
| @Override |
| protected String transform(ELReference next) { |
| return next.getName(); |
| } |
| }; |
| } |
| |
| /** |
| * Returns the references that are actually present |
| * as constraints on the database |
| */ |
| public Iterator<ELReference> databaseReferences() { |
| return new FilteringIterator<ELReference>(this.references()) { |
| @Override |
| protected boolean accept(ELReference o) { |
| return o.isOnDatabase(); |
| } |
| }; |
| } |
| |
| /** |
| * Returns all the references between the table and the |
| * specified table (either table can be the source and/or target) |
| */ |
| @SuppressWarnings("unchecked") |
| public Iterator<ELReference> referencesBetween(ELTable table) { |
| return new CompositeIterator<ELReference>( |
| this.referencesTo(table), |
| table.referencesTo(this) |
| ); |
| } |
| |
| /** |
| * Returns all the references with the table as the source and the |
| * specified table as the target |
| */ |
| public Iterator<ELReference> referencesTo(final ELTable targetTable) { |
| return new FilteringIterator<ELReference>(this.references()) { |
| @Override |
| protected boolean accept(ELReference o) { |
| return o.getTargetTable() == targetTable; |
| } |
| }; |
| } |
| |
| |
| // ********** Nominative implementation ********** |
| |
| /** |
| * Returns the appropriately-qualified name |
| */ |
| public String getName() { |
| return this.qualifiedName(); |
| } |
| |
| // ********** queries ********** |
| |
| public DatabasePlatform databasePlatform() { |
| return this.getParent().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.equalsIgnoreCase(this.catalog, cat) && |
| StringTools.equalsIgnoreCase(this.schema, sch) && |
| StringTools.equalsIgnoreCase(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.buildQualifiedName(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 StringTools.EMPTY_STRING; |
| } |
| 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 ********** |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public ELDatabase getParent() { |
| return (ELDatabase)super.getParent(); |
| } |
| |
| @Override |
| protected void addChildrenTo(List<Node> 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.getParent().checkTableName(newCatalog, newSchema, newShortName, this); |
| this.setCatalog(newCatalog); |
| this.setSchema(newSchema); |
| this.setShortName(newShortName); |
| } |
| |
| /** |
| * 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<ELColumn> stream = this.columns.iterator(); stream.hasNext(); ) { |
| stream.next().databasePlatformChanged(); |
| } |
| } |
| } |
| |
| |
| // ********** importing/refreshing ********** |
| |
| /** |
| * Returns the "external" table descriptions that share the table's name; |
| * Returns multiple entries only when the table's name |
| * is unqualified |
| */ |
| public Iterator<ExternalTableDescription> matchingExternalTableDescriptions() { |
| return this.getParent().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<ELColumn> removedColumns; |
| synchronized (this.columns) { |
| removedColumns = new HashSet<ELColumn>(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<ELColumn> removedColumns) { |
| ELColumn 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<ELReference> removedReferences; |
| synchronized (this.references) { |
| removedReferences = new HashSet<ELReference>(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<ELReference> removedReferences) { |
| // first, find the target table |
| ExternalTableDescription ttd = externalForeignKey.getTargetTableDescription(); |
| ELTable targetTable = this.getParent().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.getParent().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<ELReference> stream = removedReferences.iterator(); stream.hasNext(); ) { |
| ELReference ref = 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<ELReference> stream = removedReferences.iterator(); stream.hasNext(); ) { |
| ELReference ref = 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 |
| ELReference 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); |
| } |
| |
| /** |
| * Returns the column with the same name as the specified "external" column |
| */ |
| ELColumn column(ExternalColumn externalColumn) { |
| return (externalColumn == null) ? null : this.columnNamed(externalColumn.getName()); |
| } |
| |
| // ********** printing and displaying ********** |
| |
| public void toString(StringBuffer sb) { |
| sb.append(this.qualifiedName()); |
| } |
| |
| @Override |
| public String displayString() { |
| return this.qualifiedName(); |
| } |
| } |