All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tacitknowledge.util.migration.jdbc.SqlScriptMigrationTask Maven / Gradle / Ivy

There is a newer version: 1.4.2
Show newest version
/* 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 java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Pattern;

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

import com.tacitknowledge.util.migration.MigrationContext;
import com.tacitknowledge.util.migration.MigrationException;
import com.tacitknowledge.util.migration.MigrationTaskSupport;
import com.tacitknowledge.util.migration.jdbc.util.SqlUtil;
import com.tacitknowledge.util.migration.jdbc.util.SybaseUtil;

/**
 * Adaptss a SQL or DDL database patch for use with the AutoPatch framework.  
 *  
 * @author  Scott Askew ([email protected])
 */
public class SqlScriptMigrationTask extends MigrationTaskSupport
{
    /**
     * Class logger
     */
    private static Log log = LogFactory.getLog(SqlScriptMigrationTask.class);
    
    /**
     * The SQL to execute
     */
    private String sql = null;
    
    /**
     * SQL to migrate down a patch level
     */
    private String downSql = null;
    
    
    /**
     * Creates a new SqlScriptMigrationTask.
     */
    public SqlScriptMigrationTask()
    {
        // do nothing
    }
    
    /**
     * Creates a new SqlScriptMigrationTask
     * 
     * @param name the name of the SQL script to execute; this is just an
     * 	      identifier and does not have to correspond to a file name 
     * @param level the patch level of the migration task
     * @param upSql the SQL to execute to migrate the patch level up one
     * @param downSql the SQL to execute to rollback to this patch
     */
    public SqlScriptMigrationTask(String name, int level, String upSql, String downSql)
    {
	setName(name);
	setLevel(new Integer(level));
	this.sql = upSql;
	this.downSql = downSql;
	
	setRollbackSupported(!"".equals(downSql));
    }
    
    /**
     * Creates a new SqlScriptMigrationTask
     *
     * @param name the name of the SQL script to execute; this is just an
     *        identifier and does not have to correspond to a file name 
     * @param level the patch level of the migration task
     * @param sql the SQL to execute	
     */ 
    public SqlScriptMigrationTask(String name, int level, String sql)
    {
        setName(name);
        setLevel(new Integer(level));
        this.sql = sql;
        this.downSql = "";
    }
    
    /**
     * Creates a new SqlScriptMigrationTask containing the SQL
     * contained in the given InputStream.
     *
     * @param  name the name of the SQL script to execute; this is just an
     *         identifier and does not have to correspond to a file name 
     * @param  level the patch level of the migration task
     * @param  is the source of the SQL to execute
     * @throws IOException if there was problem reading the input stream
     */
    public SqlScriptMigrationTask(String name, int level, InputStream is) throws IOException
    {
        setName(name);
        setLevel(new Integer(level));
        StringBuffer sqlBuffer = new StringBuffer();
        BufferedReader buf = new BufferedReader(new InputStreamReader(is));
        String line = buf.readLine();
        while (line != null)
        {
            sqlBuffer.append(line).append("\n");
            line = buf.readLine();
        }
        sql = sqlBuffer.toString();
    }
    
    /** {@inheritDoc} */
    public void up(MigrationContext context) throws MigrationException 
    {
	executeSql(context, sql);
    }
    
    /** {@inheritDoc} */
    public void down(MigrationContext context) throws MigrationException
    {
	executeSql(context, downSql);
    }
    
