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

play.db.Evolutions Maven / Gradle / Ivy

There is a newer version: 1.5.0
Show newest version
package play.db;

import org.apache.commons.lang.StringUtils;

import play.Logger;
import play.Play;
import play.PlayPlugin;
import play.classloading.ApplicationClasses;
import play.classloading.ApplicationClassloader;
import play.db.evolutions.Evolution;
import play.db.evolutions.EvolutionQuery;
import play.db.evolutions.EvolutionState;
import play.db.evolutions.exceptions.InconsistentDatabase;
import play.db.evolutions.exceptions.InvalidDatabaseRevision;
import play.exceptions.UnexpectedException;
import play.libs.IO;
import play.mvc.Http.Request;
import play.mvc.Http.Response;
import play.mvc.results.Redirect;
import play.vfs.VirtualFile;

import java.io.File;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;


/**
 * Handles migration of data.
 *
 * Does only support the default DBConfig
 */
public class Evolutions extends PlayPlugin {

    private static String EVOLUTIONS_TABLE_NAME = "play_evolutions";
    protected static File evolutionsDirectory = Play.getFile("db/evolutions");
    
    private static Map modulesWithEvolutions = new LinkedHashMap();


    public static void main(String[] args) throws SQLException {
        /** Start the DB plugin **/
        Play.id = System.getProperty("play.id");
        Play.applicationPath = new File(System.getProperty("application.path"));
        Play.guessFrameworkPath();
        Play.readConfiguration();
        Play.javaPath = new ArrayList();
        Play.classes = new ApplicationClasses();
        Play.classloader = new ApplicationClassloader();

        Play.templatesPath = new ArrayList();
        Play.modulesRoutes = new HashMap();
        Play.loadModules(VirtualFile.open(Play.applicationPath));


        if (System.getProperty("modules") != null) {
            populateModulesWithSpecificModules();
        } else {
            populateModulesWithEvolutions();
        }

        if (modulesWithEvolutions.isEmpty()) {
            System.out.println("~ Nothing has evolutions, go away and think again.");
            System.exit(-1);
            return;
        }

        Logger.init();
        Logger.setUp("ERROR");
        new DBPlugin().onApplicationStart();
        
        // Look over all the DB
        Set dBNames = Configuration.getDbNames();
        boolean defaultExitCode = true;
                
        for (String dbName : dBNames) {
            Configuration dbConfig = new Configuration(dbName);
            /** Connected **/
            System.out.println("~ Connected to " + DB.getDataSource(dbName).getConnection().getMetaData().getURL());

            for (Entry moduleRoot : modulesWithEvolutions.entrySet()) {

                /** Summary **/
                Evolution database = listDatabaseEvolutions(dbName, moduleRoot.getKey()).peek();
                Evolution application = listApplicationEvolutions(dbName, moduleRoot.getKey(), moduleRoot.getValue()).peek();

                boolean needToCheck = true;
                if ("resolve".equals(System.getProperty("mode"))) {
                    needToCheck = handleResolveAction(dbName, moduleRoot);
                }
                if (needToCheck) {
                    /** Check inconsistency **/
                    try {
                        checkEvolutionsState(dbName);
                    } catch (InconsistentDatabase e) {
                        defaultExitCode = false;
                        System.out.println("~");
                        System.out.println("~ Your database " + dbName + " is in an inconsistent state!");
                        System.out.println("~");
                        System.out.println("~ While applying this script part:");
                        System.out.println("");
                        System.out.println(e.getEvolutionScript());
                        System.out.println("");
                        System.out.println("~ The following error occured:");
                        System.out.println("");
                        System.out.println(e.getError());
                        System.out.println("");
                        System.out
                                .println("~ Please correct it manually, and mark it resolved by running `play evolutions:resolve`");
                        System.out.println("~");
                        continue;
                    } catch (InvalidDatabaseRevision e) {
                        // see later
                    }

                    System.out.print("~ '" + moduleRoot.getKey() + "' Application revision is " + application.revision
                            + " [" + application.hash.substring(0, 7) + "]");
                    System.out.println(" and '" + moduleRoot.getKey() + "' Database revision is " + database.revision
                            + " [" + database.hash.substring(0, 7) + "]");
                    System.out.println("~");

                    /** Evolution script **/
                    List evolutions = getEvolutionScript(dbName, moduleRoot.getKey(), moduleRoot.getValue());
                    if (evolutions.isEmpty()) {
                        System.out.println("~ Your database " + dbName + " is up to date for " + moduleRoot.getKey());
                        System.out.println("~");
                    } else {
                        if ("apply".equals(System.getProperty("mode"))) {
                            if (!handleApplyAction(dbName, moduleRoot, evolutions)) {
                                defaultExitCode = false;
                            }
                        } else if ("markApplied".equals(System.getProperty("mode"))) {
                            if (!handleMarkAppliedAction(dbName, moduleRoot, evolutions)) {
                                defaultExitCode = false;
                            }
                        } else {
                            defaultExitCode = false;
                            handleDefaultAction(dbName, moduleRoot, evolutions);
                        }
                    }
                }
            }
        }
        
        if (!defaultExitCode) {
            System.exit(-1);
        }
    }
    /**
     * Method to handle the "default" action
     * @param dbName : database name
     * @param moduleRoot : the module root of evolutions
     * @param evolutions : list of evolutions
     */
    private static void handleDefaultAction(String dbName,  Entry moduleRoot, List evolutions){
        System.out.println("~ Your database " + dbName + " needs evolutions for " + moduleRoot.getKey() + "!");
        System.out.println("");
        System.out
                .println("# ------------------------------------------------------------------------------");
        System.out.println("");
        System.out.println(toHumanReadableScript(evolutions));
        System.out.println("");
        System.out
                .println("# ------------------------------------------------------------------------------");
        System.out.println("");
        System.out
                .println("~ Run `play evolutions:apply` to automatically apply this script to the database");
        System.out
                .println("~ or apply it yourself and mark it done using `play evolutions:markApplied`");
        System.out.println("~"); 
    }
    
