com.googlecode.flyway.commandline.Main.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.flyway.commandline.Main.java

Source

/**
 * Copyright (C) 2010-2012 the original author or authors.
 *
 * 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.googlecode.flyway.commandline;

import com.googlecode.flyway.core.Flyway;
import com.googlecode.flyway.core.exception.FlywayException;
import com.googlecode.flyway.core.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.Properties;

/**
 * Main class and central entry point of the Flyway command-line tool.
 */
public class Main {
    /**
     * Logger.
     */
    private static final Log LOG = LogFactory.getLog(Main.class);

    /**
     * Main method.
     *
     * @param args The command-line arguments.
     */
    public static void main(String[] args) {
        boolean debug = isDebug(args);

        try {
            printVersion();

            String operation = determineOperation(args);
            if (operation == null) {
                printUsage();
                return;
            }

            loadJdbcDriversAndJavaMigrations();

            Flyway flyway = new Flyway();

            Properties properties = new Properties();
            initializeDefaults(properties);
            loadConfigurationFile(properties, args);
            overrideConfiguration(properties, args);
            flyway.configure(properties);

            if ("clean".equals(operation)) {
                flyway.clean();
            } else if ("init".equals(operation)) {
                flyway.init();
            } else if ("migrate".equals(operation)) {
                flyway.migrate();
            } else if ("validate".equals(operation)) {
                flyway.validate();
            } else if ("status".equals(operation)) {
                MetaDataTableRowDumper.dumpMigration(flyway.status());
            } else if ("history".equals(operation)) {
                MetaDataTableRowDumper.dumpMigrations(flyway.history());
            } else {
                printUsage();
            }
        } catch (Exception e) {
            if (debug) {
                LOG.error("Unexpected error", e);
            } else {
                LOG.error(ClassUtils.getShortName(e.getClass()) + ": " + e.getMessage());
                outputFirstStackTraceElement(e);

                @SuppressWarnings({ "ThrowableResultOfMethodCallIgnored" })
                Throwable rootCause = ExceptionUtils.getRootCause(e);
                if (rootCause != null) {
                    LOG.error("Caused by " + rootCause.toString());
                    outputFirstStackTraceElement(rootCause);
                }
            }
            System.exit(1);
        }
    }

