org.ambraproject.migration.BootstrapMigratorServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.ambraproject.migration.BootstrapMigratorServiceImpl.java

Source

/*
 * $HeadURL$
 * $Id$
 *
 * Copyright (c) 2006-2011 by Public Library of Science
 *     http://plos.org
 *     http://ambraproject.org
 *
 * 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 org.ambraproject.migration;

import org.ambraproject.models.Version;
import org.apache.commons.configuration.Configuration;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.topazproject.ambra.configuration.ConfigurationStore;
import org.ambraproject.service.HibernateServiceImpl;

import org.hibernate.Transaction;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * Does migrations on startup.
 *
 * Note we store version as integers to avoid floating point / decimal rounding issues with mysql and java
 * So just multiply release values by 100
 *
 * @author Joe Osowski
 */
public class BootstrapMigratorServiceImpl extends HibernateServiceImpl implements BootstrapMigratorService {
    private static Logger log = LoggerFactory.getLogger(BootstrapMigratorServiceImpl.class);

    private double dbVersion;
    private double binaryVersion;
    private boolean isSnapshot;

    /**
     * Apply all migrations.
     *
     * @throws Exception on an error
     */
    public void migrate() throws Exception {
        Configuration conf = ConfigurationStore.getInstance().getConfiguration();

        setVersionData();

        //If this is a snapshot, we're developing and we don't need to do this check
        if (isSnapshot == false) {
            //Throws an exception if the database version is further into
            //the future then this version of the ambra war
            if (binaryVersion < dbVersion) {
                log.error("Binary version: " + binaryVersion + ", DB version: " + dbVersion);
                throw new Exception("The ambra war is out of date with the database, "
                        + "update this war file to the latest version.");
            }
        }

        waitForOtherMigrations();

        if (dbVersion < 220) {
            migrate210();
        }

        if (dbVersion < 223) {
            migrate222();
        }

        if (dbVersion < 232) {
            migrate230();
        }
    }