    /**
     * Method to handle the "resolve" action
     * @param dbName : database name
     * @param moduleRoot : the module root of evolutions
     * @param evolutions : list of evolutions
     * @return true if need to check, false otherwise
     */
    private static boolean handleResolveAction(String dbName,  Entry moduleRoot){
        try {
            checkEvolutionsState(dbName);
            System.out.println("~");
            System.out.println("~ Nothing to resolve for " + moduleRoot.getKey() + "...");
            System.out.println("~");
            return false;
        } catch (InconsistentDatabase e) {
            resolve(dbName, moduleRoot.getKey(), e.getRevision());
            System.out.println("~");
            System.out.println("~ Revision " + e.getRevision() + " for " + moduleRoot.getKey()
                    + " has been resolved;");
            System.out.println("~");
        } catch (InvalidDatabaseRevision e) {
            // see later
        }
        return true;
    }
    
    /**
     * Method to handle the "apply" action
     * @param dbName : database name
     * @param moduleRoot : the module root of evolutions
     * @param evolutions : list of evolutions
     * @return true if action was applied successfully, false otherwise
     */
    private static boolean handleApplyAction(String dbName,  Entry moduleRoot, List evolutions){
        System.out.println("~ Applying evolutions for " + moduleRoot.getKey() + ":");
        System.out.println("");
        System.out
                .println("# ------------------------------------------------------------------------------");
        System.out.println("");
        System.out.println(toHumanReadableScript(evolutions));
        System.out.println("");
        System.out
                .println("# ------------------------------------------------------------------------------");
        System.out.println("");
        if (applyScript(dbName, true, moduleRoot.getKey(), moduleRoot.getValue())) {
            System.out.println("~");
            System.out.println("~ Evolutions script successfully applied for " + moduleRoot.getKey()
                    + "!");
            System.out.println("~");
            return true;
        } else {
            System.out.println("~");
            System.out.println("~ Can't apply evolutions for " + moduleRoot.getKey() + "...");
            System.out.println("~");
            return false;
        }
    }
    