    /**
     * Checks whether we are in debug mode or not.
     *
     * @param args The command-line arguments.
     * @return {@code true} if we are in debug mode, {@code false} if not.
     */
    private static boolean isDebug(String[] args) {
        for (String arg : args) {
            if ("-X".equals(arg)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Output class, method and line number infos of first stack trace element
     * of the given {@link Throwable} using {@link Log#error(Object)}.
     *
     * @param t {@link Throwable} to log
     */
    private static void outputFirstStackTraceElement(Throwable t) {
        StackTraceElement firstStackTraceElement = t.getStackTrace()[0];
        LOG.error("Occured in " + firstStackTraceElement.getClassName() + "."
                + firstStackTraceElement.getMethodName() + "() at line " + firstStackTraceElement.getLineNumber());
    }

    /**
     * Initializes the properties with the default configuration for the command-line tool.
     *
     * @param properties The properties object to initialize.
     */
    private static void initializeDefaults(Properties properties) {
        properties.put("flyway.password", "");
        properties.put("flyway.locations", "/,db/migration");
    }

    /**
     * Prints the version number on the console.
     *
     * @throws IOException when the version could not be read.
     */
    private static void printVersion() throws IOException {
        String version = new ClassPathResource("version.txt").loadAsString("UTF-8");
        LOG.info("Flyway (Command-line Tool) v." + version);
        LOG.info("");
    }

    /**
     * Prints the usage instructions on the console.
     */
    private static void printUsage() {
        String extension;
        if (isWindows()) {
            extension = "cmd";
        } else {
            extension = "sh";
        }

        LOG.info("********");
        LOG.info("* Usage");
        LOG.info("********");
        LOG.info("");
        LOG.info("flyway." + extension + " [options] command");
        LOG.info("");
        LOG.info("By default, the configuration will be read from conf/flyway.properties.");
        LOG.info("Options passed from the command-line override the configuration.");
        LOG.info("");
        LOG.info("Commands");
        LOG.info("========");
        LOG.info("clean    : Drops all objects in the schema without dropping the schema itself");
        LOG.info("init     : Creates and initializes the metadata table in the schema");
        LOG.info("migrate  : Migrates the schema to the latest version");
        LOG.info("validate : Validates the applied migrations against the ones on the classpath");
        LOG.info("status   : Prints the current version of the schema");
        LOG.info("history  : Prints the full migration history of the schema");
        LOG.info("");
        LOG.info("Options (Format: -key=value)");
        LOG.info("=======");
        LOG.info("driver              : Fully qualified classname of the jdbc driver");
        LOG.info("url                 : Jdbc url to use to connect to the database");
        LOG.info("user                : User to use to connect to the database");
        LOG.info("password            : Password to use to connect to the database");
        LOG.info("schemas             : Comma-separated list of the schemas managed by Flyway");
        LOG.info("table               : Name of Flyway's metadata table");
        LOG.info("locations           : Classpath locations to scan recursively for migrations");
        LOG.info("sqlMigrationPrefix  : File name prefix for Sql migrations");
        LOG.info("sqlMigrationSuffix  : File name suffix for Sql migrations");
        LOG.info("encoding            : Encoding of Sql migrations");
        LOG.info("placeholders        : Placeholders to replace in Sql migrations");
        LOG.info("placeholderPrefix   : Prefix of every placeholder");
        LOG.info("placeholderSuffix   : Suffix of every placeholder");
        LOG.info("target              : Target version up to which Flyway should run migrations");
        LOG.info("validationMode      : Type of validation to be performed before migrating");
        LOG.info("validationErrorMode : Action to take when validation fails");
        LOG.info("initialVersion      : Initial version to put in the database");
        LOG.info("initialDescription  : Description of the initial version");
        LOG.info("disableInitCheck    : Don't check that a non-empty schema has been initialized");
        LOG.info("configFile          : Config file to use (default: conf/flyway.properties)");
        LOG.info("configFileEncoding  : Encoding of the config file (default: UTF-8)");
        LOG.info("");
        LOG.info("Example");
        LOG.info("=======");
        LOG.info("flyway." + extension + " -target=1.5 -placeholder.user=my_user history");
        LOG.info("");
        LOG.info("More info at http://code.google.com/p/flyway/wiki/CommandLine");
    }

    /**
     * Checks whether we are running on Windows or not.
     *
     * @return {@code true} if we are, {@code false} if not.
     */
    private static boolean isWindows() {
        return System.getProperty("os.name").startsWith("Windows");
    }

    /**
     * Loads all the jars contained in the jars folder. (For Jdbc drivers and Java Migrations)
     *
     * @throws IOException When the jars could not be loaded.
     */
    private static void loadJdbcDriversAndJavaMigrations() throws Exception {
        final String directoryForJdbcDriversAndJavaMigrations = getInstallationDir() + "/jars";
        File dir = new File(directoryForJdbcDriversAndJavaMigrations);
        File[] files = dir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".jar");
            }
        });

        // see javadoc of listFiles(): null if given path is not a real directory
        if (files == null) {
            LOG.warn("Directory for JDBC drivers and JavaMigrations not found: "
                    + directoryForJdbcDriversAndJavaMigrations);
            return;
        }

        for (File file : files) {
            addJarOrDirectoryToClasspath(file.getPath());
        }
    }

    /**
     * Adds a jar or a directory with this name to the classpath.
     *
     * @param name The name of the jar or directory to add.
     * @throws IOException when the jar or directory could not be found.
     */
    /* private -> for testing */
    static void addJarOrDirectoryToClasspath(String name) throws Exception {
        LOG.debug("Adding location to classpath: " + name);

        // Add the jar or dir to the classpath
        // Chain the current thread classloader
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { new File(name).toURI().toURL() },
                Thread.currentThread().getContextClassLoader());

