From 8b81f91ca6c3a05761ab0ee27b5e21e70b504ad0 Mon Sep 17 00:00:00 2001 From: felmer <felmer> Date: Mon, 18 Jun 2007 13:33:54 +0000 Subject: [PATCH] LMS-34 DBMigrationEngine refactored: Lots of interfaces introduced in to order to test the logic of the DBMigrationEngine without needing a filesysetm (for the scripts) and a database. Also a step-by-step migration is implemented. SVN: 546 --- .../cisd/dbmigration/DBMigrationEngine.java | 305 ++++++------------ .../cisd/dbmigration/DBUtilities.java | 43 +++ .../DatabaseConfigurationContext.java | 24 +- .../dbmigration/DatabaseVersionLogDAO.java | 212 ++++++++++++ .../cisd/dbmigration/IDAOFactory.java | 31 ++ .../cisd/dbmigration/IDatabaseAdminDAO.java | 35 ++ .../dbmigration/IDatabaseVersionLogDAO.java | 39 +++ .../cisd/dbmigration/ISqlScriptExecutor.java | 27 ++ .../cisd/dbmigration/ISqlScriptProvider.java | 50 +++ .../systemsx/cisd/dbmigration/LogEntry.java | 170 ++++++++++ .../cisd/dbmigration/PostgreSQLAdminDAO.java | 79 +++++ .../dbmigration/PostgreSQLDAOFactory.java | 55 ++++ .../ch/systemsx/cisd/dbmigration/Script.java | 56 ++++ .../cisd/dbmigration/SqlScriptExecutor.java | 40 +++ .../cisd/dbmigration/SqlScriptProvider.java | 90 ++++++ 15 files changed, 1046 insertions(+), 210 deletions(-) create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBUtilities.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseVersionLogDAO.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDAOFactory.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseAdminDAO.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseVersionLogDAO.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptExecutor.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptProvider.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/LogEntry.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLAdminDAO.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLDAOFactory.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/Script.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptExecutor.java create mode 100644 dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptProvider.java diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBMigrationEngine.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBMigrationEngine.java index 2ad47d5f713..12833fe2ad1 100644 --- a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBMigrationEngine.java +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBMigrationEngine.java @@ -16,29 +16,14 @@ package ch.systemsx.cisd.dbmigration; -import java.io.File; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Date; -import java.util.List; - -import javax.sql.DataSource; - -import org.apache.commons.dbcp.BasicDataSource; import org.apache.log4j.Logger; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.simple.ParameterizedRowMapper; -import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; -import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.jdbc.support.JdbcUtils; import ch.systemsx.cisd.common.db.SQLStateUtils; +import ch.systemsx.cisd.common.exceptions.CheckedExceptionTunnel; import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogFactory; -import ch.systemsx.cisd.common.utilities.FileUtilities; /** * Class for creating and migrating a database. @@ -49,67 +34,20 @@ public class DBMigrationEngine { private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, DBMigrationEngine.class); - private static final String INSERT_DB_VERSION = "INSERT INTO DATABASE_VERSION VALUES (1, ?)"; - - private static final String CREATE_DB_VERSION_TABLE = - "CREATE TABLE DATABASE_VERSION (DB_VERSION SMALLINT NOT NULL," + "DB_INSTALLATION_DATE DATE)"; - - private static final class DatabaseVersion - { - private final int version; - - private final Date installationDate; - - public DatabaseVersion(int version, Date installationDate) - { - this.version = version; - this.installationDate = installationDate; - } - - Date getInstallationDate() - { - return installationDate; - } - - int getVersion() - { - return version; - } - } - - private final DataSource metaDataSource; - - private final File scriptFolder; - - private final String owner; - - private final String databaseName; - - private final String folderOfDataScripts; - private final boolean shouldCreateFromScratch; - private final DataSource dataSource; + private final IDAOFactory daoFactory; + + private final ISqlScriptProvider scriptProvider; - public DBMigrationEngine(DatabaseConfigurationContext context) + + public DBMigrationEngine(IDAOFactory daoFactory, ISqlScriptProvider scriptProvider, boolean shouldCreateFromScratch) { - this.owner = context.getOwner(); - shouldCreateFromScratch = context.isCreateFromScratch(); - metaDataSource = createMasterDataSource(context); - databaseName = context.getDatabaseName(); - dataSource = context.getDataSource(); - scriptFolder = new File(context.getScriptFolder()); - folderOfDataScripts = context.getFolderOfDataScripts(); + this.daoFactory = daoFactory; + this.scriptProvider = scriptProvider; + this.shouldCreateFromScratch = shouldCreateFromScratch; } - /** - * Returns the name of the database. - */ - public final String getDatabaseName() - { - return databaseName; - } - public void migrateTo(String version) { if (shouldCreateFromScratch) @@ -121,208 +59,165 @@ public class DBMigrationEngine setupDatabase(version); return; } - SimpleJdbcTemplate template = new SimpleJdbcTemplate(dataSource); - List<DatabaseVersion> list = - template.query("SELECT * FROM DATABASE_VERSION", new ParameterizedRowMapper<DatabaseVersion>() - { - public DatabaseVersion mapRow(ResultSet rs, int rowNum) throws SQLException - { - int dbVersion = rs.getInt("DB_VERSION"); - java.sql.Date date = rs.getDate("DB_INSTALLATION_DATE"); - return new DatabaseVersion(dbVersion, date); - } - }); - int size = list.size(); - if (size == 0) + LogEntry entry = daoFactory.getDatabaseVersionLogDAO().getLastEntry(); + String databaseVersion = entry.getVersion(); + if (version.equals(databaseVersion)) { - throw new EnvironmentFailureException("Incompletely initialized database."); - } else if (size > 1) - { - throw new EnvironmentFailureException("To many versions found in DATABASE_VERSION: " + size); - } else - { - DatabaseVersion databaseVersion = list.get(0); - int dbVersion = databaseVersion.getVersion(); - if (Integer.parseInt(version) == dbVersion) + if (operationLog.isInfoEnabled()) { - return; // no migrate needed + operationLog.info("No migration needed. Database version " + version + "."); } - if (Integer.parseInt(version) > dbVersion) - { - if (operationLog.isInfoEnabled()) - { - operationLog.info("Migrating database from version '" + dbVersion + "' to '" + version + "'."); - } - // TODO implementation of migration - } else + return; + } + if (version.compareTo(databaseVersion) > 0) + { + if (operationLog.isInfoEnabled()) { - throw new EnvironmentFailureException("Couldn't revert from version " + dbVersion - + " to previous version " + version + "."); + operationLog.info("Migrating database from version '" + databaseVersion + "' to '" + version + "'."); } + migrate(databaseVersion, version); + } else + { + throw new EnvironmentFailureException("Couldn't revert from version " + databaseVersion + + " to previous version " + version + "."); } } private void dropDatabase() { - String dropDatabaseSQL = createScript("dropDatabase.sql", owner, databaseName); - JdbcTemplate template = new JdbcTemplate(metaDataSource); - try - { - template.execute(dropDatabaseSQL); - } catch (DataAccessException ex) - { - if (isDBNotExistException(ex) == false) - { - throw ex; - } - } + daoFactory.getDatabaseDAO().dropDatabase(); } private void setupDatabase(String version) { - createUser(); + createOwner(); createEmptyDatabase(version); fillWithInitialData(version); if (operationLog.isInfoEnabled()) { - operationLog.info("Database '" + databaseName + "' has been successfully created."); + String databaseName = daoFactory.getDatabaseDAO().getDatabaseName(); + operationLog.info("Database '" + databaseName + "' version " + version + " has been successfully created."); } } - private void createUser() + private void createOwner() { - String createUserSQL = createScript("createUser.sql", owner, databaseName); - JdbcTemplate template = new JdbcTemplate(metaDataSource); + IDatabaseAdminDAO databaseDAO = daoFactory.getDatabaseDAO(); try { - template.execute(createUserSQL); + databaseDAO.createOwner(); } catch (DataAccessException ex) { if (userAlreadyExists(ex)) { if (operationLog.isInfoEnabled()) { - operationLog.info("User '" + owner + "' already exists."); + operationLog.info("Owner '" + databaseDAO.getOwner() + "' already exists."); } } else { - operationLog.error("Executing following script '" + createUserSQL + "' threw an exception.", ex); + operationLog.error("Database owner couldn't be created:", ex); } } } private void createEmptyDatabase(String version) { - JdbcTemplate template = new JdbcTemplate(metaDataSource); - String createDatabaseSQL = createScript("createDatabase.sql", owner, databaseName); - template.execute(createDatabaseSQL); - - template = new JdbcTemplate(dataSource); - template.execute(CREATE_DB_VERSION_TABLE); - Object[] args = new Object[1]; - args[0] = new Date(); - template.update(INSERT_DB_VERSION, args); + daoFactory.getDatabaseDAO().createDatabase(); + IDatabaseVersionLogDAO logDAO = daoFactory.getDatabaseVersionLogDAO(); + logDAO.createTable(); - final String createScript = loadScript("schema", version); - template.execute(createScript); + Script script = scriptProvider.getSchemaScript(version); + if (script == null) + { + throw new EnvironmentFailureException("No schema script found for version " + version); + } + executeScript(script, version); } private void fillWithInitialData(String version) { - String initialDataScript = null; - String initialDataScriptFile = folderOfDataScripts + "/" + version + "/" + "data-" + version + ".sql"; - if (initialDataScriptFile != null) - { - initialDataScript = FileUtilities.loadToString(getClass(), "/" + initialDataScriptFile); - if (initialDataScript == null) - { - File file = new File(initialDataScriptFile); - if (file.exists()) - { - initialDataScript = FileUtilities.loadToString(file); - } - } - } + Script initialDataScript = scriptProvider.getDataScript(version); if (initialDataScript != null) { - JdbcTemplate template = new JdbcTemplate(dataSource); - template.execute(initialDataScript); + executeScript(initialDataScript, version); } } - private String createScript(String scriptTemplateFile, String user, String database) + private void migrate(String fromVersion, String toVersion) { - String script = loadScript(scriptTemplateFile); - return script.replace("$USER", user).replace("$DATABASE", database); + String version = fromVersion; + do + { + String nextVersion = increment(version); + Script migrationScript = scriptProvider.getMigrationScript(version, nextVersion); + if (migrationScript == null) + { + String message = "Missing migration script from version " + version + " to " + nextVersion; + operationLog.error(message); + throw new EnvironmentFailureException(message); + } + executeScript(migrationScript, toVersion); + version = nextVersion; + } while (version.equals(toVersion) == false); } - private String loadScript(String scriptName, String version) + private String increment(String version) { - return loadScript(version + "/" + scriptName + "-" + version + ".sql"); - } - - /** Loads given script name. */ - private String loadScript(String scriptName) - { - String resource = "/" + scriptFolder + "/" + scriptName; - String script = FileUtilities.loadToString(getClass(), resource); - if (script == null) + char[] characters = new char[version.length()]; + version.getChars(0, characters.length, characters, 0); + for (int i = characters.length - 1; i >= 0; i--) { - File file = new File(scriptFolder, scriptName); - if (operationLog.isDebugEnabled()) + char c = characters[i]; + if (c == '9') + { + characters[i] = '0'; + } else { - operationLog.debug("Resource '" + resource + "' could not be found. Trying '" + file.getPath() + "'."); + characters[i] = (char) (c + 1); + break; } - script = FileUtilities.loadToString(file); } - return script; + return new String(characters); } - /** Checks whether database already exists. */ - private final boolean databaseExists() + private void executeScript(Script script, String version) throws Error { - Connection connection = null; + IDatabaseVersionLogDAO logDAO = daoFactory.getDatabaseVersionLogDAO(); + String code = script.getCode(); + String name = script.getName(); + logDAO.logStart(version, name, code); try { - connection = DataSourceUtils.getConnection(dataSource); - return true; - } catch (DataAccessException ex) + daoFactory.getSqlScriptExecutor().execute(code); + logDAO.logSuccess(version, name); + } catch (Throwable t) { - if (isDBNotExistException(ex)) + operationLog.error("Executing script '" + name + "' failed", t); + logDAO.logFailure(version, name, t); + if (t instanceof RuntimeException) { - if (operationLog.isInfoEnabled()) - { - operationLog.info("Database '" + databaseName + "' does not exist."); - } - return false; + RuntimeException re = (RuntimeException) t; + throw re; } - throw new EnvironmentFailureException("Couldn't connect database server.", ex); - } finally - { - JdbcUtils.closeConnection(connection); + if (t instanceof Error) + { + Error error = (Error) t; + throw error; + } + throw new CheckedExceptionTunnel((Exception) t); } } - - /** Creates a <code>DataSource</code> from given <code>context</code>. */ - private final static DataSource createMasterDataSource(DatabaseConfigurationContext context) - { - BasicDataSource dataSource = new BasicDataSource(); - dataSource.setDriverClassName(context.getDriver()); - dataSource.setUrl(context.getAdminURL()); - dataSource.setUsername(context.getAdminUser()); - dataSource.setPassword(context.getAdminPassword()); - return dataSource; - } - /** - * Checks whether given <code>DataAccessException</code> is caused by a "database does not exist" exception. - * <p> - * This is database specific. - * </p> - */ - protected boolean isDBNotExistException(DataAccessException ex) + + /** Checks whether database already exists. */ + private final boolean databaseExists() { - // 3D000: INVALID CATALOG NAME - return SQLStateUtils.isInvalidCatalogName(SQLStateUtils.getSqlState(ex)); + boolean result = daoFactory.getDatabaseVersionLogDAO().canConnectToDatabase(); + if (result && operationLog.isInfoEnabled()) + { + operationLog.info("Database '" + daoFactory.getDatabaseDAO().getDatabaseName() + "' does not exist."); + } + return result; } /** diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBUtilities.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBUtilities.java new file mode 100644 index 00000000000..811cb0910fe --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DBUtilities.java @@ -0,0 +1,43 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import org.springframework.dao.DataAccessException; + +import ch.systemsx.cisd.common.db.SQLStateUtils; + +/** + * Utility database methods. + * + * @author Franz-Josef Elmer + */ +class DBUtilities +{ + /** + * Checks whether given <code>DataAccessException</code> is caused by a "database does not exist" exception. + * <p> + * This is database specific. + * </p> + */ + static boolean isDBNotExistException(DataAccessException ex) + { + // 3D000: INVALID CATALOG NAME + return SQLStateUtils.isInvalidCatalogName(SQLStateUtils.getSqlState(ex)); + } + + private DBUtilities() {} +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseConfigurationContext.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseConfigurationContext.java index 8391e44757c..ff4ccc93243 100644 --- a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseConfigurationContext.java +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseConfigurationContext.java @@ -51,6 +51,8 @@ public class DatabaseConfigurationContext private boolean createFromScratch; private DataSource dataSource; + + private DataSource adminDataSource; private String owner; @@ -76,11 +78,6 @@ public class DatabaseConfigurationContext myDataSource.setPassword(""); return myDataSource; } - - public final void setDataSource(DataSource dataSource) - { - this.dataSource = dataSource; - } /** * Returns the {@link DataSource} of this data configuration. @@ -94,6 +91,23 @@ public class DatabaseConfigurationContext return dataSource; } + /** + * Returns data source for admin purposes. + */ + final DataSource getAdminDataSource() + { + if (adminDataSource == null) + { + BasicDataSource myDataSource = new BasicDataSource(); + myDataSource.setDriverClassName(getDriver()); + myDataSource.setUrl(getAdminURL()); + myDataSource.setUsername(getAdminUser()); + myDataSource.setPassword(getAdminPassword()); + adminDataSource = myDataSource; + } + return adminDataSource; + } + /** * Returns the user name of the owner of the database. */ diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseVersionLogDAO.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseVersionLogDAO.java new file mode 100644 index 00000000000..421800232ba --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseVersionLogDAO.java @@ -0,0 +1,212 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +import ch.systemsx.cisd.common.exceptions.CheckedExceptionTunnel; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; + +/** + * Class which logs database migration steps in the database. + * + * @author Franz-Josef Elmer + */ +class DatabaseVersionLogDAO extends SimpleJdbcDaoSupport implements IDatabaseVersionLogDAO +{ + private static final String ENCODING = "utf8"; + + private static final String RUN_EXCEPTION = "run_exception"; + + private static final String MODULE_CODE = "module_code"; + + private static final String RUN_STATUS_TIMESTAMP = "run_status_timestamp"; + + private static final String RUN_STATUS = "run_status"; + + private static final String MODULE_NAME = "module_name"; + + private static final String DB_VERSION = "db_version"; + + private static final String DB_VERSION_LOG = "database_version_logs"; + + private static final String CREATE_DB_VERSION_TABLE = + "create table " + DB_VERSION_LOG + + " (" + DB_VERSION + " varchar(4) not null" + + " ," + MODULE_NAME + " varchar(250)" + + " ," + RUN_STATUS + " varchar(10)" + + " ," + RUN_STATUS_TIMESTAMP + " timestamp" + + " ," + MODULE_CODE + " bytea" + + " ," + RUN_EXCEPTION + " bytea" + + ")"; + + private static final class LogEntryRowMapper implements ParameterizedRowMapper<LogEntry> + { + private final LobHandler lobHandler; + + LogEntryRowMapper(LobHandler lobHandler) + { + this.lobHandler = lobHandler; + } + + public LogEntry mapRow(ResultSet rs, int rowNum) throws SQLException + { + final LogEntry logEntry = new LogEntry(); + logEntry.setVersion(rs.getString(DB_VERSION)); + logEntry.setModuleName(rs.getString(MODULE_NAME)); + logEntry.setRunStatus(rs.getString(RUN_STATUS)); + logEntry.setRunStatusTimestamp(rs.getDate(RUN_STATUS_TIMESTAMP)); + try + { + logEntry.setModuleCode(new String(lobHandler.getBlobAsBytes(rs, MODULE_CODE), ENCODING)); + } catch (UnsupportedEncodingException ex) + { + throw new CheckedExceptionTunnel(ex); + } + logEntry.setRunException(lobHandler.getClobAsString(rs, RUN_EXCEPTION)); + return logEntry; + } + } + + private static byte[] getAsByteArray(String string) + { + try + { + return string.getBytes(ENCODING); + } catch (UnsupportedEncodingException ex) + { + throw new CheckedExceptionTunnel(ex); + } + } + + final LobHandler lobHandler; + + DatabaseVersionLogDAO(DataSource dataSource, LobHandler lobHandler) + { + setDataSource(dataSource); + this.lobHandler = lobHandler; + } + + public boolean canConnectToDatabase() + { + Connection connection = null; + try + { + DataSource dataSource = getDataSource(); + connection = DataSourceUtils.getConnection(dataSource); + return true; + } catch (DataAccessException ex) + { + if (DBUtilities.isDBNotExistException(ex)) + { + return false; + } + throw new EnvironmentFailureException("Couldn't connect database server.", ex); + } finally + { + JdbcUtils.closeConnection(connection); + } + } + + public void createTable() + { + JdbcTemplate template = getJdbcTemplate(); + template.execute(CREATE_DB_VERSION_TABLE); + } + + public LogEntry getLastEntry() + { + SimpleJdbcTemplate template = getSimpleJdbcTemplate(); + List<LogEntry> entries = template.query("select * from " + DB_VERSION_LOG, new LogEntryRowMapper(lobHandler)); + + return entries.size() == 0 ? null : entries.get(entries.size() - 1); + } + + public void logStart(final String version, final String moduleName, final String moduleCode) + { + JdbcTemplate template = getJdbcTemplate(); + PreparedStatementCallback callback = new AbstractLobCreatingPreparedStatementCallback(this.lobHandler) + { + @Override + protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException + { + ps.setString(1, version); + ps.setString(2, moduleName); + ps.setString(3, LogEntry.RunStatus.START.toString()); + ps.setTimestamp(4, new Timestamp(System.currentTimeMillis())); + lobCreator.setBlobAsBytes(ps, 5, getAsByteArray(moduleCode)); + } + }; + template.execute("insert into " + DB_VERSION_LOG + " (" + DB_VERSION + "," + MODULE_NAME + "," + RUN_STATUS + "," + + RUN_STATUS_TIMESTAMP + "," + MODULE_CODE + ") values (?,?,?,?,?)", + callback); + } + + public void logSuccess(String version, String moduleName) + { + SimpleJdbcTemplate template = getSimpleJdbcTemplate(); + template.update("update " + DB_VERSION_LOG + " SET " + RUN_STATUS + " = ? , " + RUN_STATUS_TIMESTAMP + " = ? " + + "where " + DB_VERSION + " = ? and " + MODULE_NAME + " = ?", + LogEntry.RunStatus.SUCCESS.toString(), new Date(System.currentTimeMillis()), version, moduleName); + } + + public void logFailure(final String version, final String moduleName, Throwable runException) + { + final StringWriter stringWriter = new StringWriter(); + runException.printStackTrace(new PrintWriter(stringWriter)); + JdbcTemplate template = getJdbcTemplate(); + PreparedStatementCallback callback = new AbstractLobCreatingPreparedStatementCallback(this.lobHandler) + { + @Override + protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException + { + ps.setString(1, LogEntry.RunStatus.FAILED.toString()); + ps.setTimestamp(2, new Timestamp(System.currentTimeMillis())); + lobCreator.setBlobAsBytes(ps, 3, getAsByteArray(stringWriter.toString())); + ps.setString(4, version); + ps.setString(5, moduleName); + } + }; + template.execute("update " + DB_VERSION_LOG + " SET " + RUN_STATUS + " = ?, " + RUN_STATUS_TIMESTAMP + " = ?, " + + RUN_EXCEPTION + " = ? where " + DB_VERSION + " = ? and " + MODULE_NAME + " = ?", callback); + } + + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDAOFactory.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDAOFactory.java new file mode 100644 index 00000000000..085dbbfefe6 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDAOFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +/** + * Factory for DAOs. + * + * @author Franz-Josef Elmer + */ +public interface IDAOFactory +{ + public IDatabaseAdminDAO getDatabaseDAO(); + + public ISqlScriptExecutor getSqlScriptExecutor(); + + public IDatabaseVersionLogDAO getDatabaseVersionLogDAO(); +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseAdminDAO.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseAdminDAO.java new file mode 100644 index 00000000000..fa4b0b5ffea --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseAdminDAO.java @@ -0,0 +1,35 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +/** + * Interface for creating/dropping a database. + * + * @author Franz-Josef Elmer + */ +public interface IDatabaseAdminDAO +{ + public String getOwner(); + + public String getDatabaseName(); + + public void createOwner(); + + public void createDatabase(); + + public void dropDatabase(); +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseVersionLogDAO.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseVersionLogDAO.java new file mode 100644 index 00000000000..eb67e09dbe7 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/IDatabaseVersionLogDAO.java @@ -0,0 +1,39 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + + +/** + * Interface for a Data Access Object for the database version log. + * + * @author Franz-Josef Elmer + */ +public interface IDatabaseVersionLogDAO +{ + public boolean canConnectToDatabase(); + + public void createTable(); + + public LogEntry getLastEntry(); + + public void logStart(String version, String moduleName, String moduleCode); + + public void logSuccess(String version, String moduleName); + + public void logFailure(String version, String moduleName, Throwable runException); + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptExecutor.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptExecutor.java new file mode 100644 index 00000000000..965696d5168 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptExecutor.java @@ -0,0 +1,27 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +/** + * Executor of SQL scripts. + * + * @author Franz-Josef Elmer + */ +public interface ISqlScriptExecutor +{ + public void execute(String sqlScript); +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptProvider.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptProvider.java new file mode 100644 index 00000000000..a32de7ca80f --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/ISqlScriptProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +/** + * Provider of SQL scripts for creation and migration of database. + * + * @author Franz-Josef Elmer + */ +public interface ISqlScriptProvider +{ + /** + * Returns the script to create database schemas. + * + * @param version Version of the database. + * @return <code>null</code> if there isn't such a script. + */ + public Script getSchemaScript(String version); + + /** + * Returns the script to create initial data. + * + * @param version Version of the database. + * @return <code>null</code> if there isn't such a script. + */ + public Script getDataScript(String version); + + /** + * Returns the migration script for migrating a database. + * + * @param fromVersion The version of the current database. + * @param toVersion The version of the database after migration. + * @return <code>null</code> if there isn't such a migration script. + */ + public Script getMigrationScript(String fromVersion, String toVersion); +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/LogEntry.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/LogEntry.java new file mode 100644 index 00000000000..88fa1fc78e8 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/LogEntry.java @@ -0,0 +1,170 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import java.util.Date; + +/** + * Log entry of DATABASE_VERSION_LOG + * + * @author Franz-Josef Elmer + */ +public class LogEntry +{ + public enum RunStatus {START, SUCCESS, FAILED, UNKNOWN} + + private String version; + private String moduleName; + private RunStatus runStatus; + private Date runStatusTimestamp; + private String moduleCode; + private String runException; + + /** + * Returns moduleCode. + * + * @return <code>null</code> when undefined. + */ + public String getModuleCode() + { + return moduleCode; + } + + /** + * Sets moduleCode. + * + * @param moduleCode New value. Can be <code>null</code>. + */ + public void setModuleCode(String moduleCode) + { + this.moduleCode = moduleCode; + } + + /** + * Returns moduleName. + * + * @return <code>null</code> when undefined. + */ + public String getModuleName() + { + return moduleName; + } + + /** + * Sets moduleName. + * + * @param moduleName New value. Can be <code>null</code>. + */ + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + /** + * Returns runException. + * + * @return <code>null</code> when undefined. + */ + public String getRunException() + { + return runException; + } + + /** + * Sets runException. + * + * @param runException New value. Can be <code>null</code>. + */ + public void setRunException(String runException) + { + this.runException = runException; + } + + /** + * Returns runStatus. + * + * @return <code>null</code> when undefined. + */ + public RunStatus getRunStatus() + { + return runStatus; + } + + /** + * Sets runStatus based its string representation. If the argument does not match one of the valid constants. + * {@link RunStatus#UNKNOWN} will be used. + */ + public void setRunStatus(String runStatusAsString) + { + runStatus = RunStatus.valueOf(runStatusAsString); + if (runStatus == null) + { + runStatus = RunStatus.UNKNOWN; + } + } + + /** + * Sets runStatus. + * + * @param runStatus New value. Can be <code>null</code>. + */ + public void setRunStatus(RunStatus runStatus) + { + this.runStatus = runStatus; + } + + /** + * Returns runStatusTimestamp. + * + * @return <code>null</code> when undefined. + */ + public Date getRunStatusTimestamp() + { + return runStatusTimestamp; + } + + /** + * Sets runStatusTimestamp. + * + * @param runStatusTimestamp New value. Can be <code>null</code>. + */ + public void setRunStatusTimestamp(Date runStatusTimestamp) + { + this.runStatusTimestamp = runStatusTimestamp; + } + + /** + * Returns version. + * + * @return <code>null</code> when undefined. + */ + public String getVersion() + { + return version; + } + + /** + * Sets version. + * + * @param version New value. Can be <code>null</code>. + */ + public void setVersion(String version) + { + this.version = version; + } + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLAdminDAO.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLAdminDAO.java new file mode 100644 index 00000000000..7dd57e508a2 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLAdminDAO.java @@ -0,0 +1,79 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport; + +/** + * Implementation of {@link IDatabaseAdminDAO} for PostgreSQL. + * + * @author Franz-Josef Elmer + */ +public class PostgreSQLAdminDAO extends SimpleJdbcDaoSupport implements IDatabaseAdminDAO +{ + private final String owner; + private final String database; + + public PostgreSQLAdminDAO(DataSource dataSource, String owner, String database) + { + this.owner = owner; + this.database = database; + setDataSource(dataSource); + } + + public String getDatabaseName() + { + return database; + } + + public String getOwner() + { + return owner; + } + + public void createOwner() + { + getJdbcTemplate().execute("create user " + owner); + } + + public void createDatabase() + { + JdbcTemplate template = getJdbcTemplate(); + template.execute("create database " + database + " with owner = " + owner + + " encoding = 'utf8' tablespace = pg_default;" + + "alter database " + database + " set default_with_oids = off;"); + } + + public void dropDatabase() + { + try + { + getJdbcTemplate().execute("drop database " + database); + } catch (DataAccessException ex) + { + if (DBUtilities.isDBNotExistException(ex) == false) + { + throw ex; + } + } + } + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLDAOFactory.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLDAOFactory.java new file mode 100644 index 00000000000..6be1d8b2c42 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/PostgreSQLDAOFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import javax.sql.DataSource; + +/** + * Implementation of {@link IDAOFactory} for PostgreSQL. + * + * @author Franz-Josef Elmer + */ +public class PostgreSQLDAOFactory implements IDAOFactory +{ + private final IDatabaseAdminDAO databaseDAO; + private final ISqlScriptExecutor sqlScriptExecutor; + private final IDatabaseVersionLogDAO databaseVersionLogDAO; + + public PostgreSQLDAOFactory(DatabaseConfigurationContext context) + { + databaseDAO = new PostgreSQLAdminDAO(context.getAdminDataSource(), context.getOwner(), context.getDatabaseName()); + DataSource dataSource = context.getDataSource(); + sqlScriptExecutor = new SqlScriptExecutor(dataSource); + databaseVersionLogDAO = new DatabaseVersionLogDAO(dataSource, context.getLobHandler()); + } + + public IDatabaseAdminDAO getDatabaseDAO() + { + return databaseDAO; + } + + public ISqlScriptExecutor getSqlScriptExecutor() + { + return sqlScriptExecutor; + } + + public IDatabaseVersionLogDAO getDatabaseVersionLogDAO() + { + return databaseVersionLogDAO; + } + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/Script.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/Script.java new file mode 100644 index 00000000000..0c82ec9b966 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/Script.java @@ -0,0 +1,56 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +/** + * Bean class for a script. Holds script name and code. + * + * @author Franz-Josef Elmer + */ +public class Script +{ + private final String name; + private final String code; + + /** + * Creates an instance for the specified script name and code. + */ + public Script(String name, String code) + { + assert name != null; + assert code != null; + this.name = name; + this.code = code; + } + + /** + * Returns script code. + */ + public String getCode() + { + return code; + } + + /** + * Returns script name. + */ + public String getName() + { + return name; + } + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptExecutor.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptExecutor.java new file mode 100644 index 00000000000..54477700ba0 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptExecutor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +/** + * Implementation of {@link SqlScriptExecutor}. + * + * @author Franz-Josef Elmer + */ +class SqlScriptExecutor extends JdbcDaoSupport implements ISqlScriptExecutor +{ + SqlScriptExecutor(DataSource dataSource) + { + setDataSource(dataSource); + } + + public void execute(String sqlScript) + { + getJdbcTemplate().execute(sqlScript); + } + +} diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptProvider.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptProvider.java new file mode 100644 index 00000000000..2aaee7136b2 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/SqlScriptProvider.java @@ -0,0 +1,90 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.dbmigration; + +import java.io.File; + +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.utilities.FileUtilities; + +/** + * Implementation of {@link ISqlScriptProvider} based on files in classpath or working directory. + * + * @author Franz-Josef Elmer + */ +public class SqlScriptProvider implements ISqlScriptProvider +{ + private static final String SQL_FILE_TYPE = ".sql"; + + private static final Logger operationLog + = LogFactory.getLogger(LogCategory.OPERATION, SqlScriptProvider.class); + + private final String schemaScriptFolder; + private final String dataScriptFolder; + + public SqlScriptProvider(String schemaScriptFolder, String dataScriptFolder) + { + this.schemaScriptFolder = schemaScriptFolder; + this.dataScriptFolder = dataScriptFolder; + } + + public Script getDataScript(String version) + { + return loadScript(dataScriptFolder + "/" + version, "data-" + version + SQL_FILE_TYPE); + } + + public Script getMigrationScript(String fromVersion, String toVersion) + { + String scriptName = "migration-" + fromVersion + "-" + toVersion + SQL_FILE_TYPE; + return loadScript(schemaScriptFolder + "/migration", scriptName); + } + + public Script getSchemaScript(String version) + { + return loadScript(schemaScriptFolder + "/" + version, "schema-" + version + SQL_FILE_TYPE); + } + + private Script loadScript(String folder, String scriptName) + { + String fullScriptName = folder + "/" + scriptName; + String resource = "/" + fullScriptName; + String script = FileUtilities.loadToString(getClass(), resource); + if (script == null) + { + File file = new File(folder, scriptName); + if (operationLog.isDebugEnabled()) + { + operationLog.debug("Resource '" + resource + "' could not be found. Trying '" + file.getPath() + "'."); + } + if (file.exists() == false) + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug("File '" + file.getPath() + "' does not exist."); + } + return null; + } + script = FileUtilities.loadToString(file); + } + return new Script(fullScriptName, script); + } + + +} -- GitLab