com.adito.jdbc.DBUpgrader.java Source code

Java tutorial

Introduction

Here is the source code for com.adito.jdbc.DBUpgrader.java

Source

/*
*  Adito
*
*  Copyright (C) 2003-2006 3SP LTD. 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; either version 2 of
*  the License, or (at your option) any later version.
*  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 com.adito.jdbc;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.prefs.Preferences;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.adito.boot.ContextHolder;
import com.adito.boot.Util;
import com.adito.boot.VersionInfo;

/**
 * Checks database schemas to see if they are up to date, running the database
 * upgrade scripts if they are not.
 * <p>
 * Current database versions are stored using the Java Preferences API.
 * <p>
 * TODO This class currently assumes that HSQLDB is in use (to check for the
 * existance of databases), although it should be relatively to adapt it for use
 * with other database implementations.
 */
public class DBUpgrader {

    final static Log log = LogFactory.getLog(DBUpgrader.class);

    //
    private static HashMap removed = new HashMap();
    private static HashMap removeProcessed = new HashMap();

    // Private instance variables

    private JDBCDatabaseEngine engine;
    private File dbDir;
    private VersionInfo.Version newDbVersion;
    private File upgradeDir;
    private File versionsFile;
    private boolean useDbNameForVersionCheck;

    /**
     * Constructor
     * 
     * @param newDbVersion the new version of the database
     * @param engine the database engine to use to execute the commands
     * @param dbDir directory containing databases
     * @param upgradeDir directory containing upgrade script
     */
    public DBUpgrader(VersionInfo.Version newDbVersion, JDBCDatabaseEngine engine, File dbDir, File upgradeDir) {
        this(null, newDbVersion, engine, dbDir, upgradeDir);
    }

    /**
     * Constructor
     * 
     * @param versionsFile file to store database version
     * @param newDbVersion the new version of the database
     * @param engine the database engine to use to execute the commands
     * @param dbDir directory containing databases
     * @param upgradeDir directory containing upgrade script
     */
    public DBUpgrader(File versionsFile, VersionInfo.Version newDbVersion, JDBCDatabaseEngine engine, File dbDir,
            File upgradeDir) {
        this(versionsFile, newDbVersion, engine, dbDir, upgradeDir, false);
    }

    /**
     * Constructor
     * 
     * @param versionsFile file to store database version
     * @param newDbVersion the new version of the database
     * @param engine the database engine to use to execute the commands
     * @param dbDir directory containing databases
     * @param upgradeDir directory containing upgrade script
     * @param useDbNameForVersionCheck use database name for version check
     *        instead of alias
     */
    public DBUpgrader(File versionsFile, VersionInfo.Version newDbVersion, JDBCDatabaseEngine engine, File dbDir,
            File upgradeDir, boolean useDbNameForVersionCheck) {
        super();
        this.versionsFile = versionsFile;
        this.upgradeDir = upgradeDir;
        this.engine = engine;
        this.dbDir = dbDir;
        this.newDbVersion = newDbVersion;
        this.useDbNameForVersionCheck = useDbNameForVersionCheck;
    }

