com.tacitknowledge.util.migration.jdbc.PatchTable.java Source code

Java tutorial

Introduction

Here is the source code for com.tacitknowledge.util.migration.jdbc.PatchTable.java

Source

/* Copyright 2004 Tacit Knowledge
 *  
 * 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 com.tacitknowledge.util.migration.jdbc;

import com.tacitknowledge.util.migration.MigrationException;
import com.tacitknowledge.util.migration.PatchInfoStore;
import com.tacitknowledge.util.migration.jdbc.util.SqlUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;

/**
 * Manages interactions with the "patches" table.  The patches table stores
 * the current patch level for a given system, as well as a system-scoped lock
 * use to avoid concurrent patches to the system.  A system is defined as an
 * exclusive target of a patch.
 * <p/>
 * This class is responsible for:
 * <ul>
 * <li>Validating the existence of the patches table and creating it if it
 * doesn't exist</li>
 * <li>Determining if a patch is currently running on a given system</li>
 * <li>Obtaining and releasing patch locks for a given system</li>
 * <li>Obtaining and incrementing the patch level for a given system</li>
 * </ul>
 * <p/>
 * <strong>TRANSACTIONS:</strong> Transactions should be committed by the calling
 * class as needed.  This class does not explictly commit or rollback transactions.
 *
 * @author Scott Askew (scott@tacitknowledge.com)
 */
public class PatchTable implements PatchInfoStore {
    /**
     * Class logger
     */
    private static Log log = LogFactory.getLog(PatchTable.class);

    /**
     * The migration configuration
     */
    private JdbcMigrationContext context = null;

    /**
     * Keeps track of table validation (see #createPatchesTableIfNeeded)
     */
    private boolean tableExistenceValidated = false;

    /**
     * Create a new <code>PatchTable</code>.
     *
     * @param migrationContext the migration configuration and connection source
     */
    public PatchTable(JdbcMigrationContext migrationContext) {
        this.context = migrationContext;

        if (context.getDatabaseType() == null) {
            throw new IllegalArgumentException("The JDBC database type is required");
        }
    }

