package liquibase;

import liquibase.change.CheckSum;
import liquibase.change.core.RawSQLChange;
import liquibase.changelog.*;
import liquibase.changelog.filter.*;
import liquibase.changelog.visitor.*;
import liquibase.command.CommandExecutionException;
import liquibase.command.CommandFactory;
import liquibase.command.core.DropAllCommand;
import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.DatabaseFactory;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.diff.DiffGeneratorFactory;
import liquibase.diff.DiffResult;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.output.changelog.DiffToChangeLog;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.LockException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.executor.LoggingExecutor;
import liquibase.lockservice.DatabaseChangeLogLock;
import liquibase.lockservice.LockService;
import liquibase.lockservice.LockServiceFactory;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.resource.InputStreamList;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.ChangeLogSerializer;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.SnapshotControl;
import liquibase.snapshot.SnapshotGeneratorFactory;
import liquibase.statement.core.RawSqlStatement;
import liquibase.statement.core.UpdateStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Catalog;
import liquibase.util.LiquibaseUtil;
import liquibase.util.StreamUtil;
import liquibase.util.StringUtil;

import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.text.DateFormat;
import java.util.*;

import static java.util.ResourceBundle.getBundle;

/**
 * Primary facade class for interacting with Liquibase.
 * The built in command line, Ant, Maven and other ways of running Liquibase are wrappers around methods in this class.
 */
public class Liquibase {

    private static final Logger LOG = Scope.getCurrentScope().getLog(Liquibase.class);
    protected static final int CHANGESET_ID_NUM_PARTS = 3;
    protected static final int CHANGESET_ID_AUTHOR_PART = 2;
    protected static final int CHANGESET_ID_CHANGESET_PART = 1;
    protected static final int CHANGESET_ID_CHANGELOG_PART = 0;
    private static ResourceBundle coreBundle = getBundle("liquibase/i18n/liquibase-core");
    protected static final String MSG_COULD_NOT_RELEASE_LOCK = coreBundle.getString("could.not.release.lock");

    protected Database database;
    private DatabaseChangeLog databaseChangeLog;
    private String changeLogFile;
    private ResourceAccessor resourceAccessor;
    private ChangeLogParameters changeLogParameters;
    private ChangeExecListener changeExecListener;
    private ChangeLogSyncListener changeLogSyncListener;

    private boolean ignoreClasspathPrefix = true;

    /**
     * Creates a Liquibase instance for a given DatabaseConnection. The Database instance used will be found with {@link DatabaseFactory#findCorrectDatabaseImplementation(liquibase.database.DatabaseConnection)}
     *
     * @see DatabaseConnection
     * @see Database
     * @see #Liquibase(String, liquibase.resource.ResourceAccessor, liquibase.database.Database)
     * @see ResourceAccessor
     */
    public Liquibase(String changeLogFile, ResourceAccessor resourceAccessor, DatabaseConnection conn)
        throws LiquibaseException {
        this(changeLogFile, resourceAccessor, DatabaseFactory.getInstance().findCorrectDatabaseImplementation(conn));
    }

    /**
     * Creates a Liquibase instance. The changeLogFile parameter must be a path that can be resolved by the passed
     * ResourceAccessor. If windows style path separators are used for the changeLogFile, they will be standardized to
     * unix style for better cross-system compatibility.
     *
     * @see DatabaseConnection
     * @see Database
     * @see ResourceAccessor
     */
    public Liquibase(String changeLogFile, ResourceAccessor resourceAccessor, Database database) {
        if (changeLogFile != null) {
            // Convert to STANDARD / if using absolute path on windows:
            this.changeLogFile = changeLogFile.replace('\\', '/');
        }

        this.resourceAccessor = resourceAccessor;
        this.changeLogParameters = new ChangeLogParameters(database);
        this.database = database;
    }

    public Liquibase(DatabaseChangeLog changeLog, ResourceAccessor resourceAccessor, Database database) {
        this.databaseChangeLog = changeLog;

        if (changeLog != null) {
            this.changeLogFile = changeLog.getPhysicalFilePath();
        }
        if (this.changeLogFile != null) {
            // Convert to STANDARD "/" if using an absolute path on Windows:
            changeLogFile = changeLogFile.replace('\\', '/');
        }
        this.resourceAccessor = resourceAccessor;
        this.database = database;
        this.changeLogParameters = new ChangeLogParameters(database);
    }

    /**
     * Return the change log file used by this Liquibase instance.
     */
    public String getChangeLogFile() {
        return changeLogFile;
    }

    /**
     * Return the log used by this Liquibase instance.
     */
    public Logger getLog() {
        return LOG;
    }

    /**
     * Returns the ChangeLogParameters container used by this Liquibase instance.
     */
    public ChangeLogParameters getChangeLogParameters() {
        return changeLogParameters;
    }

    /**
     * Returns the Database used by this Liquibase instance.
     */
    public Database getDatabase() {
        return database;
    }

    /**
     * Return ResourceAccessor used by this Liquibase instance.
     */
    public ResourceAccessor getResourceAccessor() {
        return resourceAccessor;
    }

    /**
     * Convience method for {@link #update(Contexts)} that constructs the Context object from the passed string.
     */
    public void update(String contexts) throws LiquibaseException {
        this.update(new Contexts(contexts));
    }
    /**
     * Executes Liquibase "update" logic which ensures that the configured {@link Database} is up to date according to
     * the configured changelog file. To run in "no context mode", pass a null or empty context object.
     */
    public void update(Contexts contexts) throws LiquibaseException {
        update(contexts, new LabelExpression());
    }

    public void update(Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        update(contexts, labelExpression, true);
    }
    public void update(Contexts contexts, LabelExpression labelExpression, boolean checkLiquibaseTables)
        throws LiquibaseException {
        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();

            if (checkLiquibaseTables) {
                checkLiquibaseTables(true, changeLog, contexts, labelExpression);
            }

            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).generateDeploymentId();

            changeLog.validate(database, contexts, labelExpression);

            ChangeLogIterator changeLogIterator = getStandardChangelogIterator(contexts, labelExpression, changeLog);

            changeLogIterator.run(createUpdateVisitor(), new RuntimeEnvironment(database, contexts, labelExpression));
        } finally {
            database.setObjectQuotingStrategy(ObjectQuotingStrategy.LEGACY);
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
            resetServices();
        }
    }

    public DatabaseChangeLog getDatabaseChangeLog() throws LiquibaseException {
        if (databaseChangeLog == null) {
            ChangeLogParser parser = ChangeLogParserFactory.getInstance().getParser(changeLogFile, resourceAccessor);
            databaseChangeLog = parser.parse(changeLogFile, changeLogParameters, resourceAccessor);
        }

        return databaseChangeLog;
    }

    protected UpdateVisitor createUpdateVisitor() {
        return new UpdateVisitor(database, changeExecListener);
    }

    protected RollbackVisitor createRollbackVisitor() {
        return new RollbackVisitor(database, changeExecListener);
    }

    protected ChangeLogIterator getStandardChangelogIterator(Contexts contexts, LabelExpression labelExpression,
                                                             DatabaseChangeLog changeLog) throws DatabaseException {
        return new ChangeLogIterator(changeLog,
                new ShouldRunChangeSetFilter(database, ignoreClasspathPrefix),
                new ContextChangeSetFilter(contexts),
                new LabelChangeSetFilter(labelExpression),
                new DbmsChangeSetFilter(database),
                new IgnoreChangeSetFilter());
    }

    public void update(String contexts, Writer output) throws LiquibaseException {
        this.update(new Contexts(contexts), output);
    }

    public void update(Contexts contexts, Writer output) throws LiquibaseException {
        update(contexts, new LabelExpression(), output);
    }

    public void update(Contexts contexts, LabelExpression labelExpression, Writer output) throws LiquibaseException {
        update(contexts, labelExpression, output, true);
    }

    public void update(Contexts contexts, LabelExpression labelExpression, Writer output, boolean checkLiquibaseTables)
            throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        LoggingExecutor loggingExecutor = new LoggingExecutor(
            ExecutorService.getInstance().getExecutor(database), output, database
        );
        ExecutorService.getInstance().setExecutor(database, loggingExecutor);

        outputHeader("Update Database Script");

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {

            update(contexts, labelExpression, checkLiquibaseTables);

            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

        ExecutorService.getInstance().setExecutor(database, oldTemplate);
    }

    public void update(int changesToApply, String contexts) throws LiquibaseException {
        update(changesToApply, new Contexts(contexts), new LabelExpression());
    }

    public void update(int changesToApply, Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {

            DatabaseChangeLog changeLog = getDatabaseChangeLog();

            checkLiquibaseTables(true, changeLog, contexts, labelExpression);
            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).generateDeploymentId();

            changeLog.validate(database, contexts, labelExpression);

            ChangeLogIterator logIterator = new ChangeLogIterator(changeLog,
                    new ShouldRunChangeSetFilter(database, ignoreClasspathPrefix),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new DbmsChangeSetFilter(database),
                    new IgnoreChangeSetFilter(),
                    new CountChangeSetFilter(changesToApply));

            logIterator.run(createUpdateVisitor(), new RuntimeEnvironment(database, contexts, labelExpression));
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
            resetServices();
        }
    }

    public void update(String tag, String contexts) throws LiquibaseException {
        update(tag, new Contexts(contexts), new LabelExpression());
    }

    public void update(String tag, Contexts contexts) throws LiquibaseException {
        update(tag, contexts, new LabelExpression());
    }

    public void update(String tag, Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        if (tag == null) {
            update(contexts, labelExpression);
            return;
        }
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {

            DatabaseChangeLog changeLog = getDatabaseChangeLog();

            checkLiquibaseTables(true, changeLog, contexts, labelExpression);
            changeLog.validate(database, contexts, labelExpression);

            List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();
            ChangeLogIterator logIterator = new ChangeLogIterator(changeLog,
                    new ShouldRunChangeSetFilter(database, ignoreClasspathPrefix),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new DbmsChangeSetFilter(database),
                    new IgnoreChangeSetFilter(),
                    new UpToTagChangeSetFilter(tag, ranChangeSetList));

            logIterator.run(createUpdateVisitor(), new RuntimeEnvironment(database, contexts, labelExpression));
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
            resetServices();
        }
    }

    public void update(int changesToApply, String contexts, Writer output) throws LiquibaseException {
        this.update(changesToApply, new Contexts(contexts), new LabelExpression(), output);
    }

    public void update(int changesToApply, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        LoggingExecutor loggingExecutor = new LoggingExecutor(
            ExecutorService.getInstance().getExecutor(database), output, database
        );
        ExecutorService.getInstance().setExecutor(database, loggingExecutor);

        outputHeader("Update " + changesToApply + " Change Sets Database Script");

        update(changesToApply, contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

        resetServices();
        ExecutorService.getInstance().setExecutor(database, oldTemplate);
    }

    public void update(String tag, String contexts, Writer output) throws LiquibaseException {
        update(tag, new Contexts(contexts), new LabelExpression(), output);
    }

    public void update(String tag, Contexts contexts, Writer output) throws LiquibaseException {
        update(tag, contexts, new LabelExpression(), output);
    }

    public void update(String tag, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        if (tag == null) {
            update(contexts, labelExpression, output);
            return;
        }
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        LoggingExecutor loggingExecutor = new LoggingExecutor(
            ExecutorService.getInstance().getExecutor(database), output, database
        );
        ExecutorService.getInstance().setExecutor(database, loggingExecutor);

        outputHeader("Update to '" + tag + "' Database Script");

        update(tag, contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

        resetServices();
        ExecutorService.getInstance().setExecutor(database, oldTemplate);
    }

    private void outputHeader(String message) throws DatabaseException {
        Executor executor = ExecutorService.getInstance().getExecutor(database);
        executor.comment("*********************************************************************");
        executor.comment(message);
        executor.comment("*********************************************************************");
        executor.comment("Change Log: " + changeLogFile);
        executor.comment("Ran at: " +
            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date())
        );
        DatabaseConnection connection = getDatabase().getConnection();
        if (connection != null) {
            executor.comment("Against: " + connection.getConnectionUserName() + "@" + connection.getURL());
        }
        executor.comment("Liquibase version: " + LiquibaseUtil.getBuildVersion());
        executor.comment("*********************************************************************" +
            StreamUtil.getLineSeparator()
        );

        if (database instanceof OracleDatabase) {
            executor.execute(new RawSqlStatement("SET DEFINE OFF;"));
        }
        if ((database instanceof MSSQLDatabase) && (database.getDefaultCatalogName() != null)) {
            executor.execute(new RawSqlStatement("USE " +
                database.escapeObjectName(database.getDefaultCatalogName(), Catalog.class) + ";")
            );
        }
    }

    public void rollback(int changesToRollback, String contexts, Writer output) throws LiquibaseException {
        rollback(changesToRollback, null, contexts, output);
    }

    public void rollback(int changesToRollback, Contexts contexts, Writer output) throws LiquibaseException {
        rollback(changesToRollback, null, contexts, output);
    }

    public void rollback(int changesToRollback, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        rollback(changesToRollback, null, contexts, labelExpression, output);
    }

    public void rollback(int changesToRollback, String rollbackScript, String contexts, Writer output)
        throws LiquibaseException {
        rollback(changesToRollback, rollbackScript, new Contexts(contexts), output);
    }

    public void rollback(int changesToRollback, String rollbackScript, Contexts contexts, Writer output)
        throws LiquibaseException {
        rollback(changesToRollback, rollbackScript, contexts, new LabelExpression(), output);
    }
    public void rollback(int changesToRollback, String rollbackScript, Contexts contexts,
                         LabelExpression labelExpression, Writer output) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        ExecutorService.getInstance().setExecutor(database,
            new LoggingExecutor(ExecutorService.getInstance().getExecutor(database), output, database)
        );

        outputHeader("Rollback " + changesToRollback + " Change(s) Script");

        rollback(changesToRollback, rollbackScript, contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }
        ExecutorService.getInstance().setExecutor(database, oldTemplate);
        resetServices();
    }

    public void rollback(int changesToRollback, String contexts) throws LiquibaseException {
        rollback(changesToRollback, null, contexts);
    }

    public void rollback(int changesToRollback, Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        rollback(changesToRollback, null, contexts, labelExpression);
    }

    public void rollback(int changesToRollback, String rollbackScript, String contexts) throws LiquibaseException {
        rollback(changesToRollback, rollbackScript, new Contexts(contexts), new LabelExpression());
    }

    public void rollback(int changesToRollback, String rollbackScript, Contexts contexts,
                         LabelExpression labelExpression) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            checkLiquibaseTables(false, changeLog, contexts, labelExpression);

            changeLog.validate(database, contexts, labelExpression);
            changeLog.setIgnoreClasspathPrefix(ignoreClasspathPrefix);

            ChangeLogIterator logIterator = new ChangeLogIterator(database.getRanChangeSetList(), changeLog,
                    new AlreadyRanChangeSetFilter(database.getRanChangeSetList(), ignoreClasspathPrefix),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new DbmsChangeSetFilter(database),
                    new IgnoreChangeSetFilter(),
                    new CountChangeSetFilter(changesToRollback));

            if (rollbackScript == null) {
                logIterator.run(createRollbackVisitor(), new RuntimeEnvironment(database, contexts, labelExpression));
            }
            else {
                executeRollbackScript(rollbackScript, contexts, labelExpression);
                removeRunStatus(logIterator, contexts, labelExpression);
            }
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, "Error releasing lock", e);
            }
            resetServices();
        }
    }

    protected void removeRunStatus(ChangeLogIterator logIterator, Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        logIterator.run(new ChangeSetVisitor() {
            @Override
            public Direction getDirection() {
                return Direction.REVERSE;
            }

            @Override
            public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database,
                              Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
                database.removeRanStatus(changeSet);
                database.commit();
            }
        }, new RuntimeEnvironment(database, contexts, labelExpression));
    }

    protected void executeRollbackScript(String rollbackScript, Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        final Executor executor = ExecutorService.getInstance().getExecutor(database);
        String rollbackScriptContents;
        try {
            InputStreamList streams = resourceAccessor.openStreams(null, rollbackScript);
            if ((streams == null) || streams.isEmpty()) {
                throw new LiquibaseException("Cannot find rollbackScript "+rollbackScript);
            } else if (streams.size() > 1) {
                throw new LiquibaseException("Found multiple rollbackScripts named "+rollbackScript);
            }
            rollbackScriptContents = StreamUtil.readStreamAsString(streams.iterator().next());
        } catch (IOException e) {
            throw new LiquibaseException("Error reading rollbackScript "+executor+": "+e.getMessage());
        }

        RawSQLChange rollbackChange = new RawSQLChange(rollbackScriptContents);
        rollbackChange.setSplitStatements(true);
        rollbackChange.setStripComments(true);

        try {
            executor.execute(rollbackChange);
        } catch (DatabaseException e) {
            DatabaseException ex = new DatabaseException(
                "Error executing rollback script. ChangeSets will still be marked as rolled back: " + e.getMessage(),
                e
            );
            Scope.getCurrentScope().getLog(getClass()).severe(LogType.LOG, ex.getMessage());
            LOG.severe(LogType.LOG, "Error executing rollback script", ex);
            if (changeExecListener != null) {
                changeExecListener.runFailed(null, databaseChangeLog, database, ex);
            }
        }
        database.commit();
    }

    public void rollback(String tagToRollBackTo, String contexts, Writer output) throws LiquibaseException {
        rollback(tagToRollBackTo, null, contexts, output);
    }

    public void rollback(String tagToRollBackTo, Contexts contexts, Writer output) throws LiquibaseException {
        rollback(tagToRollBackTo, null, contexts, output);
    }

    public void rollback(String tagToRollBackTo, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        rollback(tagToRollBackTo, null, contexts, labelExpression, output);
    }

    public void rollback(String tagToRollBackTo, String rollbackScript, String contexts, Writer output)
        throws LiquibaseException {
        rollback(tagToRollBackTo, rollbackScript, new Contexts(contexts), output);
    }

    public void rollback(String tagToRollBackTo, String rollbackScript, Contexts contexts, Writer output)
        throws LiquibaseException {
        rollback(tagToRollBackTo, rollbackScript, contexts, new LabelExpression(), output);
    }

    public void rollback(String tagToRollBackTo, String rollbackScript, Contexts contexts,
                         LabelExpression labelExpression, Writer output) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        /* We have no other choice than to save the current Executor here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        ExecutorService.getInstance().setExecutor(database, new LoggingExecutor(
            ExecutorService.getInstance().getExecutor(database), output, database)
        );

        outputHeader("Rollback to '" + tagToRollBackTo + "' Script");

        rollback(tagToRollBackTo, contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }
        ExecutorService.getInstance().setExecutor(database, oldTemplate);
        resetServices();
    }

    public void rollback(String tagToRollBackTo, String contexts) throws LiquibaseException {
        rollback(tagToRollBackTo, null, contexts);
    }

    public void rollback(String tagToRollBackTo, Contexts contexts) throws LiquibaseException {
        rollback(tagToRollBackTo, null, contexts);
    }

    public void rollback(String tagToRollBackTo, Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        rollback(tagToRollBackTo, null, contexts, labelExpression);
    }

    public void rollback(String tagToRollBackTo, String rollbackScript, String contexts) throws LiquibaseException {
        rollback(tagToRollBackTo, rollbackScript, new Contexts(contexts));
    }

    public void rollback(String tagToRollBackTo, String rollbackScript, Contexts contexts) throws LiquibaseException {
        rollback(tagToRollBackTo, rollbackScript, contexts, new LabelExpression());
    }
    public void rollback(String tagToRollBackTo, String rollbackScript, Contexts contexts,
                         LabelExpression labelExpression) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {

            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            checkLiquibaseTables(false, changeLog, contexts, labelExpression);

            changeLog.validate(database, contexts, labelExpression);
            changeLog.setIgnoreClasspathPrefix(ignoreClasspathPrefix);

            List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();
            ChangeLogIterator logIterator = new ChangeLogIterator(ranChangeSetList, changeLog,
                    new AfterTagChangeSetFilter(tagToRollBackTo, ranChangeSetList),
                    new AlreadyRanChangeSetFilter(ranChangeSetList, ignoreClasspathPrefix),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new IgnoreChangeSetFilter(),
                    new DbmsChangeSetFilter(database));

            if (rollbackScript == null) {
                logIterator.run(createRollbackVisitor(),
                    new RuntimeEnvironment(database, contexts, labelExpression));
            } else {
                executeRollbackScript(rollbackScript, contexts, labelExpression);
                removeRunStatus(logIterator, contexts, labelExpression);
            }
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
        }
        resetServices();
    }

    public void rollback(Date dateToRollBackTo, String contexts, Writer output) throws LiquibaseException {
        rollback(dateToRollBackTo, null, contexts, output);
    }

    public void rollback(Date dateToRollBackTo, String rollbackScript, String contexts, Writer output)
        throws LiquibaseException {
        rollback(dateToRollBackTo, new Contexts(contexts), new LabelExpression(), output);
    }
    public void rollback(Date dateToRollBackTo, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        rollback(dateToRollBackTo, null, contexts, labelExpression, output);
    }

    public void rollback(Date dateToRollBackTo, String rollbackScript, Contexts contexts,
                         LabelExpression labelExpression, Writer output) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        ExecutorService.getInstance().setExecutor(database,
            new LoggingExecutor(ExecutorService.getInstance().getExecutor(database), output, database));

        outputHeader("Rollback to " + dateToRollBackTo + " Script");

        rollback(dateToRollBackTo, contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }
        ExecutorService.getInstance().setExecutor(database, oldTemplate);
        resetServices();
    }

    public void rollback(Date dateToRollBackTo, String contexts) throws LiquibaseException {
        rollback(dateToRollBackTo, null, contexts);
    }

    public void rollback(Date dateToRollBackTo, Contexts contexts,  LabelExpression labelExpression)
        throws LiquibaseException {
        rollback(dateToRollBackTo, null, contexts, labelExpression);
    }

    public void rollback(Date dateToRollBackTo, String rollbackScript, String contexts) throws LiquibaseException {
        rollback(dateToRollBackTo, new Contexts(contexts), new LabelExpression());
    }

    public void rollback(Date dateToRollBackTo, String rollbackScript, Contexts contexts,
                         LabelExpression labelExpression) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            checkLiquibaseTables(false, changeLog, contexts, labelExpression);
            changeLog.validate(database, contexts, labelExpression);
            changeLog.setIgnoreClasspathPrefix(ignoreClasspathPrefix);

            List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();
            ChangeLogIterator logIterator = new ChangeLogIterator(ranChangeSetList, changeLog,
                    new ExecutedAfterChangeSetFilter(dateToRollBackTo, ranChangeSetList),
                    new AlreadyRanChangeSetFilter(ranChangeSetList, ignoreClasspathPrefix),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new IgnoreChangeSetFilter(),
                    new DbmsChangeSetFilter(database));

            if (rollbackScript == null) {
                logIterator.run(createRollbackVisitor(),
                    new RuntimeEnvironment(database, contexts, labelExpression));
            } else {
                executeRollbackScript(rollbackScript, contexts, labelExpression);
                removeRunStatus(logIterator, contexts, labelExpression);
            }
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
        }
        resetServices();
    }

    public void changeLogSync(String contexts, Writer output) throws LiquibaseException {
        changeLogSync(new Contexts(contexts), new LabelExpression(), output);
    }

    public void changeLogSync(Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LoggingExecutor outputTemplate = new LoggingExecutor(
            ExecutorService.getInstance().getExecutor(database), output, database
        );

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        ExecutorService.getInstance().setExecutor(database, outputTemplate);

        outputHeader("SQL to add all changesets to database history table");

        changeLogSync(contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

        ExecutorService.getInstance().setExecutor(database, oldTemplate);
        resetServices();
    }

    public void changeLogSync(String contexts) throws LiquibaseException {
        changeLogSync(new Contexts(contexts), new LabelExpression());
    }

    /**
     * @deprecated use version with LabelExpression
     */
    @Deprecated
    public void changeLogSync(Contexts contexts) throws LiquibaseException {
        changeLogSync(contexts, new LabelExpression());
    }

    public void changeLogSync(Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            checkLiquibaseTables(true, changeLog, contexts, labelExpression);
            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).generateDeploymentId();

            changeLog.validate(database, contexts, labelExpression);

            ChangeLogIterator logIterator = new ChangeLogIterator(changeLog,
                    new NotRanChangeSetFilter(database.getRanChangeSetList()),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new IgnoreChangeSetFilter(),
                    new DbmsChangeSetFilter(database));

            logIterator.run(new ChangeLogSyncVisitor(database, changeLogSyncListener),
                new RuntimeEnvironment(database, contexts, labelExpression)
            );
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
            resetServices();
        }
    }

    public void markNextChangeSetRan(String contexts, Writer output) throws LiquibaseException {
        markNextChangeSetRan(new Contexts(contexts), new LabelExpression(), output);
    }

    public void markNextChangeSetRan(Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LoggingExecutor outputTemplate = new LoggingExecutor(
            ExecutorService.getInstance().getExecutor(database), output, database
        );

        /* We have no other choice than to save the current Executer here. */
        @SuppressWarnings("squid:S1941")
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        ExecutorService.getInstance().setExecutor(database, outputTemplate);

        outputHeader("SQL to add all changesets to database history table");

        markNextChangeSetRan(contexts, labelExpression);

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

        ExecutorService.getInstance().setExecutor(database, oldTemplate);
        resetServices();
    }

    public void markNextChangeSetRan(String contexts) throws LiquibaseException {
        markNextChangeSetRan(new Contexts(contexts), new LabelExpression());
    }

    public void markNextChangeSetRan(Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).generateDeploymentId();

            checkLiquibaseTables(false, changeLog, contexts, labelExpression);
            changeLog.validate(database, contexts, labelExpression);

            ChangeLogIterator logIterator = new ChangeLogIterator(changeLog,
                    new NotRanChangeSetFilter(database.getRanChangeSetList()),
                    new ContextChangeSetFilter(contexts),
                    new LabelChangeSetFilter(labelExpression),
                    new DbmsChangeSetFilter(database),
                    new IgnoreChangeSetFilter(),
                    new CountChangeSetFilter(1));

            logIterator.run(new ChangeLogSyncVisitor(database),
                new RuntimeEnvironment(database, contexts, labelExpression)
            );
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
            resetServices();
        }
    }

    public void futureRollbackSQL(String contexts, Writer output) throws LiquibaseException {
        futureRollbackSQL(null, contexts, output, true);
    }

    public void futureRollbackSQL(Writer output) throws LiquibaseException {
        futureRollbackSQL(null, null, new Contexts(), new LabelExpression(), output);
    }

    public void futureRollbackSQL(String contexts, Writer output, boolean checkLiquibaseTables)
           throws LiquibaseException {
        futureRollbackSQL(null, contexts, output, checkLiquibaseTables);
    }

    public void futureRollbackSQL(Integer count, String contexts, Writer output) throws LiquibaseException {
        futureRollbackSQL(count, new Contexts(contexts), new LabelExpression(), output, true);
    }

    public void futureRollbackSQL(Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        futureRollbackSQL(null, null, contexts, labelExpression, output);
    }

    public void futureRollbackSQL(Integer count, String contexts, Writer output, boolean checkLiquibaseTables)
           throws LiquibaseException {
        futureRollbackSQL(count, new Contexts(contexts), new LabelExpression(), output, checkLiquibaseTables);
    }

    public void futureRollbackSQL(Integer count, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        futureRollbackSQL(count, contexts, labelExpression, output, true);
    }

    public void futureRollbackSQL(Integer count, Contexts contexts, LabelExpression labelExpression, Writer output,
                                  boolean checkLiquibaseTables) throws LiquibaseException {
        futureRollbackSQL(count, null, contexts, labelExpression, output);
    }

    public void futureRollbackSQL(String tag, Contexts contexts, LabelExpression labelExpression, Writer output)
        throws LiquibaseException {
        futureRollbackSQL(null, tag, contexts, labelExpression, output);
    }

    protected void futureRollbackSQL(Integer count, String tag, Contexts contexts, LabelExpression labelExpression,
                                     Writer output) throws LiquibaseException {
        futureRollbackSQL(count, tag, contexts, labelExpression, output, true);
    }

    protected void futureRollbackSQL(Integer count, String tag, Contexts contexts, LabelExpression labelExpression,
                                     Writer output, boolean checkLiquibaseTables) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        LoggingExecutor outputTemplate = new LoggingExecutor(ExecutorService.getInstance().getExecutor(database),
            output, database);
        Executor oldTemplate = ExecutorService.getInstance().getExecutor(database);
        ExecutorService.getInstance().setExecutor(database, outputTemplate);

        outputHeader("SQL to roll back currently unexecuted changes");

        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            if (checkLiquibaseTables) {
                checkLiquibaseTables(false, changeLog, contexts, labelExpression);
            }
            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).generateDeploymentId();

            changeLog.validate(database, contexts, labelExpression);

            ChangeLogIterator logIterator;
            if ((count == null) && (tag == null)) {
                logIterator = new ChangeLogIterator(changeLog,
                        new NotRanChangeSetFilter(database.getRanChangeSetList()),
                        new ContextChangeSetFilter(contexts),
                        new LabelChangeSetFilter(labelExpression),
                        new IgnoreChangeSetFilter(),
                        new DbmsChangeSetFilter(database));
            } else if (count != null) {
                ChangeLogIterator forwardIterator = new ChangeLogIterator(changeLog,
                        new NotRanChangeSetFilter(database.getRanChangeSetList()),
                        new ContextChangeSetFilter(contexts),
                        new LabelChangeSetFilter(labelExpression),
                        new DbmsChangeSetFilter(database),
                        new IgnoreChangeSetFilter(),
                        new CountChangeSetFilter(count));
                final ListVisitor listVisitor = new ListVisitor();
                forwardIterator.run(listVisitor, new RuntimeEnvironment(database, contexts, labelExpression));

                logIterator = new ChangeLogIterator(changeLog,
                        new NotRanChangeSetFilter(database.getRanChangeSetList()),
                        new ContextChangeSetFilter(contexts),
                        new LabelChangeSetFilter(labelExpression),
                        new DbmsChangeSetFilter(database),
                        new IgnoreChangeSetFilter(),
                        new ChangeSetFilter() {
                            @Override
                            public ChangeSetFilterResult accepts(ChangeSet changeSet) {
                                return new ChangeSetFilterResult(
                                    listVisitor.getSeenChangeSets().contains(changeSet), null, null
                                );
                            }
                        });
            } else {
                List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();
                ChangeLogIterator forwardIterator = new ChangeLogIterator(changeLog,
                        new NotRanChangeSetFilter(ranChangeSetList),
                        new ContextChangeSetFilter(contexts),
                        new LabelChangeSetFilter(labelExpression),
                        new DbmsChangeSetFilter(database),
                        new IgnoreChangeSetFilter(),
                        new UpToTagChangeSetFilter(tag, ranChangeSetList));
                final ListVisitor listVisitor = new ListVisitor();
                forwardIterator.run(listVisitor, new RuntimeEnvironment(database, contexts, labelExpression));

                logIterator = new ChangeLogIterator(changeLog,
                        new NotRanChangeSetFilter(ranChangeSetList),
                        new ContextChangeSetFilter(contexts),
                        new LabelChangeSetFilter(labelExpression),
                        new DbmsChangeSetFilter(database),
                        new IgnoreChangeSetFilter(),
                        new ChangeSetFilter() {
                            @Override
                            public ChangeSetFilterResult accepts(ChangeSet changeSet) {
                                return new ChangeSetFilterResult(
                                    listVisitor.getSeenChangeSets().contains(changeSet), null, null
                                );
                            }
                        });
            }

            logIterator.run(createRollbackVisitor(),
                new RuntimeEnvironment(database, contexts, labelExpression)
            );
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
            ExecutorService.getInstance().setExecutor(database, oldTemplate);
            resetServices();
        }

        try {
            output.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

    }

    protected void resetServices() {
        LockServiceFactory.getInstance().resetAll();
        ChangeLogHistoryServiceFactory.getInstance().resetAll();
        ExecutorService.getInstance().reset();
    }

    /**
     * Drops all database objects in the default schema.
     */
    public final void dropAll() throws DatabaseException {
        dropAll(new CatalogAndSchema(getDatabase().getDefaultCatalogName(), getDatabase().getDefaultSchemaName()));
    }

    /**
     * Drops all database objects in the passed schema(s).
     */
    public final void dropAll(CatalogAndSchema... schemas) throws DatabaseException {
        if ((schemas == null) || (schemas.length == 0)) {
            schemas = new CatalogAndSchema[] {
                new CatalogAndSchema(getDatabase().getDefaultCatalogName(), getDatabase().getDefaultSchemaName())
            };
        }

        DropAllCommand dropAll = (DropAllCommand) CommandFactory.getInstance().getCommand("dropAll");
        dropAll.setDatabase(this.getDatabase());
        dropAll.setSchemas(schemas);

        try {
            dropAll.execute();
        } catch (CommandExecutionException e) {
            throw new DatabaseException(e);
        }
    }

    /**
     * 'Tags' the database for future rollback
     */
    public void tag(String tagString) throws LiquibaseException {
        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).generateDeploymentId();

            checkLiquibaseTables(false, null, new Contexts(),
                new LabelExpression());
            getDatabase().tag(tagString);
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
        }
    }

    public boolean tagExists(String tagString) throws LiquibaseException {
        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            checkLiquibaseTables(false, null, new Contexts(),
                new LabelExpression());
            return getDatabase().doesTagExist(tagString);
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
        }
    }

    public void updateTestingRollback(String contexts) throws LiquibaseException {
        updateTestingRollback(new Contexts(contexts), new LabelExpression());
    }

    public void updateTestingRollback(Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        updateTestingRollback(null, contexts, labelExpression);

    }
    public void updateTestingRollback(String tag, Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        Date baseDate = new Date();
        update(tag, contexts, labelExpression);
        rollback(baseDate, null, contexts, labelExpression);
        update(tag, contexts, labelExpression);
    }

    public void checkLiquibaseTables(boolean updateExistingNullChecksums, DatabaseChangeLog databaseChangeLog,
                                     Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        ChangeLogHistoryService changeLogHistoryService =
            ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(getDatabase());
        changeLogHistoryService.init();
        if (updateExistingNullChecksums) {
            changeLogHistoryService.upgradeChecksums(databaseChangeLog, contexts, labelExpression);
        }
        LockServiceFactory.getInstance().getLockService(getDatabase()).init();
    }

    /**
     * Returns true if it is "save" to migrate the database.
     * Currently, "safe" is defined as running in an output-sql mode or against a database on localhost.
     * It is fine to run Liquibase against a "non-safe" database, the method is mainly used to determine if the user
     * should be prompted before continuing.
     */
    public boolean isSafeToRunUpdate() throws DatabaseException {
        return getDatabase().isSafeToRunUpdate();
    }

    /**
     * Display change log lock information.
     */
    public DatabaseChangeLogLock[] listLocks() throws LiquibaseException {
        checkLiquibaseTables(false, null, new Contexts(), new LabelExpression());

        return LockServiceFactory.getInstance().getLockService(database).listLocks();
    }

    public void reportLocks(PrintStream out) throws LiquibaseException {
        DatabaseChangeLogLock[] locks = listLocks();
        out.println("Database change log locks for " + getDatabase().getConnection().getConnectionUserName()
            + "@" + getDatabase().getConnection().getURL());
        if (locks.length == 0) {
            out.println(" - No locks");
        }
        for (DatabaseChangeLogLock lock : locks) {
            out.println(" - " + lock.getLockedBy() + " at " +
                DateFormat.getDateTimeInstance().format(lock.getLockGranted()));
        }

    }

    public void forceReleaseLocks() throws LiquibaseException {
        checkLiquibaseTables(false, null, new Contexts(), new LabelExpression());

        LockServiceFactory.getInstance().getLockService(database).forceReleaseLock();
    }

    /**
     * @deprecated use version with LabelExpression
     */
    @Deprecated
    public List<ChangeSet> listUnrunChangeSets(Contexts contexts) throws LiquibaseException {
        return listUnrunChangeSets(contexts, new LabelExpression());
    }

    public List<ChangeSet> listUnrunChangeSets(Contexts contexts, LabelExpression labels) throws LiquibaseException {
        return listUnrunChangeSets(contexts, labels, true);
    }

    public List<ChangeSet> listUnrunChangeSets(Contexts contexts, LabelExpression labels, boolean checkLiquibaseTables) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labels);

        DatabaseChangeLog changeLog = getDatabaseChangeLog();

        if (checkLiquibaseTables) {
            checkLiquibaseTables(true, changeLog, contexts, labels);
        }

        changeLog.validate(database, contexts, labels);

        ChangeLogIterator logIterator = getStandardChangelogIterator(contexts, labels, changeLog);

        ListVisitor visitor = new ListVisitor();
        logIterator.run(visitor, new RuntimeEnvironment(database, contexts, labels));
        return visitor.getSeenChangeSets();
    }

    /**
     * @deprecated use version with LabelExpression
     */
    @Deprecated
    public List<ChangeSetStatus> getChangeSetStatuses(Contexts contexts) throws LiquibaseException {
        return getChangeSetStatuses(contexts, new LabelExpression());
    }

    public List<ChangeSetStatus> getChangeSetStatuses(Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        return getChangeSetStatuses(contexts, labelExpression, true);
    }
        /**
         * Returns the ChangeSetStatuses of all changesets in the change log file and history in the order they
         * would be ran.
         */
    public List<ChangeSetStatus> getChangeSetStatuses(Contexts contexts, LabelExpression labelExpression,
                                                      boolean checkLiquibaseTables) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        DatabaseChangeLog changeLog = getDatabaseChangeLog();

        if (checkLiquibaseTables) {
            checkLiquibaseTables(true, changeLog, contexts, labelExpression);
        }

        changeLog.validate(database, contexts, labelExpression);

        ChangeLogIterator logIterator = getStandardChangelogIterator(contexts, labelExpression, changeLog);

        StatusVisitor visitor = new StatusVisitor(database);
        logIterator.run(visitor, new RuntimeEnvironment(database, contexts, labelExpression));
        return visitor.getStatuses();
    }

    public void reportStatus(boolean verbose, String contexts, Writer out) throws LiquibaseException {
        reportStatus(verbose, new Contexts(contexts), new LabelExpression(), out);
    }

    public void reportStatus(boolean verbose, Contexts contexts, Writer out) throws LiquibaseException {
        reportStatus(verbose, contexts, new LabelExpression(), out);
    }

    public void reportStatus(boolean verbose, Contexts contexts, LabelExpression labels, Writer out)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labels);

        try {
            List<ChangeSet> unrunChangeSets = listUnrunChangeSets(contexts, labels, false);
            if (unrunChangeSets.isEmpty()) {
                out.append(getDatabase().getConnection().getConnectionUserName());
                out.append("@");
                out.append(getDatabase().getConnection().getURL());
                out.append(" is up to date");
                out.append(StreamUtil.getLineSeparator());
            } else {
                out.append(String.valueOf(unrunChangeSets.size()));
                out.append(" change sets have not been applied to ");
                out.append(getDatabase().getConnection().getConnectionUserName());
                out.append("@");
                out.append(getDatabase().getConnection().getURL());
                out.append(StreamUtil.getLineSeparator());
                if (verbose) {
                    for (ChangeSet changeSet : unrunChangeSets) {
                        out.append("     ").append(changeSet.toString(false))
                            .append(StreamUtil.getLineSeparator());
                    }
                }
            }

            out.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

    }

    public Collection<RanChangeSet> listUnexpectedChangeSets(String contexts) throws LiquibaseException {
        return listUnexpectedChangeSets(new Contexts(contexts), new LabelExpression());
    }

    public Collection<RanChangeSet> listUnexpectedChangeSets(Contexts contexts, LabelExpression labelExpression)
        throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        DatabaseChangeLog changeLog = getDatabaseChangeLog();
        changeLog.validate(database, contexts, labelExpression);

        ChangeLogIterator logIterator = new ChangeLogIterator(changeLog,
                new ContextChangeSetFilter(contexts),
                new LabelChangeSetFilter(labelExpression),
                new DbmsChangeSetFilter(database),
                new IgnoreChangeSetFilter());
        ExpectedChangesVisitor visitor = new ExpectedChangesVisitor(database.getRanChangeSetList());
        logIterator.run(visitor, new RuntimeEnvironment(database, contexts, labelExpression));
        return visitor.getUnexpectedChangeSets();
    }

    public void reportUnexpectedChangeSets(boolean verbose, String contexts, Writer out) throws LiquibaseException {
        reportUnexpectedChangeSets(verbose, new Contexts(contexts), new LabelExpression(), out);
    }

    public void reportUnexpectedChangeSets(boolean verbose, Contexts contexts, LabelExpression labelExpression,
                                           Writer out) throws LiquibaseException {
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);

        try {
            Collection<RanChangeSet> unexpectedChangeSets = listUnexpectedChangeSets(contexts, labelExpression);
            if (unexpectedChangeSets.isEmpty()) {
                out.append(getDatabase().getConnection().getConnectionUserName());
                out.append("@");
                out.append(getDatabase().getConnection().getURL());
                out.append(" contains no unexpected changes!");
                out.append(StreamUtil.getLineSeparator());
            } else {
                out.append(String.valueOf(unexpectedChangeSets.size()));
                out.append(" unexpected changes were found in ");
                out.append(getDatabase().getConnection().getConnectionUserName());
                out.append("@");
                out.append(getDatabase().getConnection().getURL());
                out.append(StreamUtil.getLineSeparator());
                if (verbose) {
                    for (RanChangeSet ranChangeSet : unexpectedChangeSets) {
                        out.append("     ").append(ranChangeSet.toString()).append(StreamUtil.getLineSeparator());
                    }
                }
            }

            out.flush();
        } catch (IOException e) {
            throw new LiquibaseException(e);
        }

    }

    /**
     * Sets checksums to null so they will be repopulated next run
     */
    public void clearCheckSums() throws LiquibaseException {
        LOG.info(LogType.LOG, "Clearing database change log checksums");
        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            checkLiquibaseTables(false, null, new Contexts(), new LabelExpression());

            UpdateStatement updateStatement = new UpdateStatement(
                getDatabase().getLiquibaseCatalogName(),
                getDatabase().getLiquibaseSchemaName(),
                getDatabase().getDatabaseChangeLogTableName()
            );
            updateStatement.addNewColumnValue("MD5SUM", null);
            ExecutorService.getInstance().getExecutor(database).execute(updateStatement);
            getDatabase().commit();
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
        }
        resetServices();
    }

    public final CheckSum calculateCheckSum(final String changeSetIdentifier) throws LiquibaseException {
        if (changeSetIdentifier == null) {
            throw new LiquibaseException(new IllegalArgumentException("changeSetIdentifier"));
        }
        final List<String> parts = StringUtil.splitAndTrim(changeSetIdentifier, "::");
        if ((parts == null) || (parts.size() < CHANGESET_ID_NUM_PARTS)) {
            throw new LiquibaseException(
                new IllegalArgumentException("Invalid changeSet identifier: " + changeSetIdentifier)
            );
        }
        return this.calculateCheckSum(parts.get(CHANGESET_ID_CHANGELOG_PART),
            parts.get(CHANGESET_ID_CHANGESET_PART), parts.get(CHANGESET_ID_AUTHOR_PART));
    }

    public CheckSum calculateCheckSum(final String filename, final String id, final String author)
        throws LiquibaseException {
        LOG.info(LogType.LOG, String.format("Calculating checksum for changeset %s::%s::%s", filename, id, author));
        final ChangeLogParameters clParameters = this.getChangeLogParameters();
        final ResourceAccessor resourceAccessor = this.getResourceAccessor();
        final DatabaseChangeLog changeLog =
            ChangeLogParserFactory.getInstance().getParser(
                this.changeLogFile, resourceAccessor
            ).parse(this.changeLogFile, clParameters, resourceAccessor);

        // TODO: validate?

        final ChangeSet changeSet = changeLog.getChangeSet(filename, author, id);
        if (changeSet == null) {
            throw new LiquibaseException(
                new IllegalArgumentException("No such changeSet: " + filename + "::" + id + "::" + author)
            );
        }

        return changeSet.generateCheckSum();
    }

    public void generateDocumentation(String outputDirectory) throws LiquibaseException {
        // call without context
        generateDocumentation(outputDirectory, new Contexts(), new LabelExpression());
    }

    public void generateDocumentation(String outputDirectory, String contexts) throws LiquibaseException {
        generateDocumentation(outputDirectory, new Contexts(contexts), new LabelExpression());
    }

    public void generateDocumentation(String outputDirectory, Contexts contexts,
                                      LabelExpression labelExpression) throws LiquibaseException {
        LOG.info(LogType.LOG, "Generating Database Documentation");
        changeLogParameters.setContexts(contexts);
        changeLogParameters.setLabels(labelExpression);
        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
        lockService.waitForLock();

        try {
            DatabaseChangeLog changeLog = getDatabaseChangeLog();
            checkLiquibaseTables(false, changeLog, new Contexts(), new LabelExpression());

            changeLog.validate(database, contexts, labelExpression);

            ChangeLogIterator logIterator = new ChangeLogIterator(changeLog,
                    new DbmsChangeSetFilter(database));

            DBDocVisitor visitor = new DBDocVisitor(database);
            logIterator.run(visitor, new RuntimeEnvironment(database, contexts, labelExpression));

            visitor.writeHTML(new File(outputDirectory), resourceAccessor);
        } catch (IOException e) {
            throw new LiquibaseException(e);
        } finally {
            try {
                lockService.releaseLock();
            } catch (LockException e) {
                LOG.severe(LogType.LOG, MSG_COULD_NOT_RELEASE_LOCK, e);
            }
        }
    }

    public DiffResult diff(Database referenceDatabase, Database targetDatabase, CompareControl compareControl)
        throws LiquibaseException {
        return DiffGeneratorFactory.getInstance().compare(referenceDatabase, targetDatabase, compareControl);
    }

    /**
     * Checks changelogs for bad MD5Sums and preconditions before attempting a migration
     */
    public void validate() throws LiquibaseException {

        DatabaseChangeLog changeLog = getDatabaseChangeLog();
        changeLog.validate(database);
    }

    public void setChangeLogParameter(String key, Object value) {
        this.changeLogParameters.set(key, value);
    }

    public void setChangeExecListener(ChangeExecListener listener) {
        this.changeExecListener = listener;
    }

    public void setChangeLogSyncListener(ChangeLogSyncListener changeLogSyncListener) {
        this.changeLogSyncListener = changeLogSyncListener;
    }

    public boolean isIgnoreClasspathPrefix() {
        return ignoreClasspathPrefix;
    }

    public void setIgnoreClasspathPrefix(boolean ignoreClasspathPrefix) {
        this.ignoreClasspathPrefix = ignoreClasspathPrefix;
    }

    @SafeVarargs
    public final void generateChangeLog(CatalogAndSchema catalogAndSchema, DiffToChangeLog changeLogWriter,
                                  PrintStream outputStream, Class<? extends DatabaseObject>... snapshotTypes)
        throws DatabaseException, IOException, ParserConfigurationException {
        generateChangeLog(catalogAndSchema, changeLogWriter, outputStream, null, snapshotTypes);
    }

    @SafeVarargs
    public final void generateChangeLog(CatalogAndSchema catalogAndSchema, DiffToChangeLog changeLogWriter,
                                  PrintStream outputStream, ChangeLogSerializer changeLogSerializer,
                                  Class<? extends DatabaseObject>... snapshotTypes)
        throws DatabaseException, IOException, ParserConfigurationException {

        Set<Class<? extends DatabaseObject>> finalCompareTypes = null;
        if ((snapshotTypes != null) && (snapshotTypes.length > 0)) {
            finalCompareTypes = new HashSet<>(Arrays.asList(snapshotTypes));
        }

        SnapshotControl snapshotControl = new SnapshotControl(this.getDatabase(), snapshotTypes);
        CompareControl compareControl = new CompareControl(new CompareControl.SchemaComparison[] {
            new CompareControl.SchemaComparison(catalogAndSchema, catalogAndSchema)
        }, finalCompareTypes);

        DatabaseSnapshot originalDatabaseSnapshot = null;
        try {
            originalDatabaseSnapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(
                compareControl.getSchemas(CompareControl.DatabaseRole.REFERENCE),
                getDatabase(),
                snapshotControl
            );

            DiffResult diffResult = DiffGeneratorFactory.getInstance().compare(
                originalDatabaseSnapshot,
                SnapshotGeneratorFactory.getInstance().createSnapshot(
                    compareControl.getSchemas(CompareControl.DatabaseRole.REFERENCE),
                    null,
                    snapshotControl
                ),
                compareControl
            );

            changeLogWriter.setDiffResult(diffResult);

            if(changeLogSerializer != null) {
                changeLogWriter.print(outputStream, changeLogSerializer);
            } else {
                changeLogWriter.print(outputStream);
            }
        } catch (InvalidExampleException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

}


package liquibase.util.csv.opencsv.bean;

/**
 Copyright 2007 Kyle Miller.

 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.
 */

import liquibase.util.csv.opencsv.CSVReader;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * Converts CSV data to objects.
 *
 * @param <T> - class to convert the objects to.
 */
public class CsvToBean<T> extends AbstractCSVToBean {
   private Map<Class<?>, PropertyEditor> editorMap;

   /**
    * Default constructor.
    */
   public CsvToBean() {
   }

   /**
    * parse the values from a csvReader constructed from the passed in Reader.
    * @param mapper - mapping strategy for the bean.
    * @param reader - Reader used to construct a CSVReader
    * @return List of Objects.
    */

   public List<T> parse(MappingStrategy<T> mapper, Reader reader) {
      return parse(mapper, new CSVReader(reader));
   }

   /**
    * parse the values from a csvReader constructed from the passed in Reader.
    * @param mapper - mapping strategy for the bean.
    * @param reader - Reader used to construct a CSVReader
    * @param filter - CsvToBeanFilter to apply - null if no filter.
    * @return List of Objects.
    */
   public List<T> parse(MappingStrategy<T> mapper, Reader reader, CsvToBeanFilter filter) {
      return parse(mapper, new CSVReader(reader), filter);
   }

   /**
    * parse the values from the csvReader.
    * @param mapper - mapping strategy for the bean.
    * @param csv - CSVReader
    * @return List of Objects.
    */
   public List<T> parse(MappingStrategy<T> mapper, CSVReader csv) {
      return parse(mapper, csv, null);
   }

   /**
    * parse the values from the csvReader.
    * @param mapper - mapping strategy for the bean.
    * @param csv - CSVReader
    * @param filter - CsvToBeanFilter to apply - null if no filter.
    * @return List of Objects.
    */
   public List<T> parse(MappingStrategy<T> mapper, CSVReader csv, CsvToBeanFilter filter) {
      long lineProcessed = 0;
      String[] line = null;

      try {
         mapper.captureHeader(csv);
      } catch (Exception e) {
         throw new RuntimeException("Error capturing CSV header!", e);
      }

      try {
         List<T> list = new ArrayList<>();
         while (null != (line = csv.readNext())) {
            lineProcessed++;
            processLine(mapper, filter, line, list);
         }
         return list;
      } catch (Exception e) {
         throw new RuntimeException("Error parsing CSV line: " + lineProcessed + " values: " + Arrays.toString(line), e);
      }
   }

   private void processLine(MappingStrategy<T> mapper, CsvToBeanFilter filter, String[] line, List<T> list) throws ReflectiveOperationException, IntrospectionException {
      if ((filter == null) || filter.allowLine(line)) {
         T obj = processLine(mapper, line);
         list.add(obj);
      }
   }

   /**
    * Creates a single object from a line from the csv file.
    * @param mapper - MappingStrategy
    * @param line  - array of Strings from the csv file.
    * @return - object containing the values.
    * @throws ReflectiveOperationException - thrown on error accessing bean.
    * @throws IntrospectionException - thrown on error getting the PropertyDescriptor.
    */
   protected T processLine(MappingStrategy<T> mapper, String[] line) throws ReflectiveOperationException, IntrospectionException {
      T bean = mapper.createBean();
      for (int col = 0; col < line.length; col++) {
         if (mapper.isAnnotationDriven()) {
            processField(mapper, line, bean, col);
         } else {
            processProperty(mapper, line, bean, col);
         }
      }
      return bean;
   }

   private void processProperty(MappingStrategy<T> mapper, String[] line, T bean, int col) throws IntrospectionException, ReflectiveOperationException {
      PropertyDescriptor prop = mapper.findDescriptor(col);
      if (null != prop) {
         String value = checkForTrim(line[col], prop);
         Object obj = convertValue(value, prop);
         prop.getWriteMethod().invoke(bean, obj);
      }
   }

   private void processField(MappingStrategy<T> mapper, String[] line, T bean, int col) throws IllegalAccessException {
      BeanField beanField = mapper.findField(col);
      if (beanField != null) {
         String value = line[col];
         beanField.setFieldValue(bean, value);
      }
   }

   private PropertyEditor getPropertyEditorValue(Class<?> cls) {
      if (editorMap == null) {
         editorMap = new HashMap<>();
      }

      PropertyEditor editor = editorMap.get(cls);

      if (editor == null) {
         editor = PropertyEditorManager.findEditor(cls);
         addEditorToMap(cls, editor);
      }

      return editor;
   }

   private void addEditorToMap(Class<?> cls, PropertyEditor editor) {
      if (editor != null) {
         editorMap.put(cls, editor);
      }
   }

   /**
    * Attempt to find custom property editor on descriptor first, else try the propery editor manager.
    *
    * @param desc - PropertyDescriptor.
    * @return - the PropertyEditor for the given PropertyDescriptor.
    * @throws ReflectiveOperationException - thrown when getting the PropertyEditor for the class.
    */
   protected PropertyEditor getPropertyEditor(PropertyDescriptor desc) throws ReflectiveOperationException {
      Class<?> cls = desc.getPropertyEditorClass();
      if (null != cls) {
         return (PropertyEditor) cls.getConstructor().newInstance();
      }
      return getPropertyEditorValue(desc.getPropertyType());
   }
}


package liquibase.servicelocator;

import liquibase.Scope;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.util.StringUtil;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.net.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * Default implement of {@link PackageScanClassResolver}
 */
public class DefaultPackageScanClassResolver implements PackageScanClassResolver {

    protected final transient Logger log = Scope.getCurrentScope().getLog(getClass());
    private Set<ClassLoader> classLoaders;
    private Set<PackageScanFilter> scanFilters;
    private Map<String, Set<Class>> allClassesByPackage = new HashMap<String, Set<Class>>();
    private Set<String> loadedPackages = new HashSet<String>();

    private Map<String, Set<String>> classFilesByLocation = new HashMap<String, Set<String>>();

    @Override
    public void addClassLoader(ClassLoader classLoader) {
        try {
            getClassLoaders().add(classLoader);
        } catch (UnsupportedOperationException ex) {
            // Ignore this exception as the PackageScanClassResolver
            // don't want use any other classloader
        }
    }

    @Override
    public void addFilter(PackageScanFilter filter) {
        if (scanFilters == null) {
            scanFilters = new LinkedHashSet<PackageScanFilter>();
        }
        scanFilters.add(filter);
    }

    @Override
    public void removeFilter(PackageScanFilter filter) {
        if (scanFilters != null) {
            scanFilters.remove(filter);
        }
    }

    @Override
    public Set<ClassLoader> getClassLoaders() {
        if (classLoaders == null) {
            classLoaders = new HashSet<ClassLoader>();
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl != null) {
                log.fine(LogType.LOG, "The thread context class loader: " + ccl + "  is used to load the class");
                classLoaders.add(ccl);
            }
            classLoaders.add(DefaultPackageScanClassResolver.class.getClassLoader());
        }
        return classLoaders;
    }

    @Override
    public void setClassLoaders(Set<ClassLoader> classLoaders) {
        this.classLoaders = classLoaders;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set<Class<?>> findImplementations(Class parent, String... packageNames) {
        if (packageNames == null) {
            return Collections.EMPTY_SET;
        }

        log.fine(LogType.LOG, "Searching for implementations of " + parent.getName() + " in packages: " + Arrays.asList(packageNames));

        PackageScanFilter test = getCompositeFilter(new AssignableToPackageScanFilter(parent));
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        for (String pkg : packageNames) {
            find(test, pkg, classes);
        }

        log.fine(LogType.LOG, "Found: " + classes);

        return classes;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set<Class<?>> findByFilter(PackageScanFilter filter, String... packageNames) {
        if (packageNames == null) {
            return Collections.EMPTY_SET;
        }

        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        for (String pkg : packageNames) {
            find(filter, pkg, classes);
        }

        log.fine(LogType.LOG, "Found: " + classes);

        return classes;
    }

    protected void find(PackageScanFilter test, String packageName, Set<Class<?>> classes) {
        packageName = packageName.replace('.', '/');

        Set<ClassLoader> set = getClassLoaders();

        if (!loadedPackages.contains(packageName)) {
            for (ClassLoader classLoader : set) {
                this.findAllClasses(packageName, classLoader);
            }
            loadedPackages.add(packageName);
        }

        findInAllClasses(test, packageName, classes);
    }

    protected void findAllClasses(String packageName, ClassLoader loader) {
        log.fine(LogType.LOG, "Searching for all classes in package: " + packageName + " using classloader: " + loader.getClass().getName());

        Enumeration<URL> urls;
        try {
            urls = getResources(loader, packageName);
            if (!urls.hasMoreElements()) {
                log.fine(LogType.LOG, "No URLs returned by classloader");
            }
        } catch (IOException ioe) {
            log.warning(LogType.LOG, "Cannot read package: " + packageName, ioe);
            return;
        }

        while (urls.hasMoreElements()) {
            URL url = null;
            try {
                url = urls.nextElement();
                log.fine(LogType.LOG, "URL from classloader: " + url);

                url = customResourceLocator(url);

                String urlPath = url.getFile();
                String host = null;
                urlPath = URLDecoder.decode(urlPath, LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding());

                if ("vfs".equals(url.getProtocol()) && !urlPath.startsWith("vfs")) {
                    urlPath = "vfs:"+urlPath;
                }
                if ("vfszip".equals(url.getProtocol()) && !urlPath.startsWith("vfszip")) {
                    urlPath = "vfszip:"+urlPath;
                }

                log.fine(LogType.LOG, "Decoded urlPath: " + urlPath + " with protocol: " + url.getProtocol());

                // If it's a file in a directory, trim the stupid file: spec
                if (urlPath.startsWith("file:")) {
                    // file path can be temporary folder which uses characters that the URLDecoder decodes wrong
                    // for example + being decoded to something else (+ can be used in temp folders on Mac OS)
                    // to remedy this then create new path without using the URLDecoder
                    try {
                        URI uri = new URI(url.getFile());
                        host = uri.getHost();
                        urlPath = uri.getPath();
                    } catch (URISyntaxException e) {
                        // fallback to use as it was given from the URLDecoder
                        // this allows us to work on Windows if users have spaces in paths
                    }

                    if (urlPath.startsWith("file:")) {
                        urlPath = urlPath.substring(5);
                    }
                }

                // osgi bundles should be skipped
                if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
                    log.fine(LogType.LOG, "It's a virtual osgi bundle, skipping");
                    continue;
                }

                // manifold classes should be skipped (no physical jar file present)
                // see http://manifold.systems/ for details about manifold
                if (url.toString().startsWith("manifoldclass:") || urlPath.startsWith("manifoldclass:")) {
                    log.fine(LogType.LOG, "It's a manifold classes url, skipping");
                    continue;
                }

                // Else it's in a JAR, grab the path to the jar
                if (urlPath.contains(".jar/") && !urlPath.contains(".jar!/")) {
                    urlPath = urlPath.replace(".jar/", ".jar!/");
                }

                if (urlPath.indexOf('!') > 0) {
                    urlPath = urlPath.substring(0, urlPath.indexOf('!'));
                }

                // If a host component was given prepend it to the decoded path.
                // This still has its problems as we silently skip user and password
                // information etc. but it fixes UNC urls on windows.
                if (host != null) {
                    if (urlPath.startsWith("/")) {
                        urlPath = "//" + host + urlPath;
                    } else {
                        urlPath = "//" + host + "/" + urlPath;
                    }
                }

                File file = new File(urlPath);
                if (file.isDirectory()) {
                    log.fine(LogType.LOG, "Loading from directory using file: " + file);
                    loadImplementationsInDirectory(packageName, file, loader);
                } else {
                    InputStream stream;
                    if (urlPath.startsWith("http:") || urlPath.startsWith("https:")
                            || urlPath.startsWith("sonicfs:") || urlPath.startsWith("vfs:") || urlPath.startsWith("vfszip:")) {
                        // load resources using http/https
                        // sonic ESB requires to be loaded using a regular URLConnection
                        URL urlStream = new URL(urlPath);
                        log.fine(LogType.LOG, "Loading from jar using "+urlStream.getProtocol()+": " + urlPath);
                        URLConnection con = urlStream.openConnection();
                        // disable cache mainly to avoid jar file locking on Windows
                        con.setUseCaches(false);
                        stream = con.getInputStream();
                    } else {
                        log.fine(LogType.LOG, "Loading from jar using file: " + file);
                        stream = new FileInputStream(file);
                    }

                    try {
                        loadImplementationsInJar(packageName, stream, loader, urlPath, null);
                    } catch (IOException ioe) {
                        log.warning(LogType.LOG, "Cannot search jar file '" + urlPath + "' for classes due to an IOException: " + ioe.getMessage(), ioe);
                    } finally {
                        stream.close();
                    }
                }
            } catch (IOException e) {
                // use debug logging to avoid being to noisy in logs
                log.fine(LogType.LOG, "Cannot read entries in url: " + url, e);
            }
        }
    }

    protected void findInAllClasses(PackageScanFilter test, String packageName, Set<Class<?>> classes) {
        log.fine(LogType.LOG, "Searching for: " + test + " in package: " + packageName );

        Set<Class> packageClasses = getFoundClasses(packageName);
        if (packageClasses == null) {
            log.fine(LogType.LOG, "No classes found in package: " + packageName );
            return;
        }
        for (Class type : packageClasses) {
            if (test.matches(type)) {
                classes.add(type);
            }
        }

    }

    protected void addFoundClass(Class<?> type) {
        if (type.getPackage() != null) {
            String packageName = type.getPackage().getName();
            List<String> packageNameParts = Arrays.asList(packageName.split("\\."));
            for (int i = 0; i < packageNameParts.size(); i++) {
                String thisPackage = StringUtil.join(packageNameParts.subList(0, i + 1), "/");
                addFoundClass(thisPackage, type);
            }
        }
    }

    protected void addFoundClass(String packageName, Class<?> type) {
        packageName = packageName.replace("/", ".");

        if (!this.allClassesByPackage.containsKey(packageName)) {
            this.allClassesByPackage.put(packageName, new HashSet<Class>());
        }

        this.allClassesByPackage.get(packageName).add(type);
    }

    protected Set<Class> getFoundClasses(String packageName) {
        packageName = packageName.replace("/", ".");
        return this.allClassesByPackage.get(packageName);
    }

    // We can override this method to support the custom ResourceLocator

    protected URL customResourceLocator(URL url) throws IOException {
        // Do nothing here
        return url;
    }

    /**
     * Strategy to get the resources by the given classloader.
     *

     * Notice that in WebSphere platforms there is a {@link WebSpherePackageScanClassResolver}
     * to take care of WebSphere's odditiy of resource loading.
     *
     * @param loader      the classloader
     * @param packageName the packagename for the package to load
     * @return URL's for the given package
     * @throws IOException is thrown by the classloader
     */
    protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
        log.fine(LogType.LOG, "Getting resource URL for package: " + packageName + " with classloader: " + loader);

        // If the URL is a jar, the URLClassloader.getResources() seems to require a trailing slash.  The
        // trailing slash is harmless for other URLs
        if (!packageName.endsWith("/")) {
            packageName = packageName + "/";
        }
        return loader.getResources(packageName);
    }

    private PackageScanFilter getCompositeFilter(PackageScanFilter filter) {
        if (scanFilters != null) {
            CompositePackageScanFilter composite = new CompositePackageScanFilter(scanFilters);
            composite.addFilter(filter);
            return composite;
        }
        return filter;
    }

    /**
     * Finds matches in a physical directory on a filesystem. Examines all files
     * within a directory - if the File object is not a directory, and ends with
     * .class the file is loaded. Operates recursively to find classes within a
     * folder structure matching the package structure.
     *
     * @param parent   the package name up to this directory in the package
     *                 hierarchy. E.g. if /classes is in the classpath and we wish to
     *                 examine files in /classes/org/apache then the values of
     *                 parent would be org/apache
     * @param location a File object representing a directory
     */
    private void loadImplementationsInDirectory(String parent, File location, ClassLoader classLoader) {
        Set<String> classFiles = classFilesByLocation.get(location.toString());
        if (classFiles == null) {
            classFiles = new HashSet<String>();

            File[] files = location.listFiles();
            StringBuilder builder = null;

            for (File file : files) {
                builder = new StringBuilder(100);
                String name = file.getName();
                if (name != null) {
                    name = name.trim();
                    builder.append(parent).append("/").append(name);
                    String packageOrClass = (parent == null) ? name : builder.toString();

                    if (file.isDirectory()) {
                        loadImplementationsInDirectory(packageOrClass, file, classLoader);
                    } else if (name.endsWith(".class")) {
                        classFiles.add(packageOrClass);
                    }
                }
            }
        }

        for (String packageOrClass : classFiles) {
            this.loadClass(packageOrClass, classLoader);
        }
    }

    private void loadClass(String className, ClassLoader classLoader) {
        try {
            String externalName = className.substring(0, className.indexOf('.')).replace('/', '.');
            Class<?> type = classLoader.loadClass(externalName);
            log.fine(LogType.LOG, "Loaded the class: " + type + " in classloader: " + classLoader);

            if (Modifier.isAbstract(type.getModifiers()) || Modifier.isInterface(type.getModifiers())) {
                return;
            }

            addFoundClass(type);

        } catch (ClassNotFoundException e) {
            log.fine(LogType.LOG, "Cannot find class '" + className + "' in classloader: " + classLoader
                    + ". Reason: " + e, e);
        } catch (LinkageError e) {
            log.fine(LogType.LOG, "Cannot find the class definition '" + className + "' in classloader: " + classLoader
                    + ". Reason: " + e, e);
        } catch (Exception e) {
            log.severe(LogType.LOG, "Cannot load class '"+className+"' in classloader: "+classLoader+".  Reason: "+e, e);
        }

    }

    /**
     * Finds matching classes within a jar files that contains a folder
     * structure matching the package structure. If the File is not a JarFile or
     * does not exist a warning will be logged, but no error will be raised.
     *
     * Any nested JAR files found inside this JAR will be assumed to also be
     * on the classpath and will be recursively examined for classes in `parentPackage`.
     * @param parentPackage  the parent package under which classes must be in order to
     *                be considered
     * @param parentFileStream  the inputstream of the jar file to be examined for classes
     * @param loader a classloader which can load classes contained within the JAR file
     * @param parentFileName a unique name for the parentFileStream, to be used for caching.
*                       This is the URL of the parentFileStream, if it comes from a URL,
*                       or a composite ID if we are currently examining a nested JAR.
     */
    protected void loadImplementationsInJar(
            String parentPackage,
            InputStream parentFileStream,
            ClassLoader loader,
            String parentFileName,
            String grandparentFileName) throws IOException {
        Set<String> classFiles = classFilesByLocation.get(parentFileName);

        if (classFiles == null) {
            classFiles = new HashSet<String>();
            classFilesByLocation.put(parentFileName, classFiles);

            Set<String> grandparentClassFiles = classFilesByLocation.get(grandparentFileName);
            if (grandparentClassFiles == null) {
                grandparentClassFiles = new HashSet<String>();
                classFilesByLocation.put(grandparentFileName, grandparentClassFiles);
            }
            JarInputStream jarStream;
            if (parentFileStream instanceof JarInputStream) {
                jarStream = (JarInputStream) parentFileStream;
            } else {
                jarStream = new JarInputStream(parentFileStream);
            }

            JarEntry entry;
            while ((entry = jarStream.getNextJarEntry()) != null) {
                String name = entry.getName();
                if (name != null) {
                    if (name.endsWith(".jar")) { //in a nested jar
                        log.fine(LogType.LOG, "Found nested jar " + name);

                        // To avoid needing to unzip 'parentFile' in its entirety, as that
                        // may take a very long time (see CORE-2115) or not even be possible
                        // (see CORE-2595), we load the nested JAR from the classloader and
                        // read it as a zip.
                        //
                        // It is safe to assume that the nested JAR is readable by the classloader
                        // as a resource stream, because we have reached this point by scanning
                        // through packages located from `classloader` by using `getResource`.
                        // If loading this nested JAR as a resource fails, then certainly loading
                        // classes from inside it with `classloader` would fail and we are safe
                        // to exclude it form the PackageScan.
                        InputStream nestedJarResourceStream = loader.getResourceAsStream(name);
                        if (nestedJarResourceStream != null) {
                            JarInputStream nestedJarStream = new JarInputStream(nestedJarResourceStream);
                            try {
                                loadImplementationsInJar(
                                        parentPackage,
                                        nestedJarStream,
                                        loader,
                                        parentFileName + "!" + name,
                                        parentFileName);
                            } finally {
                                nestedJarStream.close();
                            }
                        }
                    } else if (!entry.isDirectory() && name.endsWith(".class")) {
                        classFiles.add(name.trim());
                        grandparentClassFiles.add(name.trim());
                    }
                }
            }
        }

        for (String name : classFiles) {
            if (name.contains(parentPackage)) {
                loadClass(name, loader);
            }
        }
    }

    /**
     * Add the class designated by the fully qualified class name provided to
     * the set of resolved classes if and only if it is approved by the Test
     * supplied.
     *
     * @param test the test used to determine if the class matches
     * @param fqn  the fully qualified name of a class
     */
    protected void addIfMatching(PackageScanFilter test, String fqn, Set<Class<?>> classes) {
        try {
            String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
            Set<ClassLoader> set = getClassLoaders();
            boolean found = false;
            for (ClassLoader classLoader : set) {
                log.fine(LogType.LOG, "Testing that class " + externalName + " matches criteria [" + test + "] using classloader:" + classLoader);
                try {
                    Class<?> type = classLoader.loadClass(externalName);
                    log.fine(LogType.LOG, "Loaded the class: " + type + " in classloader: " + classLoader);
                    if (test.matches(type)) {
                        log.fine(LogType.LOG, "Found class: " + type + " which matches the filter in classloader: " + classLoader);
                        classes.add(type);
                    }
                    found = true;
                    break;
                } catch (ClassNotFoundException e) {
                    log.fine(LogType.LOG, "Cannot find class '" + fqn + "' in classloader: " + classLoader
                            + ". Reason: " + e, e);
                } catch (LinkageError e) {
                    log.fine(LogType.LOG, "Cannot find the class definition '" + fqn + "' in classloader: " + classLoader
                            + ". Reason: " + e, e);
                } catch (Exception e) {
                    log.severe(LogType.LOG, "Cannot load class '"+fqn+"' in classloader: "+classLoader+".  Reason: "+e, e);
                }
            }
            if (!found) {
                // use debug to avoid being noisy in logs
                log.fine(LogType.LOG, "Cannot find class '" + fqn + "' in any classloaders: " + set);
            }
        } catch (Exception e) {
            log.warning(LogType.LOG, "Cannot examine class '" + fqn + "' due to a " + e.getClass().getName()
                    + " with message: " + e.getMessage(), e);
        }
    }

}