        // Replace the thread classloader - assumes
        // you have permissions to do so
        Thread.currentThread().setContextClassLoader(urlClassLoader);
    }

    /**
     * Loads the configuration from the configuration file. If a configuration file is specified using the -configfile
     * argument it will be used, otherwise the default config file (conf/flyway.properties) will be loaded.
     *
     * @param properties The properties object to load to configuration into.
     * @param args       The command-line arguments passed in.
     * @throws FlywayException when the configuration file could not be loaded.
     */
    /* private -> for testing */
    static void loadConfigurationFile(Properties properties, String[] args) throws FlywayException {
        String configFile = determineConfigurationFile(args);

        if (configFile != null) {
            try {
                String encoding = determineConfigurationFileEncoding(args);
                Reader fileReader = new InputStreamReader(new FileInputStream(configFile), encoding);
                String propertiesData = FileCopyUtils.copyToString(fileReader);

                properties.putAll(PropertiesUtils.loadPropertiesFromString(propertiesData));
            } catch (IOException e) {
                throw new FlywayException("Unable to load config file: " + configFile, e);
            }
        }
    }

    /**
     * Determines the file to use for loading the configuration.
     *
     * @param args The command-line arguments passed in.
     * @return The path of the configuration file on disk.
     */
    private static String determineConfigurationFile(String[] args) {
        for (String arg : args) {
            if (isPropertyArgument(arg) && "configFile".equals(getArgumentProperty(arg))) {
                return getArgumentValue(arg);
            }
        }

        return getInstallationDir() + "/conf/flyway.properties";
    }

    /**
     * @return The installation directory of the Flyway Command-line tool.
     */
    private static String getInstallationDir() {
        String url = Main.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        try {
            String path = URLDecoder.decode(url, "UTF-8");
            return path.substring(0, path.lastIndexOf("/")) + "/..";
        } catch (UnsupportedEncodingException e) {
            //Can never happen.
            return null;
        }
    }

    /**
     * Determines the encoding to use for loading the configuration.
     *
     * @param args The command-line arguments passed in.
     * @return The encoding. (default: UTF-8)
     */
    private static String determineConfigurationFileEncoding(String[] args) {
        for (String arg : args) {
            if (isPropertyArgument(arg) && "configFileEncoding".equals(getArgumentProperty(arg))) {
                return getArgumentValue(arg);
            }
        }

        return "UTF-8";
    }

    /**
     * Overrides the configuration from the config file with the properties passed in directly from the command-line.
     *
     * @param properties The properties to override.
     * @param args       The command-line arguments that were passed in.
     */
    /* private -> for testing*/
    static void overrideConfiguration(Properties properties, String[] args) {
        for (String arg : args) {
            if (isPropertyArgument(arg)) {
                properties.put("flyway." + getArgumentProperty(arg), getArgumentValue(arg));
            }
        }
    }

    /**
     * Checks whether this command-line argument tries to set a property.
     *
     * @param arg The command-line argument to check.
     * @return {@code true} if it does, {@code false} if not.
     */
    /* private -> for testing*/
    static boolean isPropertyArgument(String arg) {
        return arg.startsWith("-") && arg.contains("=");
    }

    /**
     * Retrieves the property this command-line argument tries to assign.
     *
     * @param arg The command-line argument to check, typically in the form -key=value.
     * @return The property.
     */
    /* private -> for testing*/
    static String getArgumentProperty(String arg) {
        int index = arg.indexOf("=");

        return arg.substring(1, index);
    }

    /**
     * Retrieves the value this command-line argument tries to assign.
     *
     * @param arg The command-line argument to check, typically in the form -key=value.
     * @return The value or an empty string if no value is assigned.
     */
    /* private -> for testing*/
    static String getArgumentValue(String arg) {
        int index = arg.indexOf("=");

        if ((index < 0) || (index == arg.length())) {
            return "";
        }

        return arg.substring(index + 1);
    }

    /**
     * Determine the operation Flyway should execute.
     *
     * @param args The command-line arguments passed in.
     * @return The operation. {@code null} if it could not be determined.
     */
    private static String determineOperation(String[] args) {
        for (String arg : args) {
            if (!arg.startsWith("-")) {
                return arg;
            }
        }

        return null;
    }
}