    /**
     * Executes the passed sql in the passed context.
     * 
     * @param ctx the MigrationContext> to execute the SQL in
     * @param sqlToExec the SQL to execute
     * @throws MigrationException thrown if there is an error when executing the SQL
     */
    private void executeSql(MigrationContext ctx, String sqlToExec) 
    	throws MigrationException
    {
        JdbcMigrationContext context = (JdbcMigrationContext) ctx;
        
        Connection conn = null;
        Statement stmt = null;
        String sqlStatement = "";
        ListIterator listIterator = null;
        try
        {
            conn = context.getConnection();
            
            // cleaning the slate before we execute the patch.
            // This was inspired by a Sybase ASE server that did not allow
            // ALTER TABLE statements in multi-statement transactions.  Instead of putting
            // a if(sybase) conditional, we decided to clean the slate for everyone.
            context.commit();

            List sqlStatements = getSqlStatements(context, sqlToExec);
            for (listIterator = sqlStatements.listIterator(); listIterator.hasNext();)
            {
                sqlStatement = (String) listIterator.next();
                log.debug(getName() + ": Attempting to execute: " + sqlStatement);

                stmt = conn.createStatement();
                
                // handle sybase special case with illegal commands in multi
                // command transactions
                if (isSybase(context) 
                      && SybaseUtil.containsIllegalMultiStatementTransactionCommand(sqlStatement))
                {
                    log.warn("Committing current transaction since patch " + getName()
                            + " contains commands that are not allowed in multi statement"
                            + " transactions.  If the patch contains errors, this patch may"
                            + " not be rolled back cleanly.");
                    context.commit();
                    stmt.execute(sqlStatement);
                    context.commit();
                }
                else // regular case
                {
                    stmt.execute(sqlStatement);
                }

                SqlUtil.close(null, stmt, null);
            }
            
            context.commit();
        }
        catch (Exception e)
        {
            String message = getName() + ": Error running SQL at statement number "
                + listIterator.previousIndex() + " \"" + sqlStatement + "\"";
            log.error(message, e);
            
            if (e instanceof SQLException)
            {
                if (((SQLException) e).getNextException() != null)
                {
                    log.error("Chained SQL Exception", ((SQLException) e).getNextException());
                }
            }
            
            context.rollback();
            throw new MigrationException(message, e);
        }
        finally
        {
            SqlUtil.close(null, stmt, null);
        }
    }
    
    public List getSqlStatements(JdbcMigrationContext context) 
    {
	return getSqlStatements(context, sql);
    }
    