    /**
     * {@inheritDoc}
     */
    public void createPatchStoreIfNeeded() throws MigrationException {
        if (tableExistenceValidated) {
            return;
        }

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            conn = context.getConnection();

            stmt = conn.prepareStatement(getSql("level.table.exists"));
            stmt.setString(1, context.getSystemName());
            rs = stmt.executeQuery();
            log.debug("'patches' table already exists.");
            tableExistenceValidated = true;
        } catch (SQLException e) {
            // logging error in case it's not a simple patch table doesn't exist error
            log.debug(e.getMessage());
            SqlUtil.close(null, stmt, rs);

            // check connection is valid before using, because the getConnection() call
            // could have thrown the SQLException
            if (null == conn) {
                throw new MigrationException("Unable to create a connection.", e);
            }

            log.info("'patches' table must not exist; creating....");
            try {
                stmt = conn.prepareStatement(getSql("patches.create"));
                if (log.isDebugEnabled()) {
                    log.debug("Creating patches table with SQL '" + getSql("patches.create") + "'");
                }
                stmt.execute();
                context.commit();

                // We don't yet have a patch record for this system; create one
                createSystemPatchRecord();
            } catch (SQLException sqle) {
                throw new MigrationException("Unable to create patch table", sqle);
            }
            tableExistenceValidated = true;
            log.info("Created 'patches' table.");
        } catch (Exception ex) {
            throw new MigrationException("Unexpected exception while creating patch store.", ex);
        } finally {
            SqlUtil.close(conn, stmt, rs);
        }
    }

    /**
     * {@inheritDoc}
     */
    public int getPatchLevel() throws MigrationException {
        createPatchStoreIfNeeded();

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql("level.read"));
            stmt.setString(1, context.getSystemName());
            rs = stmt.executeQuery();
            if (rs.next()) {
                return rs.getInt(1);
            }

            SqlUtil.close(conn, stmt, rs);
            conn = null;
            stmt = null;
            rs = null;

            return 0;
        } catch (SQLException e) {
            throw new MigrationException("Unable to get patch level", e);
        } finally {
            SqlUtil.close(conn, stmt, rs);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void updatePatchLevel(int level) throws MigrationException {
        // Make sure a patch record already exists for this system
        getPatchLevel();

        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql("level.update"));
            stmt.setInt(1, level);
            stmt.setString(2, context.getSystemName());
            stmt.execute();
            context.commit();
        } catch (SQLException e) {
            throw new MigrationException("Unable to update patch level", e);
        } finally {
            SqlUtil.close(conn, stmt, null);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isPatchStoreLocked() throws MigrationException {
        createPatchStoreIfNeeded();

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql("lock.read"));
            stmt.setString(1, context.getSystemName());
            stmt.setString(2, context.getSystemName());
            rs = stmt.executeQuery();

            if (rs.next()) {
                return ("T".equals(rs.getString(1)));
            } else {
                return false;
            }
        } catch (SQLException e) {
            throw new MigrationException("Unable to determine if table is locked", e);
        } finally {
            SqlUtil.close(conn, stmt, rs);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void lockPatchStore() throws MigrationException, IllegalStateException {
        createPatchStoreIfNeeded();
        if (!updatePatchLock(true)) {
            throw new IllegalStateException("Patch table is already locked!");
        }
    }

    /**
     * {@inheritDoc}
     */
    public void unlockPatchStore() throws MigrationException {
        updatePatchLock(false);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isPatchApplied(int patchLevel) throws MigrationException {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql("level.exists"));
            stmt.setString(1, context.getSystemName());
            stmt.setString(2, String.valueOf(patchLevel));
            rs = stmt.executeQuery();
            if (rs.next()) {
                return patchLevel == rs.getInt(1);
            } else {
                return false;
            }

        } catch (SQLException e) {
            throw new MigrationException("Unable to determine if patch has been applied", e);
        } finally {
            SqlUtil.close(conn, stmt, rs);
        }
    }

    public void updatePatchLevelAfterRollBack(int rollbackLevel) throws MigrationException {
        // Make sure a patch record already exists for this system
        getPatchLevel();

        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql("level.rollback"));
            stmt.setInt(1, rollbackLevel);
            stmt.setString(2, context.getSystemName());
            stmt.execute();
            context.commit();
        } catch (SQLException e) {
            throw new MigrationException("Unable to update patch level", e);
        } finally {
            SqlUtil.close(conn, stmt, null);
        }

    }

    /**
     * Returns the SQL to execute for the database type associated with this patch table.
     *
     * @param key the key within <code><i>database</i>.properties</code> whose
     *            SQL should be returned
     * @return the SQL to execute for the database type associated with this patch table
     */
    protected String getSql(String key) {
        return context.getDatabaseType().getProperty(key);
    }

    /**
     * Creates an initial record in the patches table for this system.
     *
     * @throws SQLException       if an unrecoverable database error occurs
     * @throws MigrationException if an unrecoverable database error occurs
     */
    private void createSystemPatchRecord() throws MigrationException, SQLException {
        String systemName = context.getSystemName();
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql("level.create"));
            stmt.setString(1, systemName);
            stmt.execute();
            context.commit();
            log.info("Created patch record for " + systemName);
        } catch (SQLException e) {
            log.error("Error creating patch record for system '" + systemName + "'", e);
            throw e;
        } finally {
            SqlUtil.close(conn, stmt, null);
        }
    }

    /**
     * Obtains or releases a lock for this system in the patches table.  If the lock
     * was not obtained or released, then <code>false</code> is returned.
     *
     * @param  lock <code>true</code> if a lock is to be obtained, <code>false</code>
     *         if it is to be removed
     * @return true if the lock was updated successfully, otherwise false
     * @throws MigrationException if an unrecoverable database error occurs
     */
    private boolean updatePatchLock(boolean lock) throws MigrationException {
        String sqlkey = (lock) ? "lock.obtain" : "lock.release";
        Connection conn = null;
        PreparedStatement stmt = null;

        try {
            conn = context.getConnection();
            stmt = conn.prepareStatement(getSql(sqlkey));
            if (log.isDebugEnabled()) {
                log.debug("Updating patch table lock: " + getSql(sqlkey));
            }
            stmt.setString(1, context.getSystemName());
            if (lock) {
                stmt.setString(2, context.getSystemName());
            }

            int rowsUpdated = stmt.executeUpdate();
            boolean lockUpdated = (rowsUpdated == 1);
            context.commit();
            if (log.isDebugEnabled()) {
                log.debug(((lock) ? "Obtained" : "Released") + " lock? " + lockUpdated);
            }
            return lockUpdated;
        } catch (SQLException e) {
            throw new MigrationException("Unable to update patch lock to " + lock, e);
        } finally {
            SqlUtil.close(conn, stmt, null);
        }
    }

    public Set<Integer> getPatchesApplied() throws MigrationException {
        createPatchStoreIfNeeded();

        Connection connection = null;
        PreparedStatement stmt = null;
        ResultSet resultSet = null;
        Set<Integer> patches = new HashSet<Integer>();
        try {
            connection = context.getConnection();
            stmt = connection.prepareStatement(getSql("patches.all"));
            stmt.setString(1, context.getSystemName());
            resultSet = stmt.executeQuery();
            while (resultSet.next()) {
                patches.add(resultSet.getInt("patch_level"));
            }

        } catch (SQLException e) {
            throw new MigrationException("Unable to get patch levels", e);
        } finally {
            SqlUtil.close(connection, stmt, resultSet);
        }
        return patches;
    }
}