    /**
     * Check the database schema and perform any upgrades.
     * 
     * @throws Exception on any error
     */
    public void upgrade() throws Exception {
        Properties versions = null;
        if (versionsFile == null) {
            /* If required, convert from the old preferences node to the new
             * file (version 0.2.5)
             */
            versionsFile = new File(ContextHolder.getContext().getDBDirectory(), "versions.log");
            Preferences p = ContextHolder.getContext().getPreferences().node("dbupgrader");
            if (p.nodeExists("currentDataVersion")) {
                log.warn("Migrating database versions from preferences to properties file in "
                        + ContextHolder.getContext().getDBDirectory().getAbsolutePath() + ".");
                versions = new Properties();
                p = p.node("currentDataVersion");
                String[] c = p.keys();
                for (int i = 0; i < c.length; i++) {
                    versions.put(c[i], p.get(c[i], ""));
                }
                FileOutputStream fos = new FileOutputStream(versionsFile);
                try {
                    versions.store(fos, "Database versions");
                } finally {
                    Util.closeStream(fos);
                }
                p.removeNode();
            }
        }

        // Load the database versions
        if (versions == null) {
            versions = new Properties();
            if (versionsFile.exists()) {
                FileInputStream fin = new FileInputStream(versionsFile);
                try {
                    versions.load(fin);
                } finally {
                    Util.closeStream(fin);
                }
            }
        }

        try {
            String dbCheckName = useDbNameForVersionCheck ? engine.getDatabase() : engine.getAlias();

            if ((!engine.isDatabaseExists() || removed.containsKey(engine.getDatabase()))
                    && !removeProcessed.containsKey(dbCheckName)) {
                versions.remove(dbCheckName);
                removeProcessed.put(dbCheckName, Boolean.TRUE);
                if (log.isInfoEnabled())
                    log.info("Database for " + dbCheckName + " (" + engine.getDatabase()
                            + ") has been removed, assuming this is a re-install.");
                removed.put(engine.getDatabase(), Boolean.TRUE);
            }

            // Check for any SQL scripts to run to bring the databases up to
            // date
            VersionInfo.Version currentDataVersion = new VersionInfo.Version(
                    versions.getProperty(dbCheckName, "0.0.0"));
            if (log.isInfoEnabled()) {
                log.info("New logical database version for " + engine.getAlias() + " is " + newDbVersion);
                log.info("Current logical database version for " + engine.getAlias() + " is " + currentDataVersion);
                //
                log.info("Upgrade script directory is " + upgradeDir.getAbsolutePath());
            }
            List upgrades = getSortedUpgrades(upgradeDir);
            File oldLog = new File(upgradeDir, "upgrade.log");
            if (!dbDir.exists()) {
                if (!dbDir.mkdirs()) {
                    throw new Exception("Failed to create database directory " + dbDir.getAbsolutePath());
                }
            }
            File logFile = new File(dbDir, "upgrade.log");
            if (oldLog.exists()) {
                if (log.isInfoEnabled())
                    log.info(
                            "Moving upgrade.log to new location (as of version 0.1.5 it resides in the db directory.");
                if (!oldLog.renameTo(logFile)) {
                    throw new Exception("Failed to move upgrade log file from " + oldLog.getAbsolutePath() + " to "
                            + logFile.getAbsolutePath());
                }
            }
            HashMap completedUpgrades = new HashMap();
            if (!logFile.exists()) {
                OutputStream out = null;
                try {
                    out = new FileOutputStream(logFile);
                    PrintWriter writer = new PrintWriter(out, true);
                    writer.println("# This file contains a list of database upgrades");
                    writer.println("# that have completed correctly.");
                } finally {
                    if (out != null) {
                        out.flush();
                        out.close();
                    }
                }
            } else {
                InputStream in = null;
                try {
                    in = new FileInputStream(logFile);
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        line = line.trim();
                        if (!line.equals("") && !line.startsWith("#")) {
                            completedUpgrades.put(line, line);
                        }
                    }
                } finally {
                    if (in != null) {
                        in.close();
                    }
                }
            }
            OutputStream out = null;
            try {
                out = new FileOutputStream(logFile, true);
                PrintWriter writer = new PrintWriter(out, true);
                Class.forName("org.hsqldb.jdbcDriver"); // shouldnt be needed,
                // but
                // just in
                // case
                for (Iterator i = upgrades.iterator(); i.hasNext();) {
                    DBUpgradeOp upgrade = (DBUpgradeOp) i.next();
                    boolean runBefore = completedUpgrades.containsKey(upgrade.getFile().getName());
                    if (log.isInfoEnabled())
                        log.info("Checking if upgrade " + upgrade.getFile() + " [" + upgrade.getVersion()
                                + "] needs to be run. Run before = " + runBefore + ". Current data version = "
                                + currentDataVersion + ", upgrade version = " + upgrade.getVersion());
                    if ((!runBefore || (currentDataVersion.getMajor() == 0 && currentDataVersion.getMinor() == 0
                            && currentDataVersion.getBuild() == 0))
                            && upgrade.getVersion().compareTo(currentDataVersion) >= 0
                            && upgrade.getVersion().compareTo(newDbVersion) < 0) {
                        if (log.isInfoEnabled())
                            log.info("Running script " + upgrade.getName() + " [" + upgrade.getVersion()
                                    + "] on database " + engine.getDatabase());

                        // Get a JDBC connection
                        JDBCConnectionImpl conx = engine.aquireConnection();
                        try {
                            runSQLScript(conx, upgrade.getFile());
                            completedUpgrades.put(upgrade.getFile().getName(), upgrade.getFile().getName());
                            writer.println(upgrade.getFile().getName());
                        } finally {
                            engine.releaseConnection(conx);
                        }
                    }
                }
                versions.put(dbCheckName, newDbVersion.toString());

                if (log.isInfoEnabled())
                    log.info("Logical database " + engine.getAlias() + " (" + engine.getDatabase()
                            + ") is now at version " + newDbVersion);
            } finally {
                if (out != null) {
                    out.flush();
                    out.close();
                }
            }
        } finally {
            FileOutputStream fos = new FileOutputStream(versionsFile);
            try {
                versions.store(fos, "Database versions");
            } finally {
                Util.closeStream(fos);
            }
        }
    }

    private List getSortedUpgrades(File upgradeDir) {
        List sortedUpgrades = new ArrayList();
        File[] files = upgradeDir.listFiles();
        for (int i = 0; files != null && i < files.length; i++) {
            String n = files[i].getName();
            if (n.endsWith(engine.getAlias() + ".sql")) {
                sortedUpgrades.add(new DBUpgradeOp(files[i]));
            } else {
                if (log.isDebugEnabled())
                    log.debug("Skipping script " + n);
            }

        }
        Collections.sort(sortedUpgrades);
        return sortedUpgrades;
    }

    private void runSQLScript(JDBCConnectionImpl con, File sqlFile)
            throws SQLException, IllegalStateException, IOException {

        InputStream in = null;
        try {
            in = new FileInputStream(sqlFile);
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            String line = null;
            StringBuffer cmdBuffer = new StringBuffer();
            boolean quoted = false;
            char ch;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (!line.equals("") && !line.startsWith("//") && !line.startsWith("--") && !line.startsWith("#")) {
                    quoted = false;
                    for (int i = 0; i < line.length(); i++) {
                        ch = line.charAt(i);
                        if (ch == '\'') {
                            if (quoted) {
                                if ((i + 1) < line.length() && line.charAt(i + 1) == '\'') {
                                    i++;
                                    cmdBuffer.append(ch);
                                } else {
                                    quoted = false;
                                }
                            } else {
                                quoted = true;
                            }
                            cmdBuffer.append(ch);
                        } else if (ch == ';' && !quoted) {
                            if (cmdBuffer.length() > 0) {
                                executeSQLStatement(con, cmdBuffer.toString());
                                cmdBuffer.setLength(0);
                            }
                        } else {
                            if (i == 0 && ch != ' ' && cmdBuffer.length() > 0 && !quoted) {
                                cmdBuffer.append(' ');
                            }
                            cmdBuffer.append(ch);
                        }
                    }
                }
            }
            if (cmdBuffer.length() > 0) {
                executeSQLStatement(con, cmdBuffer.toString());
                cmdBuffer.setLength(0);
            }
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    private void executeSQLStatement(JDBCConnectionImpl con, String cmd) throws SQLException {
        /*
         * A hack to get around the problem of moving the HSQLDB stored
         * procedures. Prior to version 0.1.13, these functions existed in the
         * class com.adito.DBFunctions. At version 0.1.13, this were moved
         * to com.adito.server.hsqldb.DBFunctions. This meant that on a
         * fresh install, when the original 'CREATE ALIAS' statement is
         * encountered it can no longer find the class. The 'CREATE ALIAS' in
         * the 0.1.13 upgrade scripts have the correct classname so upgrades are
         * not affected by this.
         * 
         * This then happend *AGAIN* for 0.2.0.
         * 
         * TODO remove this code when we clear out all the database upgrade
         * scripts and start again
         * 
         */
        if (cmd.startsWith("CREATE ALIAS ")) {
            int idx = cmd.indexOf("com.adito.DBFunctions.");
            if (idx != -1) {
                cmd = cmd.substring(0, idx) + "com.adito.server.hsqldb.DBFunctions." + cmd.substring(idx + 28);
            }
            idx = cmd.indexOf("com.adito.server.hsqldb.DBFunctions.");
            if (idx != -1) {
                cmd = cmd.substring(0, idx) + "com.adito.jdbc.hsqldb.DBFunctions." + cmd.substring(idx + 42);
            }

        }

        if (log.isDebugEnabled())
            log.debug("Executing \"" + cmd + "\"");
        con.execute(cmd);
    }

}