    /**
     * Parses the SQL/DDL to execute and returns a list of individual statements.  For database
     * types that support mulitple statements in a single Statement.execute call,
     * this method will return a one-element List containing the entire SQL
     * file.
     * 
     * @param  context the MigrationContext, to figure out db type and if it 
     *                 can handle multiple statements at once
     * @return a list of SQL and DDL statements to execute
     */
    public List getSqlStatements(JdbcMigrationContext context, String sqlStatements)
    {
        List statements = new ArrayList();
        if (context.getDatabaseType().isMultipleStatementsSupported())
        {
            statements.add(sqlStatements);
            return statements;
        }
        
        StringBuffer currentStatement = new StringBuffer();
        boolean inQuotedString = false;
        boolean inComment = false;
        char[] sqlChars = sqlStatements.toCharArray();
        for (int i = 0; i < sqlChars.length; i++)
        {
            if (sqlChars[i] == '\n')
            {
                inComment = false;
            }
            
            if (!inComment)
            {
                switch (sqlChars[i])
                {
                    case '-' :
                    case '/' :
                        if (!inQuotedString && i + 1 < sqlChars.length
                            && sqlChars[i + 1] == sqlChars[i])
                        {
                            inComment = true;
                        }
                        else
                        {
                            currentStatement.append(sqlChars[i]);
                        }
                        break;
                    case '\'' :
                        inQuotedString = !inQuotedString;
                        currentStatement.append(sqlChars[i]);
                        break;
                    case ';' :
                        if (!inQuotedString)
                        {
                            // If we're in a stored procedure, just keep rolling
                            if (isStoredProcedure(context.getDatabaseType().getDatabaseType(),
                                                  currentStatement.toString())) 
                            {
                                currentStatement.append(sqlChars[i]);
                            }
                            else 
                            {
                                statements.add(currentStatement.toString().trim());
                                currentStatement = new StringBuffer();
                            }
                        }
                        else
                        {
                            currentStatement.append(sqlChars[i]);
                        }
                        break;
                    /* sybase uses 'GO' as it's statement delimiter */
                    case 'g':
                    case 'G':
                        /*
                         * Build up a string, reading backwards from the current index to
                         * the previous newline (or beginning of sequence) and from the
                         * current index up to the next newline.  If it matches the regex
                         * for the GO delimiter, then add the statement otherwise
                         * just append the current index's character to currentStatement
                         */
                        if (context.getDatabaseType().getDatabaseType().equals("sybase"))
                        {
                            // read from current index to previous line terminator 
                            // or start of sequence
                            StringBuffer previous = new StringBuffer();
                            for (int j = i - 1; j >= 0; j--) 
                            {
                                char c = sqlChars[j];
                                previous.append(c);
                                if (isLineTerminator(c))
                                {
                                    break;
                                }
                            }
                            
                            // reverse previous, since we've been walking backwards, but appending
                            previous = previous.reverse();
                            
                            // read from current index to upcoming line terminator 
                            // or end of sequence.  If it is the GO delimiter, 
                            // we skip up to line terminator
                            StringBuffer after = new StringBuffer();
                            int newIndex = 0; 
                            for (int k = i + 1; k < sqlChars.length; k++) 
                            {
                                char c = sqlChars[k];
                                after.append(c);
                                newIndex = k;
                                if (isLineTerminator(c))
                                {
                                    break;
                                }
                            }
                            
                            // check against the pattern if its a GO delimiter
                            String possibleDelimiter = previous
                                .append(sqlChars[i]).append(after).toString();
                            final String delimiterPattern = "^\\s*[Gg][Oo]\\s*$";
                            
                            if (Pattern.matches(delimiterPattern, possibleDelimiter))
                            {
                                // if it's blank, don't bother adding it since Sybase
                                // will complain about empty queries.
                                // This happens if there are two GO's with no
                                // actual SQL to run between them.
                                if (!StringUtils.isBlank(currentStatement.toString().trim()))
                                {
                                    statements.add(currentStatement.toString().trim());
                                }
                                currentStatement = new StringBuffer();
                                // skip up to next line terminator
                                i = newIndex;
                            }
                            else // not a delimiter, so just append
                            {
                                currentStatement.append(sqlChars[i]);
                            }
                        }
                        else // not a sybase db, so just append
                        {
                            currentStatement.append(sqlChars[i]);
                        }
                        break;
                    default :
                        currentStatement.append(sqlChars[i]);
                        break;
                }
            }
        }
        if (currentStatement.toString().trim().length() > 0)
        {
            statements.add(currentStatement.toString().trim());
        }
        
        return statements;
    }
    
    /**
     * Return true if the string represents a stored procedure
     * 
     * @param databaseType the type of the database
     * @param statement the statement that may be a stored procedure
     * @return true if the statement is a stored procedure for the given db type
     */
    protected boolean isStoredProcedure(String databaseType, String statement)
    {
        String currentStatement = statement.trim().toLowerCase();
        if ("oracle".equals(databaseType) 
                && (currentStatement.startsWith("begin") 
                        || currentStatement.startsWith("create or replace method") 
                        || currentStatement.startsWith("create or replace function") 
                        || currentStatement.startsWith("create or replace procedure") 
                        || currentStatement.startsWith("create or replace package")))
        {
            return true;
        }
        
        return false;
    }
    
    /**
     * return true if c is a line terminator as detailed in
     * http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Pattern.html
     * @param c the char to test
     * @return true if it is a line terminator
     */
    protected boolean isLineTerminator(char c)
    {
        return (c == '\n') // newline
            || (c == '\r') // carriage return
            || (c == '\u0085') // next-line
            || (c == '\u2028') // line-separator
            || (c == '\u2029'); // paragraph separator
    }
    
    /**
     * Check if the current migration context is against a sybase database.
     * @param context the context to check.
     * @return true if context is in a sybase database.
     */
    protected boolean isSybase(JdbcMigrationContext context)
    {
        return context.getDatabaseType().getDatabaseType().equalsIgnoreCase("sybase");
    }

    /** {@inheritDoc} */
    public String toString()
    {
        return getName();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy