Java tutorial
/* * 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); } }