blob: d42c5ea415768912cbe78e2e63b47821973dc1ee [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.jdbc.schema;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URL;
import java.security.AccessController;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.sql.DataSource;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl;
import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.jdbc.DelegatingDataSource;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.meta.MetaDataSerializer;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.util.InvalidStateException;
/**
* The SchemaTool is used to manage the database schema. Note that the
* tool never adds or drops unique constraints from existing tables, because
* JDBC {@link DatabaseMetaData} does not include information on these
* constraints.
*
* @author Abe White
* @author Patrick Linskey
*/
public class SchemaTool {
public static final String ACTION_ADD = "add";
public static final String ACTION_DROP = "drop";
public static final String ACTION_DROP_SCHEMA = "dropSchema";
public static final String ACTION_RETAIN = "retain";
public static final String ACTION_REFRESH = "refresh";
public static final String ACTION_BUILD = "build";
public static final String ACTION_REFLECT = "reflect";
public static final String ACTION_CREATEDB = "createDB";
public static final String ACTION_DROPDB = "dropDB";
public static final String ACTION_IMPORT = "import";
public static final String ACTION_EXPORT = "export";
public static final String ACTION_DELETE_TABLE_CONTENTS = "deleteTableContents";
public static final String ACTION_EXECUTE_SCRIPT = "executeScript";
public static final String[] ACTIONS = new String[]{
ACTION_ADD,
ACTION_DROP,
ACTION_DROP_SCHEMA,
ACTION_RETAIN,
ACTION_REFRESH,
ACTION_BUILD,
ACTION_REFLECT,
ACTION_CREATEDB,
ACTION_DROPDB,
ACTION_IMPORT,
ACTION_EXPORT,
ACTION_DELETE_TABLE_CONTENTS,
ACTION_EXECUTE_SCRIPT
};
protected static final Localizer _loc = Localizer.forPackage(SchemaTool.class);
protected final JDBCConfiguration _conf;
protected final DataSource _ds;
protected final Log _log;
protected final DBDictionary _dict;
private final String _action;
private boolean _ignoreErrs = false;
private boolean _openjpaTables = false;
private boolean _dropTables = true;
private boolean _dropSeqs = true;
private boolean _pks = true;
private boolean _fks = true;
private boolean _indexes = true;
private boolean _seqs = true;
private boolean _rollbackBeforeDDL = true;
private PrintWriter _writer = null;
private SchemaGroup _group = null;
private SchemaGroup _db = null;
protected boolean _fullDB = false;
protected String _sqlTerminator = ";";
protected String _scriptToExecute = null;
/**
* Default constructor. Tools constructed this way will not have an
* action, so the {@link #run()} method will be a no-op.
*/
public SchemaTool(JDBCConfiguration conf) {
this(conf, null);
}
/**
* Construct a tool to perform the given action.
*/
public SchemaTool(JDBCConfiguration conf, String action) {
if (action != null && !Arrays.asList(ACTIONS).contains(action)) {
Configurations.configureInstance(this, conf, action, action);
}
_conf = conf;
_action = action;
_ds = ACTION_BUILD.equals(action) ? null : conf.getDataSource2(null);
_log = conf.getLog(JDBCConfiguration.LOG_SCHEMA);
// initialize this up-front; otherwise the dbdictionaryfactory might
// try to take a connection to initialize when we've already got one:
// bad news if the max pool is 1
_dict = _conf.getDBDictionaryInstance();
}
/**
* Cleanup DataSource after run()/record()
*/
public void clear() {
if (_ds != null && _ds instanceof DelegatingDataSource) {
try {
((DelegatingDataSource)_ds).close();
} catch (Exception e) {
// no-op
}
}
}
/**
* The action supplied on construction.
*/
public String getAction() {
return _action;
}
/**
* If true, SQLExceptions thrown during schema manipulation will be
* printed but ignored.
*/
public boolean getIgnoreErrors() {
return _ignoreErrs;
}
/**
* If true, SQLExceptions thrown during schema manipulation will be
* printed but ignored.
*/
public void setIgnoreErrors(boolean ignoreErrs) {
_ignoreErrs = ignoreErrs;
}
/**
* Whether to act on special tables used by OpenJPA components
* for bookkeeping.
*/
public boolean getOpenJPATables() {
return _openjpaTables;
}
/**
* Whether to act on special tables used by OpenJPA components
* for bookkeeping.
*/
public void setOpenJPATables(boolean openjpaTables) {
_openjpaTables = openjpaTables;
}
/**
* If true, tables that appear to be unused will be dropped. Defaults to
* true.
*/
public boolean getDropTables() {
return _dropTables;
}
/**
* If true, tables that appear to be unused will be dropped. Defaults to
* true.
*/
public void setDropTables(boolean dropTables) {
_dropTables = dropTables;
}
/**
* If true, sequences that appear to be unused will be dropped. Defaults
* to true.
*/
public boolean getDropSequences() {
return _dropSeqs;
}
/**
* If true, sequences that appear to be unused will be dropped. Defaults
* to true.
*/
public void setDropSequences(boolean dropSeqs) {
_dropSeqs = dropSeqs;
if (dropSeqs)
setSequences(true);
}
/**
* If true, rollback will be performed before each DDL statement is executed. Defaults to true.
*/
public boolean getRollbackBeforeDDL() {
return _rollbackBeforeDDL;
}
/**
* If true, rollback will be performed before each DDL statement is executed. Defaults to true.
*/
public void setRollbackBeforeDDL(boolean rollbackBeforeDDL) {
_rollbackBeforeDDL = rollbackBeforeDDL;
}
/**
* Whether sequences should be manipulated. Defaults to true.
*/
public boolean getSequences() {
return _seqs;
}
/**
* Whether sequences should be manipulated. Defaults to true.
*/
public void setSequences(boolean seqs) {
_seqs = seqs;
}
/**
* Whether indexes on existing tables should be manipulated.
* Defaults to true.
*/
public boolean getIndexes() {
return _indexes;
}
/**
* Whether indexes on existing tables should be manipulated.
* Defaults to true.
*/
public void setIndexes(boolean indexes) {
_indexes = indexes;
}
/**
* Whether foreign keys on existing tables should be manipulated.
* Defaults to true.
*/
public boolean getForeignKeys() {
return _fks;
}
/**
* Whether foreign keys on existing tables should be manipulated.
* Defaults to true.
*/
public void setForeignKeys(boolean fks) {
_fks = fks;
}
/**
* Whether primary keys on existing tables should be manipulated.
* Defaults to true.
*/
public boolean getPrimaryKeys() {
return _pks;
}
/**
* Whether primary keys on existing tables should be manipulated.
* Defaults to true.
*/
public void setPrimaryKeys(boolean pks) {
_pks = pks;
}
/**
* The stream to write to for the creation of SQL scripts. If the
* stream is non-null, all SQL will be written to this stream rather than
* executed against the database.
*/
public Writer getWriter() {
return _writer;
}
/**
* The stream to write to for the creation of SQL scripts. If the
* stream is non-null, all SQL will be written to this stream rather than
* executed against the database.
*/
public void setWriter(Writer writer) {
if (writer == null)
_writer = null;
else if (writer instanceof PrintWriter)
_writer = (PrintWriter) writer;
else
_writer = new PrintWriter(writer);
}
public void setSQLTerminator(String t) {
_sqlTerminator = t;
}
public void setScriptToExecute(String scriptToExecute) {
_scriptToExecute = scriptToExecute;
}
/**
* Return the schema group the tool will act on.
*/
public SchemaGroup getSchemaGroup() {
return _group;
}
/**
* Set the schema group the tool will act on.
*/
public void setSchemaGroup(SchemaGroup group) {
_group = group;
}
///////////
// Actions
///////////
/**
* Run the tool action.
*/
public void run()
throws SQLException {
if (_action == null)
return;
if (ACTION_ADD.equals(_action))
add();
else if (ACTION_DROP.equals(_action))
drop();
else if (ACTION_DROP_SCHEMA.equals(_action))
dropSchema();
else if (ACTION_RETAIN.equals(_action))
retain();
else if (ACTION_REFRESH.equals(_action))
refresh();
else if (ACTION_BUILD.equals(_action))
build();
else if (ACTION_CREATEDB.equals(_action))
createDB();
else if (ACTION_DROPDB.equals(_action))
dropDB();
else if (ACTION_DELETE_TABLE_CONTENTS.equals(_action))
deleteTableContents();
else if (ACTION_EXECUTE_SCRIPT.equals(_action)) {
executeScript();
}
}
/**
* Adds any components present in the schema repository but absent from
* the database.
*/
protected void add()
throws SQLException {
add(getDBSchemaGroup(false), assertSchemaGroup());
}
/**
* Drops all schema components in the schema repository that also exist
* in the database.
*/
protected void drop()
throws SQLException {
drop(getDBSchemaGroup(false), assertSchemaGroup());
}
/**
* Drops all schema components in the schema repository.
*/
protected void dropSchema()
throws SQLException {
drop(getDBSchemaGroup(false), assertSchemaGroup(), false);
}
/**
* Drops database components that are not mentioned in the schema
* repository.
*/
protected void retain()
throws SQLException {
retain(getDBSchemaGroup(true), assertSchemaGroup(),
getDropTables(), getDropSequences());
}
/**
* Adds any components present in the schema repository but absent from
* the database, and drops unused database components.
*/
protected void refresh()
throws SQLException {
SchemaGroup local = assertSchemaGroup();
SchemaGroup db = getDBSchemaGroup(true);
retain(db, local, getDropTables(), getDropSequences());
add(db, local);
}
/**
* Re-execute all SQL used for the creation of the current database;
* this action is usually used when creating SQL scripts.
*/
protected void createDB()
throws SQLException {
SchemaGroup group = new SchemaGroup();
group.addSchema();
add(group, getDBSchemaGroup(true));
}
/**
* Re-execute all SQL used for the creation of the current database;
* this action is usually used when creating SQL scripts.
*/
protected void build()
throws SQLException {
SchemaGroup group = new SchemaGroup();
group.addSchema();
buildSchema(group, assertSchemaGroup(), true);
}
/**
* Drop the current database.
*/
protected void dropDB()
throws SQLException {
retain(getDBSchemaGroup(true), new SchemaGroup(), true, true);
}
/**
* Issue DELETE statement against all known tables.
*/
protected void deleteTableContents()
throws SQLException {
SchemaGroup group = getSchemaGroup();
Schema[] schemas = group.getSchemas();
Collection<Table> tables = new LinkedHashSet<>();
for (Schema schema : schemas) {
Table[] ts = schema.getTables();
for (Table t : ts) {
tables.add(t);
}
}
Table[] tableArray = tables.toArray(new Table[tables.size()]);
Connection conn = _ds.getConnection();
try {
String[] sql = _conf.getDBDictionaryInstance()
.getDeleteTableContentsSQL(tableArray, conn);
if (!executeSQL(sql)) {
_log.warn(_loc.get("delete-table-contents"));
}
} finally {
closeConnection(conn);
}
}
protected void executeScript() throws SQLException {
if (_scriptToExecute == null) {
_log.warn(_loc.get("generating-execute-script-not-defined"));
return;
}
URL url = AccessController.doPrivileged(
J2DoPrivHelper.getResourceAction(_conf.getClassResolverInstance().
getClassLoader(SchemaTool.class, null), _scriptToExecute));
if (url == null) {
_log.error(_loc.get("generating-execute-script-not-found", _scriptToExecute));
return;
}
_log.info(_loc.get("generating-execute-script", _scriptToExecute));
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(url.openStream()));
String sql;
List<String> script = new ArrayList<>();
while ((sql = reader.readLine()) != null) {
sql = sql.trim();
if (sql.startsWith("--") || sql.startsWith("/*") || sql.startsWith("//")) {
continue;
}
int semiColonPosition = sql.indexOf(";"); // ';' can be in string, don't blindly drop it
if (sql.endsWith(";")) {
sql = sql.substring(0, sql.length() - 1);
}
if (sql.isEmpty()) {
continue;
}
script.add(sql);
}
executeSQL(script.toArray(new String[script.size()]));
} catch (IOException e) {
_log.error(e.getMessage(), e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
_log.error(e.getMessage(), e);
}
}
}
/**
* Record the changes made to the DB in the current {@link SchemaFactory}.
*/
public void record() {
if (_db != null && _writer == null)
_conf.getSchemaFactoryInstance().storeSchema(_db);
}
/**
* Adds all database components in the repository schema that are not
* present in the given database schema to the database.
*/
protected void add(SchemaGroup db, SchemaGroup repos)
throws SQLException {
buildSchema(db, repos, true);
}
protected void buildSchema(SchemaGroup db, SchemaGroup repos, boolean considerDatabaseState) throws SQLException {
// add sequences
Schema[] schemas = repos.getSchemas();
Schema schema;
if (_seqs) {
Sequence[] seqs;
for (Schema value : schemas) {
seqs = value.getSequences();
for (Sequence seq : seqs) {
if (considerDatabaseState && db.findSequence(value, seq.getQualifiedPath()) != null) {
continue;
}
if (createSequence(seq)) {
schema = db.getSchema(seq.getSchemaIdentifier());
if (schema == null)
schema = db.addSchema(seq.getSchemaIdentifier());
schema.importSequence(seq);
}
else
_log.warn(_loc.get("add-seq", seq));
}
}
}
// order is important in this method; start with columns
Table[] tabs;
Table dbTable = null;
Column[] cols;
Column col;
DBIdentifier defaultSchemaName = DBIdentifier.newSchema(_dict.getDefaultSchemaName());
for (Schema schema2 : schemas) {
tabs = schema2.getTables();
for (Table tab : tabs) {
cols = tab.getColumns();
if (considerDatabaseState) {
dbTable = db.findTable(schema2, tab.getQualifiedPath(), defaultSchemaName);
}
for (Column column : cols) {
if (dbTable != null) {
DBIdentifier colName = column.getIdentifier();
col = dbTable.getColumn(colName);
if (col == null) {
if (addColumn(column))
dbTable.importColumn(column);
else
_log.warn(_loc.get("add-col", column,
tab));
}
else if (!column.equalsColumn(_dict, col)) {
_log.warn(_loc.get("bad-col", new Object[]{
col, dbTable, col.getDescription(),
column.getDescription()}));
}
}
}
}
}
// primary keys
if (_pks) {
PrimaryKey pk;
for (Schema value : schemas) {
tabs = value.getTables();
for (Table tab : tabs) {
pk = tab.getPrimaryKey();
if (considerDatabaseState) {
dbTable = db.findTable(value, tab.getQualifiedPath());
}
if (pk != null && !pk.isLogical() && dbTable != null) {
if (dbTable.getPrimaryKey() == null
&& addPrimaryKey(pk))
dbTable.importPrimaryKey(pk);
else if (dbTable.getPrimaryKey() == null)
_log.warn(_loc.get("add-pk", pk, tab));
else if (!pk.equalsPrimaryKey(dbTable.getPrimaryKey()))
_log.warn(_loc.get("bad-pk",
dbTable.getPrimaryKey(), dbTable));
}
}
}
}
// tables
Set<Table> newTables = new HashSet<>();
for (Schema schema1 : schemas) {
tabs = schema1.getTables();
for (Table tab : tabs) {
if (considerDatabaseState && db.findTable(schema1, tab.getQualifiedPath()) != null) {
continue;
}
if (createTable(tab)) {
newTables.add(tab);
schema = db.getSchema(tab.getSchemaIdentifier());
if (schema == null)
schema = db.addSchema(tab.getSchemaIdentifier());
schema.importTable(tab);
}
else
_log.warn(_loc.get("add-table", tab));
}
}
// indexes
Index[] idxs;
Index idx;
for (Schema element : schemas) {
tabs = element.getTables();
for (Table tab : tabs) {
// create indexes on new tables even if indexes
// have been turned off
if (!_indexes && !newTables.contains(tab))
continue;
idxs = tab.getIndexes();
if (considerDatabaseState) {
dbTable = db.findTable(element, tab.getQualifiedPath());
}
for (Index index : idxs) {
if (dbTable != null) {
idx = findIndex(dbTable, index);
if (idx == null) {
if (createIndex(index, dbTable, tab.getUniques()))
dbTable.importIndex(index);
else
_log.warn(_loc.get("add-index", index,
tab));
}
else if (!index.equalsIndex(idx))
_log.warn(_loc.get("bad-index", idx, dbTable));
}
}
}
}
// Unique Constraints on group of columns
Unique[] uniques;
for (Schema item : schemas) {
tabs = item.getTables();
for (Table tab : tabs) {
// create unique constraints only on new tables
if (!newTables.contains(tab)) {
continue;
}
uniques = tab.getUniques();
if (uniques == null || uniques.length == 0)
continue;
if (considerDatabaseState) {
dbTable = db.findTable(tab);
}
if (dbTable == null)
continue;
for (Unique unique : uniques) {
dbTable.importUnique(unique);
}
}
}
// foreign keys
ForeignKey[] fks;
ForeignKey fk;
for (Schema value : schemas) {
tabs = value.getTables();
for (Table tab : tabs) {
// create foreign keys on new tables even if fks
// have been turned off
if (!_fks && !newTables.contains(tab)) {
continue;
}
fks = tab.getForeignKeys();
if (considerDatabaseState) {
dbTable = db.findTable(value, tab.getQualifiedPath());
}
for (ForeignKey foreignKey : fks) {
if (!foreignKey.isLogical() && dbTable != null) {
fk = findForeignKey(dbTable, foreignKey);
if (fk == null) {
if (addForeignKey(foreignKey))
dbTable.importForeignKey(foreignKey);
else
_log.warn(_loc.get("add-fk",
foreignKey, tab));
}
else if (!foreignKey.equalsForeignKey(fk))
_log.warn(_loc.get("bad-fk", fk, dbTable));
}
}
}
}
}
/**
* Drops all database components that are in the given database schema
* but not in the repository schema.
*/
protected void retain(SchemaGroup db, SchemaGroup repos, boolean tables,
boolean sequences)
throws SQLException {
Schema[] schemas = db.getSchemas();
if (_seqs && sequences) {
Sequence[] seqs;
for (Schema schema : schemas) {
seqs = schema.getSequences();
for (Sequence seq : seqs) {
if (!isDroppable(seq))
continue;
if (repos.findSequence(seq) == null) {
if (dropSequence(seq))
schema.removeSequence(seq);
else
_log.warn(_loc.get("drop-seq", seq));
}
}
}
}
// order is important in this method; start with foreign keys
Table[] tabs;
Table reposTable;
if (_fks) {
ForeignKey[] fks;
ForeignKey fk;
for (Schema schema : schemas) {
tabs = schema.getTables();
for (Table tab : tabs) {
if (!isDroppable(tab))
continue;
fks = tab.getForeignKeys();
reposTable = repos.findTable(tab);
if (!tables && reposTable == null)
continue;
for (ForeignKey foreignKey : fks) {
if (foreignKey.isLogical())
continue;
fk = null;
if (reposTable != null)
fk = findForeignKey(reposTable, foreignKey);
if (reposTable == null || fk == null
|| !foreignKey.equalsForeignKey(fk)) {
if (dropForeignKey(foreignKey))
tab.removeForeignKey(foreignKey);
else
_log.warn(_loc.get("drop-fk", foreignKey,
tab));
}
}
}
}
}
// primary keys
if (_pks) {
PrimaryKey pk;
for (Schema schema : schemas) {
tabs = schema.getTables();
for (Table tab : tabs) {
if (!isDroppable(tab))
continue;
pk = tab.getPrimaryKey();
if (pk != null && pk.isLogical())
continue;
reposTable = repos.findTable(tab);
if (pk != null && reposTable != null
&& (reposTable.getPrimaryKey() == null
|| !pk.equalsPrimaryKey(reposTable.getPrimaryKey()))) {
if (dropPrimaryKey(pk))
tab.removePrimaryKey();
else
_log.warn(_loc.get("drop-pk", pk, tab));
}
}
}
}
// columns
Column[] cols;
Column col;
Collection<Table> drops = new LinkedList<>();
for (Schema value : schemas) {
tabs = value.getTables();
for (Table tab : tabs) {
if (!isDroppable(tab))
continue;
cols = tab.getColumns();
reposTable = repos.findTable(tab);
if (reposTable != null) {
for (Column column : cols) {
col = reposTable.getColumn(column.getIdentifier());
if (col == null || !column.equalsColumn(_dict, col)) {
if (tab.getColumns().length == 1)
drops.add(tab);
else if (dropColumn(column))
tab.removeColumn(column);
else
_log.warn(_loc.get("drop-col", column,
tab));
}
}
}
}
}
// now tables
if (tables) {
for (Schema schema : schemas) {
tabs = schema.getTables();
for (Table tab : tabs)
if (isDroppable(tab)
&& repos.findTable(tab) == null)
drops.add(tab);
}
}
dropTables(drops, db);
}
protected void drop(SchemaGroup db, SchemaGroup repos)
throws SQLException {
drop(db, repos, true);
}
/**
* Drops all database components in the given repository schema.
*/
private void drop(SchemaGroup db, SchemaGroup repos, boolean considerDatabaseState)
throws SQLException {
// drop sequences
Schema[] schemas = repos.getSchemas();
if (_seqs) {
Sequence[] seqs;
Sequence dbSeq;
for (Schema schema : schemas) {
seqs = schema.getSequences();
for (Sequence seq : seqs) {
if (!isDroppable(seq))
continue;
dbSeq = db.findSequence(seq);
if (dbSeq != null) {
if (dropSequence(seq))
dbSeq.getSchema().removeSequence(dbSeq);
else
_log.warn(_loc.get("drop-seq", seq));
}
else if (_writer != null) {
dropSequence(seq);
}
}
}
}
// calculate tables to drop; we can only drop tables if we're sure
// the user listed the entire table definition in the stuff they want
// dropped; else they might just want to drop a few columns
Collection<Table> drops = new LinkedList<>();
Table[] tabs;
Table dbTable;
Column[] dbCols;
for (Schema value : schemas) {
tabs = value.getTables();
tables:
for (Table tab : tabs) {
if (!isDroppable(tab))
continue;
if (!considerDatabaseState) {
drops.add(tab);
continue;
}
dbTable = db.findTable(tab);
if (dbTable == null) {
if (_writer != null) {
drops.add(tab);
}
continue;
}
dbCols = dbTable.getColumns();
for (Column dbCol : dbCols) {
if (!dbCol.getIdentifier().getName().equals(_dict.getIdentityColumnName()) &&
!tab.containsColumn(dbCol))
continue tables;
}
drops.add(tab);
}
}
// order is important in this method; start with foreign keys mentioned
// in the drop schema
if (_fks) {
ForeignKey[] fks;
ForeignKey fk;
for (Schema schema : schemas) {
tabs = schema.getTables();
for (Table tab : tabs) {
if (!isDroppable(tab))
continue;
fks = tab.getForeignKeys();
dbTable = db.findTable(tab);
for (ForeignKey foreignKey : fks) {
if (foreignKey.isLogical())
continue;
fk = null;
if (dbTable != null)
fk = findForeignKey(dbTable, foreignKey);
if (dbTable == null || fk == null)
continue;
if (dropForeignKey(foreignKey))
if (dbTable != null)
dbTable.removeForeignKey(fk);
else
_log.warn(_loc.get("drop-fk", foreignKey, tab));
}
}
}
// also drop imported foreign keys for tables that will be dropped
Table tab;
for (Table drop : drops) {
tab = drop;
dbTable = db.findTable(tab);
if (dbTable == null)
continue;
fks = db.findExportedForeignKeys(dbTable.getPrimaryKey());
for (ForeignKey foreignKey : fks) {
if (dropForeignKey(foreignKey))
dbTable.removeForeignKey(foreignKey);
else
_log.warn(_loc.get("drop-fk", foreignKey, dbTable));
}
}
}
// drop the tables we calculated above
dropTables(drops, db);
if (considerDatabaseState) {
// columns
Column[] cols;
Column col;
for (Schema schema : schemas) {
tabs = schema.getTables();
for (Table tab : tabs) {
if (!isDroppable(tab))
continue;
cols = tab.getColumns();
dbTable = db.findTable(tab);
for (Column column : cols) {
col = null;
if (dbTable != null)
col = dbTable.getColumn(column.getIdentifier());
if (dbTable == null || col == null)
continue;
if (dropColumn(column)) {
dbTable.removeColumn(col);
}
}
}
}
}
}
/**
* Return true if the table is droppable.
*/
protected boolean isDroppable(Table table) {
return _openjpaTables
|| (!DBIdentifier.toUpper(table.getIdentifier()).getName().startsWith("OPENJPA_")
&& !DBIdentifier.toUpper(table.getIdentifier()).getName().startsWith("JDO_")); // legacy
}
/**
* Return true if the sequence is droppable.
*/
protected boolean isDroppable(Sequence seq) {
return _openjpaTables
|| (!DBIdentifier.toUpper(seq.getIdentifier()).getName().startsWith("OPENJPA_")
&& !DBIdentifier.toUpper(seq.getIdentifier()).getName().startsWith("JDO_")); // legacy
}
/**
* Find an index in the given table that matches the given one.
*/
protected Index findIndex(Table dbTable, Index idx) {
Index[] idxs = dbTable.getIndexes();
for (Index index : idxs)
if (idx.columnsMatch(index.getColumns()))
return index;
return null;
}
/**
* Find a foreign key in the given table that matches the given one.
*/
protected ForeignKey findForeignKey(Table dbTable, ForeignKey fk) {
if (fk.getConstantColumns().length > 0
|| fk.getConstantPrimaryKeyColumns().length > 0)
return null;
ForeignKey[] fks = dbTable.getForeignKeys();
for (ForeignKey foreignKey : fks)
if (fk.columnsMatch(foreignKey.getColumns(),
foreignKey.getPrimaryKeyColumns()))
return foreignKey;
return null;
}
/**
* Remove the given collection of tables from the database schema. Orders
* the removals according to foreign key constraints on the tables.
*/
protected void dropTables(Collection<Table> tables, SchemaGroup change)
throws SQLException {
if (tables.isEmpty())
return;
Table table;
Table changeTable;
for (Table value : tables) {
table = value;
if (dropTable(table)) {
changeTable = change.findTable(table);
if (changeTable != null)
changeTable.getSchema().removeTable(changeTable);
}
else
_log.warn(_loc.get("drop-table", table));
}
}
/**
* Add the given table to the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean createTable(Table table)
throws SQLException {
return executeSQL(_dict.getCreateTableSQL(table, _db));
}
/**
* Drop the given table from the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean dropTable(Table table)
throws SQLException {
return executeSQL(_dict.getDropTableSQL(table));
}
/**
* Add the given sequence to the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean createSequence(Sequence seq)
throws SQLException {
return executeSQL(_dict.getCreateSequenceSQL(seq));
}
/**
* Drop the given sequence from the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean dropSequence(Sequence seq)
throws SQLException {
return executeSQL(_dict.getDropSequenceSQL(seq));
}
/**
* Add the given index to the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean createIndex(Index idx, Table table)
throws SQLException {
return createIndex(idx, table, null);
}
public boolean createIndex(Index idx, Table table, Unique[] uniques)
throws SQLException {
// Informix will automatically create a unique index for the
// primary key, so don't create another index again
if (!_dict.needsToCreateIndex(idx,table,uniques))
return false;
int max = _dict.maxIndexesPerTable;
int len = table.getIndexes().length;
if (table.getPrimaryKey() != null)
len += table.getPrimaryKey().getColumns().length;
if (len >= max) {
_log.warn(_loc.get("too-many-indexes", idx, table, max + ""));
return false;
}
return executeSQL(_dict.getCreateIndexSQL(idx));
}
/**
* Drop the given index from the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean dropIndex(Index idx)
throws SQLException {
return executeSQL(_dict.getDropIndexSQL(idx));
}
/**
* Add the given column to the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean addColumn(Column col)
throws SQLException {
return executeSQL(_dict.getAddColumnSQL(col));
}
/**
* Drop the given column from the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean dropColumn(Column col)
throws SQLException {
return executeSQL(_dict.getDropColumnSQL(col));
}
/**
* Add the given primary key to the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean addPrimaryKey(PrimaryKey pk)
throws SQLException {
return executeSQL(_dict.getAddPrimaryKeySQL(pk));
}
/**
* Drop the given primary key from the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean dropPrimaryKey(PrimaryKey pk)
throws SQLException {
return executeSQL(_dict.getDropPrimaryKeySQL(pk));
}
/**
* Add the given foreign key to the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean addForeignKey(ForeignKey fk)
throws SQLException {
return executeSQL(_dict.getAddForeignKeySQL(fk));
}
/**
* Drop the given foreign key from the database schema.
*
* @return true if the operation was successful, false otherwise
*/
public boolean dropForeignKey(ForeignKey fk)
throws SQLException {
Connection conn = _ds.getConnection();
try {
return executeSQL(_dict.getDropForeignKeySQL(fk,conn));
} finally {
closeConnection(conn);
}
}
/**
* Return the database schema.
*/
public SchemaGroup getDBSchemaGroup() {
try {
return getDBSchemaGroup(true);
} catch (SQLException se) {
throw SQLExceptions.getStore(se, _dict);
}
}
/**
* Set the database schema.
*/
public void setDBSchemaGroup(SchemaGroup db) {
_db = db;
if (db != null)
_fullDB = true;
}
/**
* Return the database schema.
*
* @param full if false, only the tables named in the set schema
* repository will be generated
*/
protected SchemaGroup getDBSchemaGroup(boolean full)
throws SQLException {
if (_db == null || (full && !_fullDB)) {
SchemaGenerator gen = new SchemaGenerator(_conf);
gen.setPrimaryKeys(_pks);
gen.setForeignKeys(_fks);
gen.setIndexes(_indexes);
if (full)
gen.generateSchemas();
else {
// generate only the tables in the given repository
// group; some may not exist yet, which is OK; we just need
// to make sure we can detect the changes to the ones that
// do exist
Collection<DBIdentifier> tables = new LinkedList<>();
SchemaGroup group = assertSchemaGroup();
Schema[] schemas = group.getSchemas();
Table[] tabs;
for (Schema schema : schemas) {
tabs = schema.getTables();
for (Table tab : tabs) {
if (DBIdentifier.isNull(tab.getSchemaIdentifier())) {
tables.add(tab.getIdentifier());
}
else {
DBIdentifier sName = tab.getFullIdentifier();
tables.add(sName);
}
}
}
if (!tables.isEmpty())
gen.generateSchemas(tables.toArray(new DBIdentifier[tables.size()]));
}
_db = gen.getSchemaGroup();
}
return _db;
}
protected SchemaGroup assertSchemaGroup() {
SchemaGroup local = getSchemaGroup();
if (local == null)
throw new InvalidStateException(_loc.get("tool-norepos"));
return local;
}
/////////////
// Utilities
/////////////
/**
* Executes the given array of non-selecting SQL statements, correctly
* logging the SQL calls and optionally ignoring errors.
*
* @return true if there was SQL to execute and the calls were
* successful, false otherwise
*/
protected boolean executeSQL(String[] sql)
throws SQLException {
// if no sql, probably b/c dictionary doesn't support operation
if (sql.length == 0)
return false;
boolean err = false;
if (_writer == null) {
// this is outside the try-catch because a failure here is
// really bad, and should not be ignored.
Connection conn = _ds.getConnection();
Statement statement = null;
boolean wasAuto = true;
try {
if (_rollbackBeforeDDL) {
wasAuto = conn.getAutoCommit();
if (!wasAuto) {
conn.setAutoCommit(true);
}
}
for (String s : sql) {
try {
if (_rollbackBeforeDDL) {
// some connections require that rollback be
// called on the connection before any DDL statements
// can be run on it, even when autocommit is on.
// This is sometimes because the connection does not
// allow DDL statements when there are multiple
// commands issued on the connection, and the
// connection pool may have issued some validation SQL.
try {
conn.rollback();
}
catch (Exception e) {
}
}
statement = conn.createStatement();
statement.executeUpdate(s);
// some connections seem to require an explicit
// commit for DDL statements, even when autocommit
// is on. The DataDirect drivers seem to suffer from
// this limitation.
try {
conn.commit();
}
catch (Exception e) {
}
}
catch (SQLException se) {
err = true;
handleException(se);
}
finally {
if (statement != null)
try {
statement.close();
}
catch (SQLException se) {
}
}
}
}
finally {
if (_rollbackBeforeDDL && !wasAuto) {
conn.setAutoCommit(false);
}
try {
closeConnection(conn);
} catch (SQLException se) {
//X TODO why catch silently?
}
}
} else {
for (String s : sql) {
_writer.println(s + _sqlTerminator);
}
_writer.flush();
}
return !err;
}
/**
* Handle the given exception, logging it and optionally ignoring it,
* depending on the flags this SchemaTool was created with.
*/
protected void handleException(SQLException sql)
throws SQLException {
if (!_ignoreErrs)
throw sql;
_log.warn(sql.getMessage(), sql);
}
////////
// Main
////////
/**
* Usage: java org.apache.openjpa.jdbc.schema.SchemaTool [option]*
* [-action/-a &lt;add | retain | drop | refresh | createDB | dropDB
* | build | reflect | import | export&gt;]
* &lt;.schema file or resource&gt;*
* Where the following options are recognized.
* <ul>
* <li><i>-properties/-p &lt;properties file or resource&gt;</i>: The
* path or resource name of a OpenJPA properties file containing
* information such as the license key and connection data as
* outlined in {@link JDBCConfiguration}. Optional.</li>
* <li><i>-&lt;property name&gt; &lt;property value&gt;</i>: All bean
* properties of the OpenJPA {@link JDBCConfiguration} can be set by
* using their names and supplying a value. For example:
* <code>-licenseKey adslfja83r3lkadf</code></li>
* <li><i>-ignoreErrors/-i &lt;true/t | false/f&gt;</i>: If false, an
* exception will will be thrown if the tool encounters any database
* exceptions; defaults to <code>false</code>.</li>
* <li><i>-file/-f &lt;stdout | output file or resource&gt;</i>: Use this
* option to write a SQL script for the planned schema modifications,
* rather than committing them to the database. This option also
* applies to the <code>export</code> and <code>reflect</code> actions.</li>
* <li><i>-openjpaTables/-kt &lt;true/t | false/f&gt;</i>: Under the
* <code>reflect</code> action, whether to reflect on tables with
* the name <code>OPENJPA_*</code>. Under other actions, whether to
* drop such tables. Defaults to <code>false</code>.</li>
* <li><i>-dropTables/-dt &lt;true/t | false/f&gt;</i>: Set this option to
* true to drop tables that appear to be unused during
* <code>retain</code> and <code>refresh</code> actions. Defaults to
* <code>true</code>.</li>
* <li><i>-dropSequences/-dsq &lt;true/t | false/f&gt;</i>: Set this option
* to true to drop sequences that appear to be unused during
* <code>retain</code> and <code>refresh</code> actions. Defaults to
* <code>true</code>.</li>
* <li><i>-rollbackBeforeDDL/-rbddl &lt;true/t | false/f&gt;</i>: Set this option
* to true to send an initail rollback on the connection before any DDL statement
* is sent</li>
* <li><i>-primaryKeys/-pk &lt;true/t | false/f&gt;</i>: Whether primary
* keys on existing tables are manipulated. Defaults to true.</li>
* <li><i>-foreignKeys/-fk &lt;true/t | false/f&gt;</i>: Whether foreign
* keys on existing tables are manipulated. Defaults to true.</li>
* <li><i>-indexes/-ix &lt;true/t | false/f&gt;</i>: Whether indexes
* on existing tables are manipulated. Defaults to true.</li>
* <li><i>-sequences/-sq &lt;true/t | false/f&gt;</i>: Whether to
* manipulate sequences. Defaults to true.</li>
* <li><i>-record/-r &lt;true/t | false/f&gt;</i>: Set this option to
* <code>false</code> to prevent writing the schema changes to the
* current {@link SchemaFactory}.</li>
* </ul>
* Actions can be composed in a comma-separated list. The various actions
* are as follows.
* <ul>
* <li><i>add</i>: Bring the schema up-to-date with the latest
* changes to the schema XML data by adding tables, columns,
* indexes, etc. This action never drops any data. This is the
* default action.</li>
* <li><i>retain</i>: Keep all schema components in the schema XML, but
* drop the rest from the database. This action never adds any data.</li>
* <li><i>drop</i>: Drop all the schema components in the schema XML.</li>
* <li><i>refresh</i>: Equivalent to retain, then add.</li>
* <li><i>createDB</i>: Execute SQL to re-create the current database.
* This action is typically used in conjuction with the
* <code>file</code> option.</li>
* <li><i>build</i>: Execute SQL to build the schema defined in the XML.
* Because it doesn't take the current database schema into account,
* this action is typically used in conjuction with the
* <code>file</code> option.</li>
* <li><i>reflect</i>: Reflect on the current database schema. Write the
* schema's XML representation to the file specified with the
* <code>file</code> option, or to stdout if no file is given.</li>
* <li><i>dropDB</i>: Execute SQL to drop the current database. This
* action implies <code>dropTables</code>.</li>
* <li><i>deleteTableContents</i>: Execute SQL to delete all rows from
* all tables that OpenJPA knows about.</li>
* <li><i>import</i>: Import the given XML schema definition into the
* current {@link SchemaFactory}.</li>
* <li><i>export</i>: Export the current {@link SchemaFactory}'s recorded
* schema to an XML schema definition file.</li>
* </ul>
* Examples:
* <ul>
* <li>Write a script to stdout to re-create the current database
* schema:<br />
* <code>java org.apache.openjpa.jdbc.schema.SchemaTool -f stdout
* -a createDB</code></li>
* <li>Drop the current database schema:<br />
* <code>java org.apache.openjpa.jdbc.schema.SchemaTool
* -a dropDB</code></li>
* <li>Refresh the schema and delete all records in all tables:<br />
* <code>java org.apache.openjpa.jdbc.schema.SchemaTool
* -a refresh,deleteTableContents</code></li>
* <li>Create a schema based on an XML schema definition file:<br />
* <code>java org.apache.openjpa.jdbc.schema.SchemaTool
* myschema.xml</code></li>
* </ul>
*/
public static void main(String[] args)
throws IOException, SQLException {
Options opts = new Options();
final String[] arguments = opts.setFromCmdLine(args);
boolean ret = Configurations.runAgainstAllAnchors(opts,
new Configurations.Runnable() {
@Override
public boolean run(Options opts) throws Exception {
JDBCConfiguration conf = new JDBCConfigurationImpl();
try {
return SchemaTool.run(conf, arguments, opts);
} finally {
conf.close();
}
}
});
if (!ret) {
// START - ALLOW PRINT STATEMENTS
System.out.println(_loc.get("tool-usage"));
// STOP - ALLOW PRINT STATEMENTS
}
}
/**
* Run the tool. Returns false if any invalid options were given.
*
* @see #main
*/
public static boolean run(JDBCConfiguration conf, String[] args,
Options opts)
throws IOException, SQLException {
Flags flags = new Flags();
flags.dropTables = opts.removeBooleanProperty
("dropTables", "dt", flags.dropTables);
flags.dropSequences = opts.removeBooleanProperty
("dropSequences", "dsq", flags.dropSequences);
flags.rollbackBeforeDDL = opts.removeBooleanProperty
("rollbackBeforeDDL", "rbddl", flags.rollbackBeforeDDL);
flags.ignoreErrors = opts.removeBooleanProperty
("ignoreErrors", "i", flags.ignoreErrors);
flags.openjpaTables = opts.removeBooleanProperty
("openjpaTables", "ot", flags.openjpaTables);
flags.primaryKeys = opts.removeBooleanProperty
("primaryKeys", "pk", flags.primaryKeys);
flags.foreignKeys = opts.removeBooleanProperty
("foreignKeys", "fks", flags.foreignKeys);
flags.indexes = opts.removeBooleanProperty
("indexes", "ix", flags.indexes);
flags.sequences = opts.removeBooleanProperty
("sequences", "sq", flags.sequences);
flags.record = opts.removeBooleanProperty("record", "r", flags.record);
String fileName = opts.removeProperty("file", "f", null);
String schemas = opts.removeProperty("s");
if (schemas != null)
opts.setProperty("schemas", schemas);
String[] actions = opts.removeProperty("action", "a", flags.action)
.split(",");
// setup a configuration instance with cmd-line info
Configurations.populateConfiguration(conf, opts);
// create script writer
ClassLoader loader = conf.getClassResolverInstance().
getClassLoader(SchemaTool.class, null);
flags.writer = Files.getWriter(fileName, loader);
boolean returnValue = true;
for (String action : actions) {
flags.action = action;
returnValue &= run(conf, args, flags, loader);
}
return returnValue;
}
/**
* Run the tool. Return false if invalid options were given.
*/
public static boolean run(JDBCConfiguration conf, String[] args,
Flags flags, ClassLoader loader)
throws IOException, SQLException {
Log log = conf.getLog(OpenJPAConfiguration.LOG_TOOL);
if (ACTION_REFLECT.equals(flags.action)) {
if (args.length > 0)
return false;
if (flags.writer == null)
flags.writer = new PrintWriter(System.out);
SchemaGenerator gen = new SchemaGenerator(conf);
gen.setPrimaryKeys(flags.primaryKeys);
gen.setIndexes(flags.indexes);
gen.setForeignKeys(flags.foreignKeys);
gen.setSequences(flags.sequences);
gen.setOpenJPATables(flags.openjpaTables);
String schemas = conf.getSchemas();
if (StringUtil.isEmpty(schemas))
schemas = "all";
log.info(_loc.get("sch-reflect", schemas));
gen.generateSchemas();
// record the schema
log.info(_loc.get("sch-reflect-write"));
SchemaSerializer ser = new XMLSchemaSerializer(conf);
ser.addAll(gen.getSchemaGroup());
ser.serialize(flags.writer, MetaDataSerializer.PRETTY);
return true;
}
if (args.length == 0
&& !ACTION_CREATEDB.equals(flags.action)
&& !ACTION_DROPDB.equals(flags.action)
&& !ACTION_EXPORT.equals(flags.action)
&& !ACTION_DELETE_TABLE_CONTENTS.equals(flags.action))
return false;
// parse in the arguments
SchemaParser parser = new XMLSchemaParser(conf);
parser.setDelayConstraintResolve(true);
File file;
for (String arg : args) {
file = Files.getFile(arg, loader);
log.info(_loc.get("tool-running", file));
parser.parse(file);
}
parser.resolveConstraints();
if (ACTION_IMPORT.equals(flags.action)) {
log.info(_loc.get("tool-import-store"));
SchemaGroup schema = parser.getSchemaGroup();
conf.getSchemaFactoryInstance().storeSchema(schema);
return true;
}
if (ACTION_EXPORT.equals(flags.action)) {
if (flags.writer == null)
flags.writer = new PrintWriter(System.out);
log.info(_loc.get("tool-export-gen"));
SchemaGroup schema = conf.getSchemaFactoryInstance().readSchema();
log.info(_loc.get("tool-export-write"));
SchemaSerializer ser = new XMLSchemaSerializer(conf);
ser.addAll(schema);
ser.serialize(flags.writer, MetaDataSerializer.PRETTY);
return true;
}
SchemaTool tool = new SchemaTool(conf, flags.action);
tool.setIgnoreErrors(flags.ignoreErrors);
tool.setDropTables(flags.dropTables);
tool.setSequences(flags.sequences); // set before dropseqs
tool.setDropSequences(flags.dropSequences);
tool.setRollbackBeforeDDL(flags.rollbackBeforeDDL);
tool.setPrimaryKeys(flags.primaryKeys);
tool.setForeignKeys(flags.foreignKeys);
tool.setIndexes(flags.indexes);
tool.setOpenJPATables(flags.openjpaTables);
if (args.length > 0)
tool.setSchemaGroup(parser.getSchemaGroup());
if (flags.writer != null)
tool.setWriter(flags.writer);
log.info(_loc.get("tool-action", flags.action));
try {
tool.run();
} finally {
if (flags.record) {
log.info(_loc.get("tool-record"));
tool.record();
}
}
if (flags.writer != null)
flags.writer.flush();
return true;
}
private void closeConnection(Connection conn) throws SQLException {
if (conn != null && !conn.isClosed()) {
conn.close();
}
}
/**
* Run flags.
*/
public static class Flags {
public String action = ACTION_ADD;
public Writer writer = null;
public boolean dropTables = true;
public boolean rollbackBeforeDDL = true;
public boolean dropSequences = true;
public boolean ignoreErrors = false;
public boolean openjpaTables = false;
public boolean primaryKeys = true;
public boolean foreignKeys = true;
public boolean indexes = true;
public boolean sequences = true;
public boolean record = true;
}
}