    /**
     * Method to handle the "markApplied" action
     * @param dbName : database name
     * @param moduleRoot : the module root of evolutions
     * @param evolutions : list of evolutions
     * @return true if action was applied successfully, false otherwise
     */
    private static boolean handleMarkAppliedAction(String dbName,  Entry moduleRoot, List evolutions){ 
        if (applyScript(dbName, false, moduleRoot.getKey(), moduleRoot.getValue())) {
            System.out
                    .println("~ Evolutions script marked as applied for " + moduleRoot.getKey() + "!");
            System.out.println("~");
            return true;
        } else {
            System.out.println("~ Can't apply evolutions for " + moduleRoot.getKey() + "...");
            System.out.println("~");
            return false;
        }
    }
    

    private static void populateModulesWithSpecificModules() {
        String[] specificModules = System.getProperty("modules").split(",");

        System.out.println("~ You've requested running evolutions only for these modules: ");
        for (String specificModule : specificModules) {
            System.out.println("~~ '" + specificModule + "'");
        }
        System.out.println("~");

        boolean weShouldAddTheMainProject = false;

        for (String specificModule : specificModules) {
            if (Play.modules.containsKey(specificModule)) {
                VirtualFile moduleRoot = Play.modules.get(specificModule);
                
                if(!isModuleEvolutionDisabled(specificModule) && moduleRoot.child("db/evolutions").exists()) {
                    modulesWithEvolutions.put(specificModule, moduleRoot.child("db/evolutions"));
                } else {
                    System.out.println("~ '" + specificModule + "' module doesn't have any evolutions scripts in it or evolutions are disabled.");
	                System.out.println("~");
                    System.exit(-1);
                }
            } else if (Play.configuration.getProperty("application.name").equals(specificModule))  {
                weShouldAddTheMainProject = true;
            } else {
                System.out.println("~ Couldn't find a module with the name '" + specificModule + "'. ");
                System.exit(-1);
            }
        }

        if (weShouldAddTheMainProject) {
            addMainProjectToModuleList();
        }
    }

    private static void populateModulesWithEvolutions() {
        /** Check that evolutions are enabled **/
        if(!isModuleEvolutionDisabled()){
            for(Entry moduleRoot : Play.modules.entrySet()) {
                if(moduleRoot.getValue().child("db/evolutions").exists()) {
                    if(!isModuleEvolutionDisabled(moduleRoot.getKey())){
                        modulesWithEvolutions.put(moduleRoot.getKey(), moduleRoot.getValue().child("db/evolutions"));
                    } else {
                        System.out.println("~ '" + moduleRoot.getKey() + "' module evolutions are disabled.");
                    }
                }
            }
        }else{
            System.out.println("~ Module evolutions are disabled.");
        }

        addMainProjectToModuleList();
    }

    private static void addMainProjectToModuleList() {
        if (evolutionsDirectory.exists()) {
            modulesWithEvolutions.put(Play.configuration.getProperty("application.name"), VirtualFile.open(evolutionsDirectory));
        }
    }


    @Override
    public boolean rawInvocation(Request request, Response response) throws Exception {

        // Mark an evolution as resolved
        if (Play.mode.isDev() && request.method.equals("POST") && request.url.matches("^/@evolutions/force/[a-zA-Z0-9]+/[0-9]+$")) {            
            int index = request.url.lastIndexOf("/@evolutions/force/") + "/@evolutions/force/".length();
            
            String dbName = DB.DEFAULT;
            String moduleKey = request.url.substring(index, request.url.lastIndexOf("/"));
            int revision = Integer.parseInt(request.url.substring(request.url.lastIndexOf("/") + 1));
            
            resolve(dbName, moduleKey, revision);
            new Redirect("/").apply(request, response);
            return true;
        }

        // Apply the current evolution script
        if (Play.mode.isDev() && request.method.equals("POST") && request.url.equals("/@evolutions/apply")) {
            
             for(Entry moduleRoot : modulesWithEvolutions.entrySet()) {            
                 applyScript(true, moduleRoot.getKey(), moduleRoot.getValue());
             }
            new Redirect("/").apply(request, response);
            return true;
        }
        return super.rawInvocation(request, response);
    }

