/******************************************************************************* * Copyright (c) 1998, 2016 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 * 02/04/2013-2.5 Guy Pelletier * - 389090: JPA 2.1 DDL Generation Support ******************************************************************************/ package org.eclipse.persistence.tools.schemaframework; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import org.eclipse.persistence.exceptions.DatabaseException; import org.eclipse.persistence.internal.helper.DatabaseField; import org.eclipse.persistence.internal.sessions.AbstractSession; import org.eclipse.persistence.logging.SessionLog; import org.eclipse.persistence.platform.database.DatabasePlatform; import org.eclipse.persistence.sequencing.Sequence; import org.eclipse.persistence.sequencing.TableSequence; import org.eclipse.persistence.sessions.DatabaseRecord; import org.eclipse.persistence.sessions.DatabaseSession; import org.eclipse.persistence.sessions.Session; /** * <b>Purpose</b>: This class is responsible for creating the tables defined in the project. * A specific subclass of this class is created for each project. The specific table information * is defined in the subclass. * * @since TopLink 2.0 * @author Peter Krogh */ public class TableCreator { /** Default identity generator sequence name. * Copy of value from JPA: {@code MetadataProject.DEFAULT_IDENTITY_GENERATOR}. */ public static final String DEFAULT_IDENTITY_GENERATOR = "SEQ_GEN_IDENTITY"; /** Flag to disable table existence check before create. */ public static boolean CHECK_EXISTENCE = true; protected List<TableDefinition> tableDefinitions; protected String name; protected boolean ignoreDatabaseException; //if true, DDL generation will continue even if exceptions occur public TableCreator() { this(new ArrayList<TableDefinition>()); } public TableCreator(List<TableDefinition> tableDefinitions) { super(); this.tableDefinitions = tableDefinitions; } /** * Add the table. */ public void addTableDefinition(TableDefinition tableDefinition) { this.tableDefinitions.add(tableDefinition); } /** * Add a set of tables. */ public void addTableDefinitions(Collection<TableDefinition> tableDefs) { this.tableDefinitions.addAll(tableDefs); } /** * Create constraints. */ public void createConstraints(DatabaseSession session) { //CR2612669 createConstraints(session, new SchemaManager(session)); } /** * Create constraints. */ public void createConstraints(DatabaseSession session, SchemaManager schemaManager) { createConstraints(session, schemaManager, true); } /** * Create constraints. */ public void createConstraints(DatabaseSession session, SchemaManager schemaManager, boolean build) { createConstraints(getTableDefinitions(), session, schemaManager, build); } /** * Create constraints. */ public void createConstraints(List<TableDefinition> tables, DatabaseSession session, SchemaManager schemaManager, boolean build) { buildConstraints(schemaManager, build); // Unique constraints should be generated before foreign key constraints, // because foreign key constraints can reference unique constraints for (TableDefinition table : tables) { try { schemaManager.createUniqueConstraints(table); } catch (DatabaseException ex) { if (!shouldIgnoreDatabaseException()) { throw ex; } } } for (TableDefinition table : tables) { try { schemaManager.createForeignConstraints(table); } catch (DatabaseException ex) { if (!shouldIgnoreDatabaseException()) { throw ex; } } } } /** * This creates the tables on the database. * If the table already exists this will fail. */ public void createTables(org.eclipse.persistence.sessions.DatabaseSession session) { //CR2612669 createTables(session, new SchemaManager(session)); } /** * This creates the tables on the database. * If the table already exists this will fail. */ public void createTables(DatabaseSession session, SchemaManager schemaManager) { createTables(session, schemaManager, true); } /** * This creates the tables on the database. * If the table already exists this will fail. */ public void createTables(DatabaseSession session, SchemaManager schemaManager, boolean build) { createTables(session, schemaManager, build, true, true, true); } /** * This creates the tables on the database. * If the table already exists this will fail. * @param session Active database session. * @param schemaManager Database schema manipulation manager. * @param build Whether to build constraints. * @param check Whether to check for tables existence. * @param createSequenceTables Whether to create sequence tables. * @param createSequences Whether to create sequences. */ public void createTables(final DatabaseSession session, final SchemaManager schemaManager, final boolean build, final boolean check, final boolean createSequenceTables, final boolean createSequences) { buildConstraints(schemaManager, build); final String sequenceTableName = getSequenceTableName(session); final List<TableDefinition> missingTables = new ArrayList<TableDefinition>(); for (TableDefinition table : getTableDefinitions()) { // Must not create sequence table as done in createSequences. if (!table.getName().equals(sequenceTableName)) { boolean alreadyExists = false; // Check if the table already exists, to avoid logging create error. if (check && CHECK_EXISTENCE && schemaManager.shouldWriteToDatabase()) { alreadyExists = schemaManager.checkTableExists(table); } if (!alreadyExists) { missingTables.add(table); try { schemaManager.createObject(table); session.getSessionLog().log(SessionLog.FINEST, SessionLog.DDL, "default_tables_created", table.getFullName()); } catch (DatabaseException ex) { session.getSessionLog().log(SessionLog.FINEST, SessionLog.DDL, "default_tables_already_existed", table.getFullName()); if (!shouldIgnoreDatabaseException()) { throw ex; } } } } } createConstraints(missingTables, session, schemaManager, false); schemaManager.createOrReplaceSequences(createSequenceTables, createSequences); session.getDatasourcePlatform().initIdentitySequences(session, DEFAULT_IDENTITY_GENERATOR); } /** * Drop the table constraints from the database. */ public void dropConstraints(DatabaseSession session) { //CR2612669 dropConstraints(session, new SchemaManager(session)); } /** * Drop the table constraints from the database. */ public void dropConstraints(DatabaseSession session, SchemaManager schemaManager) { dropConstraints(session, schemaManager, true); } /** * Drop the table constraints from the database. */ public void dropConstraints(DatabaseSession session, SchemaManager schemaManager, boolean build) { buildConstraints(schemaManager, build); for (TableDefinition table : getTableDefinitions()) { try { schemaManager.dropConstraints(table); } catch (DatabaseException exception) { //ignore } } } /** * Drop the tables from the database. */ public void dropTables(DatabaseSession session) { //CR2612669 dropTables(session, new SchemaManager(session)); } /** * Drop the tables from the database. */ public void dropTables(DatabaseSession session, SchemaManager schemaManager) { dropTables(session, schemaManager, true); } /** * Drop the tables from the database. * @param session Active database session. * @param schemaManager Database schema manipulation manager. * @param build Whether to build constraints. */ public void dropTables(final DatabaseSession session, final SchemaManager schemaManager, final boolean build) { buildConstraints(schemaManager, build); // CR 3870467, do not log stack, or log at all if not fine boolean shouldLogExceptionStackTrace = session.getSessionLog().shouldLogExceptionStackTrace(); final int level = session.getSessionLog().getLevel(); if (shouldLogExceptionStackTrace) { session.getSessionLog().setShouldLogExceptionStackTrace(false); } if (level > SessionLog.FINE) { session.getSessionLog().setLevel(SessionLog.SEVERE); } try { dropConstraints(session, schemaManager, false); final String sequenceTableName = getSequenceTableName(session); List<TableDefinition> tables = getTableDefinitions(); int trys = 1; if (SchemaManager.FORCE_DROP) { trys = 5; } while ((trys > 0) && !tables.isEmpty()) { trys--; final List<TableDefinition> failed = new ArrayList<TableDefinition>(); final Set<String> tableNames = new HashSet<>(tables.size()); for (final TableDefinition table : tables) { final String tableName = table.getName(); // Must not create sequence table as done in createSequences. if (!tableName.equals(sequenceTableName)) { try { schemaManager.dropObject(table); tableNames.add(tableName); } catch (DatabaseException exception) { failed.add(table); if (!shouldIgnoreDatabaseException()) { throw exception; } } } } session.getDatasourcePlatform().removeIdentitySequences(session, DEFAULT_IDENTITY_GENERATOR, tableNames); tables = failed; } } finally { if (shouldLogExceptionStackTrace) { session.getSessionLog().setShouldLogExceptionStackTrace(true); } if (level > SessionLog.FINE) { session.getSessionLog().setLevel(level); } } } /** * Return the name. */ public String getName() { return name; } /** * Return the tables. */ public List<TableDefinition> getTableDefinitions() { return tableDefinitions; } /** * Recreate the tables on the database. * This will drop the tables if they exist and recreate them. */ public void replaceTables(DatabaseSession session) { replaceTables(session, new SchemaManager(session)); } /** * Recreate the tables on the database. * This will drop the tables if they exist and recreate them. */ public void replaceTables(DatabaseSession session, SchemaManager schemaManager) { replaceTables(session, schemaManager, true, true); } /** * Recreate the tables on the database. * This will drop the tables if they exist and recreate them. */ public void replaceTables(DatabaseSession session, SchemaManager schemaManager, boolean createSequenceTables) { replaceTables(session, schemaManager, createSequenceTables, false); } /** * Recreate the tables on the database. * This will drop the tables if they exist and recreate them. */ public void replaceTables(DatabaseSession session, SchemaManager schemaManager, boolean createSequenceTables, boolean createSequences) { replaceTablesAndConstraints(schemaManager, session, createSequenceTables, createSequences); } protected void replaceTablesAndConstraints(SchemaManager schemaManager, DatabaseSession session, boolean createSequenceTables, boolean createSequences) { buildConstraints(schemaManager, true); boolean ignore = shouldIgnoreDatabaseException(); setIgnoreDatabaseException(true); try { dropTables(session, schemaManager, false); } finally { setIgnoreDatabaseException(ignore); } createTables(session, schemaManager, false, false, createSequenceTables, createSequences); } protected void replaceTablesAndConstraints(SchemaManager schemaManager, DatabaseSession session) { replaceTables(session, schemaManager, false, false); } /** * Convert any field constraint to constraint objects. */ protected void buildConstraints(SchemaManager schemaManager, boolean build) { if (build) { for (TableDefinition table : getTableDefinitions()) { schemaManager.buildFieldTypes(table); } } } /** * Set the name. */ public void setName(String name) { this.name = name; } /** * Set the tables. */ public void setTableDefinitions(Vector tableDefinitions) { this.tableDefinitions = tableDefinitions; } /** * Return true if DatabaseException is to be ignored. */ public boolean shouldIgnoreDatabaseException() { return ignoreDatabaseException; } /** * Set flag whether DatabaseException should be ignored. */ public void setIgnoreDatabaseException(boolean ignoreDatabaseException) { this.ignoreDatabaseException = ignoreDatabaseException; } /** * This returns the Sequence Table's qualified name, without delimiting. * @param session * @return the qualified table name */ protected String getSequenceTableName(Session session) { String sequenceTableName = null; if (session.getProject().usesSequencing()) { Sequence sequence = session.getLogin().getDefaultSequence(); if (sequence instanceof TableSequence) { sequenceTableName = ((TableSequence)sequence).getQualifiedTableName(); } } return sequenceTableName; } /** * Create or extend the tables on the database. * This will alter existing tables to add missing fields or create the table otherwise. * It will also create Sequences tables and objects. */ public void extendTables(DatabaseSession session, SchemaManager schemaManager) { extendTablesAndConstraints(schemaManager, session); schemaManager.createOrReplaceSequences(true, true); } protected void extendTablesAndConstraints(SchemaManager schemaManager, DatabaseSession session) { buildConstraints(schemaManager, true); boolean ignore = shouldIgnoreDatabaseException(); setIgnoreDatabaseException(true); try { extendTables(session, schemaManager, false); } finally { setIgnoreDatabaseException(ignore); } } /** * This creates/extends the tables on the database. * @param session Active database session. * @param schemaManager Database schema manipulation manager. * @param build Whether to build constraints. */ public void extendTables(final DatabaseSession session, final SchemaManager schemaManager, final boolean build) { buildConstraints(schemaManager, build); final String sequenceTableName = getSequenceTableName(session); for (final TableDefinition table : getTableDefinitions()) { // Must not create sequence table as done in createSequences. if (!table.getName().equals(sequenceTableName)) { final AbstractSession abstractSession = (AbstractSession) session; boolean alreadyExists = false; // Check if the table already exists, to avoid logging create error. if (CHECK_EXISTENCE && schemaManager.shouldWriteToDatabase()) { alreadyExists = schemaManager.checkTableExists(table); } DatabaseException createTableException = null; if (!alreadyExists) { //assume table does not exist try { schemaManager.createObject(table); session.getSessionLog().log(SessionLog.FINEST, SessionLog.DDL, "default_tables_created", table.getFullName()); } catch (final DatabaseException exception) { createTableException = exception; alreadyExists = true; } } if (alreadyExists) { //Assume the table exists, so lookup the column info //While SQL is case insensitive, getColumnInfo is and will not return the table info unless the name is passed in //as it is stored internally. String tableName = table.getTable()==null? table.getName(): table.getTable().getName(); final boolean usesDelimiting = (table.getTable()!=null && table.getTable().shouldUseDelimiters()); List<DatabaseRecord> columnInfo = null; //I need the actual table catalog, schema and tableName for getTableInfo. columnInfo = abstractSession.getAccessor().getColumnInfo(null, null, tableName, null, abstractSession); if (!usesDelimiting && (columnInfo == null || columnInfo.isEmpty()) ) { tableName = tableName.toUpperCase(); columnInfo = abstractSession.getAccessor().getColumnInfo(null, null, tableName, null, abstractSession); if (( columnInfo == null || columnInfo.isEmpty()) ){ tableName = tableName.toLowerCase(); columnInfo = abstractSession.getAccessor().getColumnInfo(null, null, tableName, null, abstractSession); } } if (columnInfo != null && !columnInfo.isEmpty()) { //Table exists, add individual fields as necessary //hash the table's existing columns by name final Map<DatabaseField, DatabaseRecord> columns = new HashMap(columnInfo.size()); final DatabaseField columnNameLookupField = new DatabaseField("COLUMN_NAME"); final DatabaseField schemaLookupField = new DatabaseField("TABLE_SCHEM"); boolean schemaMatchFound = false; // Determine the probably schema for the table, this is a heuristic, so should not cause issues if wrong. String qualifier = table.getQualifier(); if ((qualifier == null) || (qualifier.length() == 0)) { qualifier = session.getDatasourcePlatform().getTableQualifier(); if ((qualifier == null) || (qualifier.length() == 0)) { qualifier = session.getLogin().getUserName(); // Oracle DB DS defined in WLS does not contain user name so it's stored in platform. if ((qualifier == null) || (qualifier.length() == 0)) { final DatabasePlatform platform = session.getPlatform(); if (platform.supportsConnectionUserName()) { qualifier = platform.getConnectionUserName(); } } } } final boolean checkSchema = (qualifier != null) && (qualifier.length() > 0); for (final DatabaseRecord record : columnInfo) { final String fieldName = (String)record.get(columnNameLookupField); if (fieldName != null && fieldName.length() > 0) { final DatabaseField column = new DatabaseField(fieldName); if (session.getPlatform().shouldForceFieldNamesToUpperCase()) { column.useUpperCaseForComparisons(true); } final String schema = (String)record.get(schemaLookupField); // Check the schema as well. Ignore columns for other schema if a schema match is found. if (schemaMatchFound) { if (qualifier.equalsIgnoreCase(schema)) { columns.put(column, record); } } else { if (checkSchema) { if (qualifier.equalsIgnoreCase(schema)) { schemaMatchFound = true; // Remove unmatched columns from other schemas. columns.clear(); } } // If none of the schemas match what is expected, assume what is expected is wrong, and use all columns. columns.put(column, record); } } } //Go through each field we need to have in the table to see if it already exists for (final FieldDefinition fieldDef : table.getFields()){ DatabaseField dbField = fieldDef.getDatabaseField(); if ( dbField == null ) { dbField = new DatabaseField(fieldDef.getName()); } if (columns.get(dbField)== null) { //field does not exist so add it to the table try { table.addFieldOnDatabase(abstractSession, fieldDef); } catch (final DatabaseException addFieldEx) { session.getSessionLog().log(SessionLog.FINEST, SessionLog.DDL, "table_cannot_add_field", dbField.getName(), table.getFullName(), addFieldEx.getMessage()); if (!shouldIgnoreDatabaseException()) { throw addFieldEx; } } } } } else if (createTableException != null) { session.getSessionLog().log(SessionLog.FINEST, SessionLog.DDL, "cannot_create_table", table.getFullName(), createTableException.getMessage()); if (!shouldIgnoreDatabaseException()) { throw createTableException; } } } } } createConstraints(session, schemaManager, false); schemaManager.createSequences(); session.getDatasourcePlatform().initIdentitySequences(session, DEFAULT_IDENTITY_GENERATOR); } }