    /*
    * Run the migration from 230 to 232
    **/
    private void migrate230() {
        log.info("Migration from 230 starting");

        hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Version v = new Version();
                v.setName("Ambra 2.32");
                v.setVersion(232);
                v.setUpdateInProcess(true);
                session.save(v);

                log.debug("Creating new tables.");
                execSQLScript(session, "migrate_ambra_2_3_2_part1.sql");
                log.debug("Tables created, now migrating and cleaning up data.");
                execSQLScript(session, "migrate_ambra_2_3_2_part2.sql");
                log.debug("Migrated data, now dropping tables");
                execSQLScript(session, "migrate_ambra_2_3_2_part3.sql");

                v.setUpdateInProcess(false);
                session.update(v);

                return null;
            }
        });

        log.info("Migration from 230 complete");
    }

    /*
    * Run the migration from 222 to 223
    **/
    private void migrate222() {
        log.info("Migration from 222 starting");

        hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Version v = new Version();
                v.setName("Ambra 2.30");
                v.setVersion(230);
                v.setUpdateInProcess(true);
                session.save(v);

                log.debug("Creating new tables.");

                execSQLScript(session, "migrate_ambra_2_2_2_part1.sql");

                log.debug("Tables created, now migrating data.");

                execSQLScript(session, "migrate_ambra_2_2_2_part2.sql");

                log.debug("Cleaning up data");

                execSQLScript(session, "migrate_ambra_2_2_2_part3.sql");

                log.debug("Migrated data, now dropping tables");

                execSQLScript(session, "migrate_ambra_2_2_2_part4.sql");

                v.setUpdateInProcess(false);
                session.update(v);

                return null;
            }
        });

        log.info("Migration from 222 complete");
    }

    /*
    * Run the migration for ambra 2.10 to 2.20
    **/
    private void migrate210() {
        log.info("Migration from 210 starting");
        //First create version table and add one row

        final boolean isSnapshot = this.isSnapshot;

        hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                log.debug("Creating new tables.");

                execSQLScript(session, "migrate_ambra_2_2_0_part1.sql");

                Version v = new Version();
                v.setName("Ambra 2.20");
                v.setVersion(220);
                v.setUpdateInProcess(true);
                session.save(v);

                log.debug("Tables created, now migrating data and removing old tables.");

                //We execute step #2 in a slightly different way as this file has SQL delimited in a different fashion
                //since it creates a trigger
                String sqlScript = "";
                try {
                    log.debug("migrate_ambra_2_2_0_part2.sql started");
                    sqlScript = getSQLScript("migrate_ambra_2_2_0_part2.sql");
                    log.debug("migrate_ambra_2_2_0_part2.sql completed");
                } catch (IOException ex) {
                    throw new HibernateException(ex.getMessage(), ex);
                }

                session.createSQLQuery(sqlScript).executeUpdate();

                execSQLScript(session, "migrate_ambra_2_2_0_part3.sql");

                //step 4 also creates a trigger, so we need to execute it the same as with step 2
                try {
                    log.debug("migrate_ambra_2_2_0_part4.sql started");
                    sqlScript = getSQLScript("migrate_ambra_2_2_0_part4.sql");
                    log.debug("migrate_ambra_2_2_0_part4.sql completed");
                } catch (IOException ex) {
                    throw new HibernateException(ex.getMessage(), ex);
                }
                session.createSQLQuery(sqlScript).executeUpdate();

                execSQLScript(session, "migrate_ambra_2_2_0_part5.sql");
                execSQLScript(session, "migrate_ambra_2_2_0_part6.sql");

                v.setUpdateInProcess(false);
                session.update(v);

                return null;
            }
        });

        log.info("Migration from 210 complete");
    }

    /*
    * Wait for other migrations to complete.  This will prevent two instances of ambra from attempting to execute the
    * same migration
    * */
    private void waitForOtherMigrations() throws InterruptedException {
        while (isMigrateRunning()) {
            log.debug("Waiting for another migration to complete.");
            Thread.sleep(10000);
        }
    }

    /*
    * Determine if a migration is already running
    **/
    private boolean isMigrateRunning() {
        return (Boolean) hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                SQLQuery q = session.createSQLQuery("show tables");
                List<String> tables = q.list();

                //Check to see if the version table exists.
                //If it does not exist then no migrations have been run yet
                if (!tables.contains("version")) {
                    return false;
                }

                //If we get this far, return the version column out of the database
                Criteria c = session.createCriteria(Version.class).setProjection(Projections.max("version"));

                int version = (Integer) c.uniqueResult();

                c = session.createCriteria(Version.class).add(Restrictions.eq("version", version));

                Version v = (Version) c.uniqueResult();

                return (v == null) ? false : v.getUpdateInProcess();
            }
        });
    }

    /*
    * Load a mysql script from a resource
    * */
    private static String getSQLScript(String filename) throws IOException {
        InputStream is = BootstrapMigratorServiceImpl.class.getResourceAsStream(filename);
        StringBuilder out = new StringBuilder();

        byte[] b = new byte[4096];
        for (int n; (n = is.read(b)) != -1;) {
            out.append(new String(b, 0, n));
        }

        return out.toString();
    }

    private static String[] getSQLCommands(String filename) throws IOException {
        String sqlString = getSQLScript(filename);
        ArrayList<String> sqlCommands = new ArrayList<String>();

        String sqlCommandsTemp[] = sqlString.split(";");

        for (String sqlCommand : sqlCommandsTemp) {
            if (sqlCommand.trim().length() > 0) {
                sqlCommands.add(sqlCommand);
            }
        }
        return sqlCommands.toArray(new String[0]);
    }

    private void setVersionData() throws IOException {
        setBinaryVersion();
        setDatabaseVersion();
    }

    /**
     * Get the current version of the binaries
     * <p/>
     * Assumptions about the version number: 
     *   Only contains single-digit integers between dots (e.g., 2.2.1.6.9.3)
     *
     * @return binary version
     * @throws IOException when the class loader fails
     */
    private void setBinaryVersion() throws IOException {
        InputStream is = BootstrapMigratorServiceImpl.class.getResourceAsStream("version.properties");

        Properties prop = new Properties();
        prop.load(is);

        String sVersion = (String) prop.get("version");

        if (sVersion.indexOf("-SNAPSHOT") > 0) {
            sVersion = sVersion.substring(0, sVersion.indexOf("-SNAPSHOT"));
            this.isSnapshot = true;
        }

        Double dVersion;

        //  If the version has multiple dots, then it cannot be directly parsed as a Double.
        String[] versionArray = sVersion.split("\\.");
        if (versionArray.length < 3) { //  example version numbers: 2 and 2.2 and 2.46 and 3.65
            dVersion = Double.parseDouble(sVersion) * 100; //         200   220     246      365
        } else { //  example version numbers: 2.1.1 and 2.2.5.3 and
            dVersion = Double.parseDouble(versionArray[0] + "." + versionArray[1]) * 100;
            for (int i = 2; i < versionArray.length; i++) {
                dVersion = dVersion
                        + Math.pow((new Double(versionArray[i])).doubleValue(), (new Double(1 - i)).doubleValue());
            }
        }

        this.binaryVersion = dVersion.intValue();
    }

    /*
    * Get the current version of the database
    * */
    @SuppressWarnings("unchecked")
    private void setDatabaseVersion() {
        this.dbVersion = ((Integer) hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                SQLQuery q = session.createSQLQuery("show tables");
                List<String> tables = q.list();

                //Check to see if the version table exists.
                //If it does not exist then it's ambra 2.00
                if (!tables.contains("version")) {
                    return 210;
                }

                //If we get this far, return the version column out of the database
                Criteria c = session.createCriteria(Version.class).setProjection(Projections.max("version"));

                Integer i = (Integer) c.uniqueResult();

                return (i == null) ? 210 : c.uniqueResult();
            }
        }));
    }

    private void execSQLScript(Session session, String sqlScript) throws SQLException, HibernateException {
        log.debug("{} started.", sqlScript);
        String sqlStatements[] = { "" };

        Transaction transaction = session.getTransaction();

        try {
            sqlStatements = getSQLCommands(sqlScript);
        } catch (IOException ex) {
            throw new HibernateException(ex.getMessage(), ex);
        }

        transaction.begin();

        for (String sqlStatement : sqlStatements) {
            log.debug("Running: {}", sqlStatement);
            session.createSQLQuery(sqlStatement).executeUpdate();
        }

        transaction.commit();
        log.debug("{} completed.", sqlScript);
    }
}