    @Override
    public void beforeInvocation() {
        if(isDisabled() || Play.mode.isProd()) {
            return;
        }
        try {
            checkEvolutionsState();
        } catch (InvalidDatabaseRevision e) {
            Set dbNames = Configuration.getDbNames();
            for (String dbName : dbNames) {
                Configuration dbConfig = new Configuration(dbName);
              
                for (Entry moduleRoot : modulesWithEvolutions.entrySet()) {
                    if ("mem".equals(dbConfig.getProperty("db"))
                            && listDatabaseEvolutions(e.getDbName(), moduleRoot.getKey()).peek().revision == 0) {
                        Logger.info("Automatically applying evolutions in in-memory database");
                        Logger.info("Applying evolutions for '" + moduleRoot.getKey() + "'");
                        applyScript(true, moduleRoot.getKey(), moduleRoot.getValue());
                    } else {
                        throw e;
                    }
                }  
            }
        }
    }

    @Override
    public void onApplicationStart() {
        if (!isDisabled()) {
            populateModulesWithEvolutions();
            if ( Play.mode.isProd()) {
                try {
                    checkEvolutionsState();
                } catch (InvalidDatabaseRevision e) {
                    Logger.warn("");
                    Logger.warn("Your database is not up to date.");
                    Logger.warn("Use `play evolutions` command to manage database evolutions.");
                    throw e;
                }
            }
        }        
    }

    /**
     * Checks if evolutions is disabled in application.conf (property "evolutions.enabled")
     */
    private boolean isDisabled() {
        return "false".equals(Play.configuration.getProperty("evolutions.enabled", "true"));
    }
    
    private static boolean isModuleEvolutionDisabled() {
        return "false".equals(Play.configuration.getProperty("modules.evolutions.enabled", "true")); 
    }
    
    private static boolean isModuleEvolutionDisabled(String name) {
        return "false".equals(Play.configuration.getProperty(name + ".evolutions.enabled", "true")); 
    }

    public static boolean autoCommit() {
        return ! "false".equals(Play.configuration.getProperty("evolutions.autocommit", "true"));
    }
    
    
    public static synchronized void resolve(int revision) {
        try {
            EvolutionQuery.resolve(DB.DEFAULT, revision, Play.configuration.getProperty("application.name"));
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }
    
    public static synchronized void resolve(String dBName, int revision) {
        try {
            EvolutionQuery.resolve(dBName, revision, Play.configuration.getProperty("application.name"));
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }

    
    public static synchronized void resolve(String dBName, String moduleKey, int revision) {
        try {
            EvolutionQuery.resolve(dBName, revision, moduleKey);
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }

    public static synchronized boolean applyScript(boolean runScript, String moduleKey, VirtualFile evolutionsDirectory) {       
        // Look over all the DB
        Set dBNames = Configuration.getDbNames();
        for(String dbName: dBNames){
            return applyScript(dbName, runScript, moduleKey, evolutionsDirectory);
        }
        return true;
    }
    
    
    public static synchronized boolean applyScript(String dbName, boolean runScript, String moduleKey, VirtualFile evolutionsDirectory) {
        try {
            Connection connection = EvolutionQuery.getNewConnection(dbName, Evolutions.autoCommit());
            int applying = -1;
            try {
                List evolutions = getEvolutionScript(dbName, moduleKey, evolutionsDirectory);
                for (Evolution evolution : evolutions) {
                    applying = evolution.revision;                  
                    EvolutionQuery.apply(connection, runScript, evolution, moduleKey);               
                }
                
                if(!Evolutions.autoCommit()) {
                    connection.commit();
                }
                
                return true;
            } catch (Exception e) {
            	Logger.error(e, "Can't apply evolution");
            	if(Evolutions.autoCommit()) {
	                String message = e.getMessage();
	                if (e instanceof SQLException) {
	                    SQLException ex = (SQLException) e;
	                    message += " [ERROR:" + ex.getErrorCode() + ", SQLSTATE:" + ex.getSQLState() + "]";
	                }
                
                	EvolutionQuery.setProblem(connection, applying, moduleKey, message);                 
            	} else {
            		connection.rollback();
            	}
                return false;
            } finally {
                EvolutionQuery.closeConnection(connection);               
            }
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }

    public static String toHumanReadableScript(List evolutionScript) {
        // Construct the script
        StringBuilder sql = new StringBuilder();
        boolean containsDown = false;
        for (Evolution evolution : evolutionScript) {
            if (!evolution.applyUp) {
                containsDown = true;
            }
            sql.append("# --- Rev:").append(evolution.revision).append(",").append(evolution.applyUp ? "Ups" : "Downs").append(" - ").append(evolution.hash.substring(0, 7)).append("\n");
            sql.append("\n");
            sql.append(evolution.applyUp ? evolution.sql_up : evolution.sql_down);
            sql.append("\n\n");
        }

        if (containsDown) {
            sql.insert(0, "# !!! WARNING! This script contains DOWNS evolutions that are likely destructives\n\n");
        }

        return sql.toString().trim();
    }
    
    public synchronized static void checkEvolutionsState() {        
        // Look over all the DB
        Set dBNames = Configuration.getDbNames();
        for(String dbName: dBNames){
            checkEvolutionsState(dbName);
        }
    }
    

    public synchronized static void checkEvolutionsState(String dbName) {
        for(Entry moduleRoot : modulesWithEvolutions.entrySet()) {            

            if (DB.getDataSource(dbName) != null) {
                List evolutionScript = getEvolutionScript(dbName, moduleRoot.getKey(), moduleRoot.getValue());
                Connection connection = null;
                try {
                    connection = EvolutionQuery.getNewConnection(dbName);   
                    ResultSet rs = EvolutionQuery.getEvolutionsToApply(connection, moduleRoot.getKey());
                    if (rs.next()) {
                        int revision = rs.getInt("id");
                        String state = rs.getString("state");
                        String hash = rs.getString("hash").substring(0, 7);
                        String script = "";
                        if (EvolutionState.APPLYING_UP.getStateWord().equals(state)) {
                            script = rs.getString("apply_script");
                        } else {
                            script = rs.getString("revert_script");
                        }
                        script = "# --- Rev:" + revision + "," + (EvolutionState.APPLYING_UP.getStateWord().equals(state) ? "Ups" : "Downs") + " - " + hash + "\n\n" + script;
                        String error = rs.getString("last_problem");
                        throw new InconsistentDatabase(dbName, script, error, revision, moduleRoot.getKey());
                    }
                } catch (SQLException e) {
                    throw new UnexpectedException(e);
                } finally {
                    EvolutionQuery.closeConnection(connection);
                }

                if (!evolutionScript.isEmpty()) {
                    throw new InvalidDatabaseRevision(dbName, toHumanReadableScript(evolutionScript));
                }
            }
        }
    }

    public synchronized static List getEvolutionScript(String dbName, String moduleKey, VirtualFile evolutionsDirectory) {
        Stack app = listApplicationEvolutions(dbName, moduleKey, evolutionsDirectory);
        Stack db = listDatabaseEvolutions(dbName, moduleKey);
        List downs = new ArrayList();
        List ups = new ArrayList();

        // Apply non conflicting evolutions (ups and downs)
        while (db.peek().revision != app.peek().revision) {
            if (db.peek().revision > app.peek().revision) {
                downs.add(db.pop());
            } else {
                ups.add(app.pop());
            }
        }

        // Revert conflicting to fork node
        while (db.peek().revision == app.peek().revision && !(db.peek().hash.equals(app.peek().hash))) {
            downs.add(db.pop());
            ups.add(app.pop());
        }

        // Ups need to be applied earlier first
        Collections.reverse(ups);

        List script = new ArrayList();
        script.addAll(downs);
        script.addAll(ups);

        return script;
    }

    public synchronized static Stack listApplicationEvolutions(String dBName, String moduleKey, VirtualFile evolutionsDirectory) {
        Stack evolutions = new Stack();
        evolutions.add(new Evolution("", 0, "", "", true));
        if (evolutionsDirectory.exists()) {
            for (File evolution : evolutionsDirectory.getRealFile().listFiles()) {
                if (evolution.getName().matches("^"+ dBName + ".[0-9]+[.]sql$") ||
                        (DB.DEFAULT.equals(dBName) && evolution.getName().matches("^[0-9]+[.]sql$") )) {
                    if (Logger.isTraceEnabled()) {
                        Logger.trace("Loading evolution %s", evolution);
                    }

                    int version = 0;
                    if(evolution.getName().contains(dBName)){
                        version = Integer.parseInt(evolution.getName().substring(evolution.getName().indexOf(".") + 1, evolution.getName().lastIndexOf(".")));
                    } else {
                        version = Integer.parseInt(evolution.getName().substring(0, evolution.getName().indexOf(".")));
                    }
                    
                    String sql = IO.readContentAsString(evolution);
                    StringBuffer sql_up = new StringBuffer();
                    StringBuffer sql_down = new StringBuffer();
                    StringBuffer current = new StringBuffer();
                    for (String line : sql.split("\r?\n")) {
                        if (line.trim().matches("^#.*[!]Ups")) {
                            current = sql_up;
                        } else if (line.trim().matches("^#.*[!]Downs")) {
                            current = sql_down;
                        } else if (line.trim().startsWith("#")) {
                            // skip
                        } else if (!StringUtils.isEmpty(line.trim())) {
                            current.append(line).append("\n");
                        }
                    }
                    evolutions.add(new Evolution(moduleKey, version, sql_up.toString(), sql_down.toString(), true));
                }
            }
            Collections.sort(evolutions);
        }
        return evolutions;
    }
    
    private static boolean isEvolutionsTableExist(Connection connection){
        String tableName = EVOLUTIONS_TABLE_NAME;
        try {
            ResultSet rs = connection.getMetaData().getTables(null, null, tableName, null);
            if (!rs.next()) {
                // Table in lowercase does not exist
                // oracle gives table names in upper case
                tableName = tableName.toUpperCase();
                Logger.trace("Checking " + tableName);
                rs.close();
                rs = connection.getMetaData().getTables(null, null, tableName, null);
                // Does it exist?
                if (!rs.next()) {
                    // did not find it in uppercase either
                    return false;
                }
            }
        } catch (SQLException e) {
            Logger.error(e, "SQL error while checking if play evolutions exist");
        }
        return true;     
    }

    public synchronized static Stack listDatabaseEvolutions(String dbName, String moduleKey) {
        Stack evolutions = new Stack();
        evolutions.add(new Evolution("", 0, "", "", false));
        Connection connection = null;
        try {
            connection = EvolutionQuery.getNewConnection(dbName);    
            // Do we have a
            if (isEvolutionsTableExist(connection) ) {           
                checkAndUpdateEvolutionsForMultiModuleSupport(dbName, connection);                    

                ResultSet databaseEvolutions = EvolutionQuery.getEvolutions(connection, moduleKey);
                                
                while (databaseEvolutions.next()) {
                    Evolution evolution = new Evolution(moduleKey, databaseEvolutions.getInt(1), databaseEvolutions.getString(3), databaseEvolutions.getString(4), false);
                    evolutions.add(evolution);
                }
            
            } else {
                EvolutionQuery.createTable(dbName);
            }
        } catch (SQLException e) {
            Logger.error(e, "SQL error while checking play evolutions");
        } finally {
            EvolutionQuery.closeConnection(connection);
        }
        Collections.sort(evolutions);
        return evolutions;
    }
    
    private static void checkAndUpdateEvolutionsForMultiModuleSupport(String dbName, Connection connection) throws SQLException {
        ResultSet rs = connection.getMetaData().getColumns(null, null, "play_evolutions", "module_key");
        if (!rs.next()) {
            System.out.println("!!! - Updating the play_evolutions table to cope with multiple modules - !!!");
            EvolutionQuery.alterForModuleSupport(dbName, connection);
        }
    }
   
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy