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

liquibase.command.core.AbstractUpdateCommandStep Maven / Gradle / Ivy

The newest version!
package liquibase.command.core;

import liquibase.*;
import liquibase.changelog.*;
import liquibase.changelog.filter.*;
import liquibase.changelog.visitor.ChangeExecListener;
import liquibase.changelog.visitor.DefaultChangeExecListener;
import liquibase.changelog.visitor.StatusVisitor;
import liquibase.changelog.visitor.UpdateVisitor;
import liquibase.command.AbstractCommandStep;
import liquibase.command.CleanUpCommandStep;
import liquibase.command.CommandResultsBuilder;
import liquibase.command.CommandScope;
import liquibase.command.core.helpers.DatabaseChangelogCommandStep;
import liquibase.database.Database;
import liquibase.exception.LiquibaseException;
import liquibase.exception.LockException;
import liquibase.executor.ExecutorService;
import liquibase.lockservice.LockService;
import liquibase.lockservice.LockServiceFactory;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.MdcObject;
import liquibase.logging.mdc.MdcValue;
import liquibase.logging.mdc.customobjects.ChangesetsUpdated;
import liquibase.report.UpdateReportParameters;
import liquibase.util.ShowSummaryUtil;
import liquibase.util.StringUtil;
import liquibase.util.UpdateSummaryDetails;

import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static liquibase.Liquibase.MSG_COULD_NOT_RELEASE_LOCK;
import static liquibase.executor.jvm.JdbcExecutor.ROWS_AFFECTED_SCOPE_KEY;

public abstract class AbstractUpdateCommandStep extends AbstractCommandStep implements CleanUpCommandStep {
    public static final String DEFAULT_CHANGE_EXEC_LISTENER_RESULT_KEY = "defaultChangeExecListener";
    private static final String DATABASE_UP_TO_DATE_MESSAGE = "Database is up to date, no changesets to execute";
    private boolean isFastCheckEnabled = true;

    private boolean isDBLocked = true;

    public abstract String getChangelogFileArg(CommandScope commandScope);

    public abstract String getContextsArg(CommandScope commandScope);

    public abstract String getLabelFilterArg(CommandScope commandScope);

    public abstract String[] getCommandName();

    public abstract UpdateSummaryEnum getShowSummary(CommandScope commandScope);

    public UpdateSummaryOutputEnum getShowSummaryOutput(CommandScope commandScope) {
        return (UpdateSummaryOutputEnum) commandScope.getDependency(UpdateSummaryOutputEnum.class);
    }

    @Override
    public List> requiredDependencies() {
        return Arrays.asList(Database.class, LockService.class, DatabaseChangeLog.class, ChangeExecListener.class, ChangeLogParameters.class);
    }

    @Override
    public void run(CommandResultsBuilder resultsBuilder) throws Exception {
        UpdateReportParameters updateReportParameters = new UpdateReportParameters();
        updateReportParameters.setCommandTitle(getFormattedCommandName(getCommandName()));
        resultsBuilder.addResult("updateReport", updateReportParameters);
        CommandScope commandScope = resultsBuilder.getCommandScope();
        Database database = (Database) commandScope.getDependency(Database.class);
        updateReportParameters.getDatabaseInfo().setDatabaseType(database.getDatabaseProductName());
        updateReportParameters.getDatabaseInfo().setVersion(database.getDatabaseProductVersion());
        updateReportParameters.getDatabaseInfo().setDatabaseUrl(database.getConnection().getURL());
        updateReportParameters.setJdbcUrl(database.getConnection().getURL());
        final ChangeLogParameters changeLogParameters = (ChangeLogParameters) commandScope.getDependency(ChangeLogParameters.class);
        Contexts contexts = new Contexts(getContextsArg(commandScope));
        LabelExpression labelExpression = new LabelExpression(getLabelFilterArg(commandScope));
        updateReportParameters.getOperationInfo().setLabels(labelExpression.getOriginalString());
        updateReportParameters.getOperationInfo().setContexts(contexts.toString());
        DatabaseChangelogCommandStep.addCommandFiltersMdc(labelExpression, contexts);
        customMdcLogging(commandScope);

        ChangeExecListener changeExecListener = getChangeExecListener(resultsBuilder, commandScope);
        try {
            DatabaseChangeLog databaseChangeLog = (DatabaseChangeLog) commandScope.getDependency(DatabaseChangeLog.class);
            updateReportParameters.setChangelogArgValue(databaseChangeLog.getFilePath());
            ChangeLogIterator runChangeLogIterator = getStandardChangelogIterator(commandScope, database, contexts, labelExpression, databaseChangeLog);
            preRun(commandScope, runChangeLogIterator, changeLogParameters);
            if (isFastCheckEnabled && isUpToDate(commandScope, database, databaseChangeLog, contexts, labelExpression, resultsBuilder.getOutputStream())) {
                updateReportParameters.getOperationInfo().setRowsAffected(0);
                updateReportParameters.getOperationInfo().setUpdateSummaryMsg(DATABASE_UP_TO_DATE_MESSAGE);
                return;
            }
            if (!isDBLocked) {
                LockServiceFactory.getInstance().getLockService(database).waitForLock();
                isDBLocked = true;
            }
            // waitForLock resets the changelog history service, so we need to rebuild that and generate a final deploymentId.
            ChangeLogHistoryService changelogService = Scope.getCurrentScope().getSingleton(ChangeLogHistoryServiceFactory.class).getChangeLogService(database);
            changelogService.generateDeploymentId();

            Scope.getCurrentScope().addMdcValue(MdcKey.DEPLOYMENT_ID, changelogService.getDeploymentId());
            Scope.getCurrentScope().getLog(getClass()).info(String.format("Using deploymentId: %s", changelogService.getDeploymentId()));

            StatusVisitor statusVisitor = getStatusVisitor(commandScope, database, contexts, labelExpression, databaseChangeLog);

            AtomicInteger rowsAffected = new AtomicInteger(0);
            HashMap scopeValues = new HashMap<>();
            scopeValues.put("showSummary", getShowSummary(commandScope));
            scopeValues.put(ROWS_AFFECTED_SCOPE_KEY, rowsAffected);
            Scope.child(scopeValues, () -> {
                try {
                    runChangeLogIterator.run(new UpdateVisitor(database, changeExecListener, new ShouldRunChangeSetFilter(database)),
                            new RuntimeEnvironment(database, contexts, labelExpression));
                } finally {
                    UpdateSummaryDetails details = ShowSummaryUtil.buildSummaryDetails(databaseChangeLog, getShowSummary(commandScope), getShowSummaryOutput(commandScope), statusVisitor, resultsBuilder.getOutputStream(), runChangeLogIterator, changeExecListener);
                    if (details != null) {
                        updateReportParameters.getOperationInfo().setUpdateSummaryMsg(details.getOutput());
                        updateReportParameters.getChangesetInfo().addAllToPendingChangesetInfoList(details.getSkipped());
                        updateReportParameters.getChangesetInfo().setPendingChangesetCount(updateReportParameters.getChangesetInfo().getPendingChangesetInfoList().size());
                    }
                }
            });
            updateReportParameters.getOperationInfo().setRowsAffected(rowsAffected.get());
            database.afterUpdate();
            resultsBuilder.addResult("statusCode", 0);
            addChangelogFileToMdc(getChangelogFileArg(commandScope), databaseChangeLog);
            Scope.getCurrentScope().addMdcValue(MdcKey.ROWS_AFFECTED, String.valueOf(rowsAffected.get()));
            logDeploymentOutcomeMdc(changeExecListener, true, updateReportParameters);
            postUpdateLog(rowsAffected.get(), runChangeLogIterator.getExceptionChangeSets());
        } catch (Exception e) {
            DatabaseChangeLog databaseChangeLog = (DatabaseChangeLog) commandScope.getDependency(DatabaseChangeLog.class);
            addChangelogFileToMdc(getChangelogFileArg(commandScope), databaseChangeLog);
            logDeploymentOutcomeMdc(changeExecListener, false, updateReportParameters);
            updateReportParameters.getOperationInfo().setException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage());
            resultsBuilder.addResult("statusCode", 1);
            throw e;
        } finally {
            if (isDBLocked) {
                try {
                    LockServiceFactory.getInstance().getLockService(database).releaseLock();
                } catch (LockException e) {
                    Scope.getCurrentScope().getLog(getClass()).severe(MSG_COULD_NOT_RELEASE_LOCK, e);
                }
            }
        }
    }

    /**
     * Executed before running any updates against the database.
     */
    protected void preRun(CommandScope commandScope, ChangeLogIterator runChangeLogIterator, ChangeLogParameters changeLogParameters) throws LiquibaseException {
        // do nothing by default
    }

    private StatusVisitor getStatusVisitor(CommandScope commandScope, Database database, Contexts contexts, LabelExpression labelExpression, DatabaseChangeLog databaseChangeLog) throws LiquibaseException {
        StatusVisitor statusVisitor = new StatusVisitor(database);
        ChangeLogIterator shouldRunIterator = getStatusChangelogIterator(commandScope, database, contexts, labelExpression, databaseChangeLog);
        shouldRunIterator.run(statusVisitor, new RuntimeEnvironment(database, contexts, labelExpression));
        return statusVisitor;
    }

    private ChangeExecListener getChangeExecListener(CommandResultsBuilder resultsBuilder, CommandScope commandScope) {
        //
        // Create and add the listener to the resultsBuilder so that it is available
        // for exception handling when there is an error
        //
        ChangeExecListener changeExecListener = (ChangeExecListener) commandScope.getDependency(ChangeExecListener.class);
        // Because of MDC we need to reset the cached changesets in the listener, because in the case of something like
        // update-testing-rollback, the same listener is used for both updates
        if (changeExecListener instanceof DefaultChangeExecListener) {
            ((DefaultChangeExecListener) changeExecListener).reset();
        }
        resultsBuilder.addResult(DEFAULT_CHANGE_EXEC_LISTENER_RESULT_KEY, changeExecListener);
        return changeExecListener;
    }

    private void addChangelogFileToMdc(String changeLogFile, DatabaseChangeLog databaseChangeLog) {
        if (StringUtil.isNotEmpty(databaseChangeLog.getLogicalFilePath())) {
            Scope.getCurrentScope().addMdcValue(MdcKey.CHANGELOG_FILE, databaseChangeLog.getLogicalFilePath());
        } else if (StringUtil.isNotEmpty(changeLogFile)) {
            Scope.getCurrentScope().addMdcValue(MdcKey.CHANGELOG_FILE, changeLogFile);
        }
    }

    protected void customMdcLogging(CommandScope commandScope) {
        // do nothing by default
    }

    @Override
    public void cleanUp(CommandResultsBuilder resultsBuilder) {
        LockServiceFactory.getInstance().resetAll();
        Scope.getCurrentScope().getSingleton(ChangeLogHistoryServiceFactory.class).resetAll();
        Scope.getCurrentScope().getSingleton(ExecutorService.class).reset();
    }

    private void logDeploymentOutcomeMdc(ChangeExecListener defaultListener, boolean success, UpdateReportParameters updateReportParameters) {
        String successLog = "Update command completed successfully.";
        String failureLog = "Update command encountered an exception.";
        if (defaultListener instanceof DefaultChangeExecListener) {
            List deployedChangeSets = ((DefaultChangeExecListener) defaultListener).getDeployedChangeSets();
            int deployedChangeSetCount = deployedChangeSets.size();
            List failedChangeSets = ((DefaultChangeExecListener) defaultListener).getFailedChangeSets();
            int failedChangeSetCount = failedChangeSets.size();
            ChangesetsUpdated changesetsUpdated = new ChangesetsUpdated(deployedChangeSets);
            updateReportParameters.getChangesetInfo().setChangesetCount(deployedChangeSetCount + failedChangeSetCount);
            updateReportParameters.getChangesetInfo().setFailedChangesetCount(failedChangeSetCount);
            updateReportParameters.getChangesetInfo().addAllToChangesetInfoList(deployedChangeSets, false);
            updateReportParameters.getChangesetInfo().addAllToChangesetInfoList(failedChangeSets, false);
            if (!updateReportParameters.getChangesetInfo().getPendingChangesetInfoList().isEmpty()) {
                // If there are failures remove these changes from the pending changeset list
                // and update the count to reflect only skipped(pending) changes by removing the failed count
                updateReportParameters.getChangesetInfo().getPendingChangesetInfoList()
                        .removeIf(pendingChangesetInfo -> failedChangeSets.stream().anyMatch(changeSet -> changeSet.equals(pendingChangesetInfo.getChangeSet())));
                updateReportParameters.getChangesetInfo().setPendingChangesetCount(updateReportParameters.getChangesetInfo().getPendingChangesetCount() - failedChangeSetCount);
            }
            Scope.getCurrentScope().addMdcValue(MdcKey.DEPLOYMENT_OUTCOME_COUNT, String.valueOf(deployedChangeSetCount));
            Scope.getCurrentScope().addMdcValue(MdcKey.CHANGESETS_UPDATED, changesetsUpdated);
            // Now that the command is completed reset the generated sql in case we are running these changes in some chain
            // like during update-testing-rollback
            deployedChangeSets.forEach(changeSet -> changeSet.setGeneratedSql(new ArrayList<>()));
        }
        String deploymentOutcome = success ? MdcValue.COMMAND_SUCCESSFUL : MdcValue.COMMAND_FAILED;
        updateReportParameters.setSuccess(success);
        updateReportParameters.getOperationInfo().setOperationOutcome(deploymentOutcome);
        try (MdcObject deploymentOutcomeMdc = Scope.getCurrentScope().addMdcValue(MdcKey.DEPLOYMENT_OUTCOME, deploymentOutcome)) {
            Scope.getCurrentScope().getLog(getClass()).info(success ? successLog : failureLog);
        }
    }

    @Beta
    public ChangeLogIterator getStandardChangelogIterator(CommandScope commandScope, Database database, Contexts contexts, LabelExpression labelExpression, DatabaseChangeLog changeLog) throws LiquibaseException {
        List changesetFilters = this.getStandardChangelogIteratorFilters(database, contexts, labelExpression);
        return new ChangeLogIterator(changeLog, changesetFilters.toArray(new ChangeSetFilter[0]));
    }

    @Beta
    public ChangeLogIterator getStatusChangelogIterator(CommandScope commandScope, Database database, Contexts contexts, LabelExpression labelExpression, DatabaseChangeLog changeLog) throws LiquibaseException {
        List changesetFilters = this.getStandardChangelogIteratorFilters(database, contexts, labelExpression);
        changesetFilters.add(new ShouldRunChangeSetFilter(database));
        return new StatusChangeLogIterator(changeLog, changesetFilters.toArray(new ChangeSetFilter[0]));
    }

    protected List getStandardChangelogIteratorFilters(Database database, Contexts contexts, LabelExpression labelExpression) {
        return new ArrayList<>(Arrays.asList(new ContextChangeSetFilter(contexts),
                new LabelChangeSetFilter(labelExpression),
                new DbmsChangeSetFilter(database),
                new IgnoreChangeSetFilter()));
    }


    /**
     * Checks if the database is up-to-date.
     *
     * @param commandScope
     * @param database          the database to check
     * @param databaseChangeLog the databaseChangeLog of the database
     * @param contexts          the command contexts
     * @param labelExpression   the command label expressions
     * @param outputStream      the current global OutputStream
     * @return true if there are no additional changes to execute, otherwise false
     * @throws LiquibaseException if there was a problem running any queries
     * @throws IOException        if there was a problem handling the update summary
     */
    @Beta
    public boolean isUpToDate(CommandScope commandScope, Database database, DatabaseChangeLog databaseChangeLog, Contexts contexts, LabelExpression labelExpression, OutputStream outputStream) throws LiquibaseException, IOException {
        FastCheckService fastCheck = Scope.getCurrentScope().getSingleton(FastCheckService.class);
        List filters = this.getStandardChangelogIteratorFilters(database, contexts, labelExpression);

        if (fastCheck.isUpToDateFastCheck(filters, database, databaseChangeLog, contexts, labelExpression)) {
            Scope.getCurrentScope().getUI().sendMessage(DATABASE_UP_TO_DATE_MESSAGE);
            StatusVisitor statusVisitor = getStatusVisitor(commandScope, database, contexts, labelExpression, databaseChangeLog);
            UpdateSummaryEnum showSummary = getShowSummary(commandScope);
            UpdateSummaryOutputEnum showSummaryOutput = getShowSummaryOutput(commandScope);
            ShowSummaryUtil.showUpdateSummary(databaseChangeLog, showSummary, showSummaryOutput, statusVisitor, outputStream, null);
            return true;
        }
        return false;
    }

    public void setFastCheckEnabled(boolean fastCheckEnabled) {
        isFastCheckEnabled = fastCheckEnabled;
    }


    /**
     * Generate post update log messages
     * @param rowsAffected # of rows affected
     * @param exceptionChangeSets list of changesets that failed
     */
    public void postUpdateLog(int rowsAffected, List exceptionChangeSets) {
    }

    /**
     * Generic method called by all Update commands that actually apply the change to the database (ie !update-sql)
     * @param rowsAffected # of rows affected
     * @param exceptionChangeSets list of changesets that failed
     * @param messageWithRowCount message to display when rowsAffected > -1
     * @param messageWithoutRowCount message to display when rowsAffected == -1
     */
    protected void postUpdateLogForActualUpdate(int rowsAffected, List exceptionChangeSets, String messageWithRowCount, String messageWithoutRowCount) {
        if (exceptionChangeSets != null && !exceptionChangeSets.isEmpty()) {
            Scope.getCurrentScope().getUI().sendMessage("Errors encountered while deploying the following changesets: ");
            for (ChangeSet changeSet : exceptionChangeSets) {
                Scope.getCurrentScope().getUI().sendMessage("     " + changeSet.toString(false));
            }
            Scope.getCurrentScope().getUI().sendMessage("For more information use the --log-level flag.\n");
        }
        if (rowsAffected > -1) {
            Scope.getCurrentScope().getUI().sendMessage(String.format(messageWithRowCount, rowsAffected));
        } else {
            Scope.getCurrentScope().getUI().sendMessage(messageWithoutRowCount);
        }
    }


    /**
     * @deprecated Use {@link #postUpdateLog(int, List)} instead
     */
    @Deprecated
    public void postUpdateLog(int rowsAffected) {
        this.postUpdateLog(rowsAffected, Collections.emptyList());
    }

    protected void setDBLock(boolean locked) {
        isDBLocked = locked;
    }

    /**
     * Given a camel case command name array, format into a user-friendly command name string.
     * Ex: Given {"updateToTag"}, produces "Update To Tag"
     *
     * @param commandName the command name to reformat
     * @return the formatted command name
     */
    private String getFormattedCommandName(String[] commandName) {
        return Arrays.stream(commandName)
                .filter(Objects::nonNull)
                .map(camelCaseName -> StringUtil.join(StringUtil.splitCamelCase(camelCaseName), " "))
                .map(StringUtil::upperCaseFirst)
                .collect(Collectors.joining(" "));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy