/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.core.db.ant.dbupgrade;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import mazz.i18n.Msg;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.rhq.core.db.DatabaseType;
import org.rhq.core.db.DatabaseTypeFactory;
import org.rhq.core.db.DbUtil;
import org.rhq.core.db.TypeMap;
import org.rhq.core.db.ant.DbAntI18NFactory;
import org.rhq.core.db.ant.DbAntI18NResourceKeys;
/**
* ANT task that performs database upgrades of an existing schema.
*/
public class DBUpgrader extends Task {
private static final Msg MSG = DbAntI18NFactory.getMsg();
private static final String SCHEMA_MOD_IN_PROGRESS = " *** UPGRADE IN PROGRESS: migrating to version ";
private List<SchemaSpec> schemaSpecs = new ArrayList<SchemaSpec>();
private String jdbcUrl;
private String jdbcUser;
private String jdbcPassword;
// The query to find the existing schema version uses these.
// It is of the form: SELECT valueColumn FROM tableName WHERE keyColumn = keyMatch
private String valueColumn;
private String tableName;
private String keyColumn;
private String keyMatch;
private File typeMapFile;
private Collection<TypeMap> typeMaps;
private String startSchemaVersionString;
private SchemaVersion startSchemaVersion;
private String targetSchemaVersionString;
private SchemaVersion targetSchemaVersion;
private DatabaseType databaseType;
private Connection connection;
private boolean doCloseConnection;
public DBUpgrader() {
doCloseConnection = true;
}
public DBUpgrader(Connection connection) {
this.connection = connection;
doCloseConnection = false;
}
/**
* The URL to the database that is to be upgraded.
*
* @param jdbcUrl
*/
public void setJdbcUrl(String jdbcUrl) {
this.jdbcUrl = jdbcUrl;
}
/**
* Log into the database as this user.
*
* @param jdbcUser
*/
public void setJdbcUser(String jdbcUser) {
this.jdbcUser = jdbcUser;
}
/**
* The database user's credentials used to log into the database.
*
* @param jdbcPassword
*/
public void setJdbcPassword(String jdbcPassword) {
this.jdbcPassword = jdbcPassword;
}
/**
* The column in the {@link #setTable(String) table} whose value is the schema version. The value in this column
* tells you what version the schema is currently at.
*
* @param v value column name
*/
public void setValueColumn(String v) {
valueColumn = v;
}
/**
* The table name that contains the row where the schema version can be found. This table must have a
* {@link #setValueColumn(String) value column} whose value is the schema version and it must have a
* {@link #setKeyColumn(String) key column} whose value matches {@link #setKeyMatch(String)} (which simply
* identifies the row that contains the schema version value).
*
* @param t table name
*/
public void setTable(String t) {
tableName = t;
}
/**
* This is the name of the column in the {@link #setTable(String) table} that identifies the row that has the schema
* version in it. Finding the row where this key column's value is {@link #setKeyMatch(String)} will get you the row
* whose value in the {@link #setValueColumn(String) value column} is the schema version.
*
* @param k key column name
*/
public void setKeyColumn(String k) {
keyColumn = k;
}
/**
* This is the value of the {@link #setKeyColumn(String) key column} that identifies the row whose
* {@link #setValueColumn(String) value column} is the schema version.
*
* @param m key value identifying the row that has the schema version
*/
public void setKeyMatch(String m) {
keyMatch = m;
}
/**
* A file that contains database type mappings. This is rarely going to be used, since database type mappings never
* change (unless future databases add new types or change the semantics of their current types).
*
* @param f mapping file
*/
public void setTypeMap(File f) {
this.typeMapFile = f;
}
/**
* This is the schema version that the schema should be upgraded to.
*
* @param v schema version
*/
public void setTargetSchemaVersion(String v) {
this.targetSchemaVersionString = v;
}
/**
* Creates a new schema spec object.
*
* @return new schema spec
*/
public SchemaSpec createSchemaSpec() {
SchemaSpec ss = new SchemaSpec(this);
schemaSpecs.add(ss);
return ss;
}
/**
* Returns the collection of database type mappings.
*
* @return collection of type mappings
*/
public Collection<TypeMap> getTypeMaps() {
return typeMaps;
}
/**
* @see org.apache.tools.ant.Task#execute()
*/
@Override
public void execute() throws BuildException {
validateAttributes();
List<SchemaSpec> newSpecs = new ArrayList<SchemaSpec>();
newSpecs.addAll(schemaSpecs);
// Sort the schema specs - if any reordering occurred, consider that
// an error. Also, if there are any duplicate versions, that's an error
Collections.sort(newSpecs);
int size = schemaSpecs.size();
for (int i = 0; i < size; i++) {
if (!newSpecs.get(i).equals(schemaSpecs.get(i))) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_SCHEMA_SPECS_OUT_OF_ORDER,
schemaSpecs.get(i).getVersion()));
}
if ((i > 0) && newSpecs.get(i).equals(newSpecs.get(i - 1))) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_DUPLICATE_SCHEMA_SPECS,
newSpecs.get(i).getVersion()));
}
}
Connection conn = null;
try {
// Connect to the database to grab the starting schema version
conn = getConnection();
databaseType = DatabaseTypeFactory.getDatabaseType(conn);
startSchemaVersionString = loadStartSchemaVersion(conn, databaseType);
if (startSchemaVersionString.indexOf(SCHEMA_MOD_IN_PROGRESS) != -1) {
// try to fix this by making the schema version the last successful version
try {
String ver = startSchemaVersionString.substring(0, startSchemaVersionString
.indexOf(SCHEMA_MOD_IN_PROGRESS));
updateSchemaVersion(conn, databaseType, ver);
conn.commit();
} catch (Exception e) {
log(e.toString());
}
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_INCONSISTENT_STATE,
startSchemaVersionString));
}
try {
startSchemaVersion = new SchemaVersion(startSchemaVersionString);
} catch (IllegalArgumentException e) {
throw new BuildException(e.getMessage(), e);
}
log(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_STARTING, startSchemaVersion, targetSchemaVersion));
// If the target version is LATEST, then figure out the "real" target version.
String realTargetSchemaVersion = targetSchemaVersion.toString();
if (targetSchemaVersion.getIsLatest()) {
SchemaSpec latestSpec = schemaSpecs.get(size - 1);
realTargetSchemaVersion = latestSpec.getVersion().toString();
}
// Ensure that we're not trying to "downgrade" - that is,
// ensure that the target version is not earlier than the
// existing server version. In particular, if the target
// version is LATEST but the actual latest SchemaSpec is
// earlier than the current database's schema version, we
// consider that a downgrade as well.
if (targetSchemaVersion.compareTo(startSchemaVersion) < 0) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_DOWNGRADING,
startSchemaVersion, realTargetSchemaVersion));
}
size = schemaSpecs.size();
SchemaSpec ss;
conn.setAutoCommit(false);
SchemaVersion fromVersion = startSchemaVersion;
SchemaVersion toVersion;
for (int i = 0; i < size; i++) {
ss = schemaSpecs.get(i);
toVersion = ss.getVersion();
if (!shouldExecSpecVersion(toVersion)) {
continue;
}
log(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_UPGRADE_STEP, fromVersion, toVersion));
try {
markSchemaModificationInProgress(conn, databaseType, fromVersion, toVersion);
ss.initialize(conn, this);
ss.execute();
updateSchemaVersion(conn, databaseType, toVersion.toString());
conn.commit();
log(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_UPGRADE_STEP_DONE, fromVersion, toVersion));
fromVersion = toVersion;
} catch (Exception e) {
try {
conn.rollback();
} catch (Exception e2) {
log("rollback() exception: " + e2.toString());
}
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_UPGRADE_STEP_ERROR, ss
.getVersion(), e), e);
}
}
// If this was a "upgrade to latest", then ensure that
// the schema version gets set correctly.
if (targetSchemaVersion.getIsLatest()) {
updateSchemaVersion(conn, databaseType, realTargetSchemaVersion);
conn.commit();
}
log(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_SUCCESS, realTargetSchemaVersion));
} catch (Exception e) {
throw new BuildException(e.getMessage(), e);
} finally {
if ((conn != null) && (databaseType != null) && doCloseConnection) {
databaseType.closeConnection(conn);
}
}
}
/**
* Returns <code>true</code> if the given version's schema spec should be executed.
*
* @param version
*
* @return <code>true</code> if this version is on the upgrade path
*/
protected boolean shouldExecSpecVersion(SchemaVersion version) {
return version.getIsLatest() || version.between(startSchemaVersion, targetSchemaVersion);
}
/**
* Makes sure the task's attributes are set properly.
*
* @throws BuildException
*/
private void validateAttributes() throws BuildException {
if (jdbcUrl == null) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_NO_JDBC_URL));
}
if (typeMapFile == null) {
typeMaps = TypeMap.loadKnownTypeMaps();
} else {
FileInputStream fis = null;
try {
fis = new FileInputStream(typeMapFile);
typeMaps = TypeMap.loadTypeMapsFromStream(fis);
} catch (Exception e) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_TYPE_MAP_FILE_ERROR, typeMapFile
.getAbsolutePath(), e), e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// ignore
}
}
}
}
if (targetSchemaVersionString == null) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_NO_VERSION));
}
try {
targetSchemaVersion = new SchemaVersion(targetSchemaVersionString);
} catch (IllegalArgumentException e) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_INVALID_VERSION, e.getMessage()), e);
}
return;
}
/**
* Finding what the current schema version is by looking it up in the version {@link #setTable(String) table}.
*
* @param c
* @param db_type
*
* @return schema version string as found in the {@link #setKeyColumn(String) key column}.
*
* @throws BuildException
*/
private String loadStartSchemaVersion(Connection c, DatabaseType db_type) throws BuildException {
PreparedStatement ps = null;
ResultSet rs = null;
String versionString;
String sql = "SELECT " + valueColumn + " " + "FROM " + tableName + " " + "WHERE " + keyColumn + " = ? ";
try {
ps = c.prepareStatement(sql);
ps.setString(1, keyMatch);
rs = ps.executeQuery();
if (rs.next()) {
versionString = rs.getString(1);
} else {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_MISSING_VERSION, tableName,
valueColumn, keyColumn));
}
if (rs.next()) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_DUPLICATE_VERSION, tableName,
valueColumn, keyColumn));
}
return versionString;
} catch (SQLException e) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_LOADING_START_VERSION, e), e);
} finally {
db_type.closeStatement(ps);
db_type.closeResultSet(rs);
}
}
/**
* Sets the schema version in the {@link #setValueColumn(String) value column} of the
* {@link #setTable(String) version table} to a value that indicates an upgrade is currently in progress.
*
* @param conn
* @param db_type
* @param fromVersion
* @param toVersion
*
* @throws BuildException
*/
private void markSchemaModificationInProgress(Connection conn, DatabaseType db_type, SchemaVersion fromVersion,
SchemaVersion toVersion) throws BuildException {
String versionString = fromVersion.toString() + SCHEMA_MOD_IN_PROGRESS + toVersion.toString();
updateSchemaVersion(conn, db_type, versionString);
return;
}
/**
* Sets the schema version in the {@link #setValueColumn(String) value column} of the version
* {@link #setTable(String)}.
*
* @param conn
* @param db_type
* @param version
*
* @throws BuildException
*/
private void updateSchemaVersion(Connection conn, DatabaseType db_type, String version) throws BuildException {
PreparedStatement ps = null;
ResultSet rs = null;
String sql = "UPDATE " + tableName + " " + "SET " + valueColumn + " = ? " + "WHERE " + keyColumn + " = ? ";
try {
ps = conn.prepareStatement(sql);
ps.setString(1, version);
ps.setString(2, keyMatch);
ps.executeUpdate();
} catch (SQLException e) {
throw new BuildException(MSG.getMsg(DbAntI18NResourceKeys.DBUPGRADE_ERROR_UPDATING_VERSION, version, e), e);
} finally {
db_type.closeStatement(ps);
db_type.closeResultSet(rs);
}
return;
}
/**
* Gets a connection to the database as configured by this task's attributes.
*
* @return database connection
*
* @throws SQLException
*/
public Connection getConnection() throws SQLException {
if (connection == null) {
connection = DbUtil.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
}
return connection;
}
/**
* This can be used to programatically override the JDBC connection to be used
* by this task.
*
* @param connection
*/
public void setConnection(Connection connection) {
this.connection = connection;
doCloseConnection = connection == null;
}
/**
* Returns the type of database that is being upgraded.
*
* @return db type
*/
public DatabaseType getDatabaseType() {
return databaseType;
}
}