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

liquibase.util.ShowSummaryUtil Maven / Gradle / Ivy

The newest version!
package liquibase.util;

import liquibase.GlobalConfiguration;
import liquibase.Scope;
import liquibase.UpdateSummaryEnum;
import liquibase.UpdateSummaryOutputEnum;
import liquibase.changelog.ChangeLogIterator;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.ChangeSetStatus;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.changelog.filter.ChangeSetFilter;
import liquibase.changelog.filter.ChangeSetFilterResult;
import liquibase.changelog.filter.DbmsChangeSetFilter;
import liquibase.changelog.filter.ShouldRunChangeSetFilter;
import liquibase.changelog.visitor.ChangeExecListener;
import liquibase.changelog.visitor.DefaultChangeExecListener;
import liquibase.changelog.visitor.StatusVisitor;
import liquibase.exception.LiquibaseException;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.MdcObject;
import liquibase.logging.mdc.customobjects.UpdateSummary;
import liquibase.report.ShowSummaryGenerator;
import liquibase.report.ShowSummaryGeneratorFactory;
import lombok.Data;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Methods to show a summary of change set counts after an update
 */
public class ShowSummaryUtil {

    /**
     * Show a summary of the changesets which were executed
     *
     * @param changeLog         The changelog used in this update
     * @param showSummary       Flag to control whether or not we show the summary
     * @param showSummaryOutput Flag to control where we show the summary
     * @param statusVisitor     The StatusVisitor used to determine statuses
     * @param outputStream      The OutputStream to use for the summary
     * @throws LiquibaseException Thrown by this method
     * @throws IOException        Thrown by this method
     */
    @Deprecated
    public static void showUpdateSummary(DatabaseChangeLog changeLog, UpdateSummaryEnum showSummary, UpdateSummaryOutputEnum showSummaryOutput, StatusVisitor statusVisitor, OutputStream outputStream)
            throws LiquibaseException, IOException {
        showUpdateSummary(changeLog, showSummary, showSummaryOutput, statusVisitor, outputStream, null);
    }

    /**
     * Show a summary of the changesets which were executed
     *
     * @param changeLog         The changelog used in this update
     * @param showSummary       Flag to control whether or not we show the summary
     * @param showSummaryOutput Flag to control where we show the summary
     * @param statusVisitor     The StatusVisitor used to determine statuses
     * @param outputStream      The OutputStream to use for the summary
     * @throws LiquibaseException Thrown by this method
     * @throws IOException        Thrown by this method
     */
    public static void showUpdateSummary(DatabaseChangeLog changeLog, UpdateSummaryEnum showSummary, UpdateSummaryOutputEnum showSummaryOutput, StatusVisitor statusVisitor, OutputStream outputStream, ChangeLogIterator runChangeLogIterator)
            throws LiquibaseException, IOException {
        buildSummaryDetails(changeLog, showSummary, showSummaryOutput, statusVisitor, outputStream, runChangeLogIterator, null);
    }

    /**
     * Show a summary of the changesets which were executed AND return an object with the records of what has happened.
     *
     * @param changeLog          The changelog used in this update
     * @param showSummary        Flag to control whether we show the summary
     * @param showSummaryOutput  Flag to control where we show the summary
     * @param statusVisitor      The StatusVisitor used to determine statuses
     * @param outputStream       The OutputStream to use for the summary
     * @return the details of the update summary
     * @throws LiquibaseException Thrown by this method
     * @throws IOException        Thrown by this method
     * @deprecated use {@link ShowSummaryUtil#buildSummaryDetails(DatabaseChangeLog, UpdateSummaryEnum, UpdateSummaryOutputEnum, StatusVisitor, OutputStream, ChangeLogIterator, ChangeExecListener)} instead.
     */
    @Deprecated
    public static UpdateSummaryDetails buildSummaryDetails(DatabaseChangeLog changeLog, UpdateSummaryEnum showSummary, UpdateSummaryOutputEnum showSummaryOutput, StatusVisitor statusVisitor, OutputStream outputStream, ChangeLogIterator runChangeLogIterator) throws LiquibaseException, IOException {
        return buildSummaryDetails(changeLog, showSummary, showSummaryOutput, statusVisitor,outputStream, runChangeLogIterator, null);
    }

    /**
     * Show a summary of the changesets which were executed AND return an object with the records of what has happened.
     *
     * @param changeLog          The changelog used in this update
     * @param showSummary        Flag to control whether we show the summary
     * @param showSummaryOutput  Flag to control where we show the summary
     * @param statusVisitor      The StatusVisitor used to determine statuses
     * @param outputStream       The OutputStream to use for the summary
     * @param changeExecListener
     * @return the details of the update summary
     * @throws LiquibaseException Thrown by this method
     * @throws IOException        Thrown by this method
     */
    public static UpdateSummaryDetails buildSummaryDetails(DatabaseChangeLog changeLog, UpdateSummaryEnum showSummary, UpdateSummaryOutputEnum showSummaryOutput, StatusVisitor statusVisitor, OutputStream outputStream, ChangeLogIterator runChangeLogIterator, ChangeExecListener changeExecListener)
            throws LiquibaseException, IOException {
        //
        // Check the global flag to turn the summary off
        //
        if (showSummary == null || showSummary == UpdateSummaryEnum.OFF) {
            return null;
        }

        //
        // Obtain two lists:  the list of filtered change sets that
        // The StatusVisitor discovered, and also any change sets which
        // were skipped during parsing, i.e. they had mismatched DBMS values
        //
        List denied = statusVisitor.getChangeSetsToSkip();
        List skippedChangeSets = changeLog.getSkippedChangeSets();
        List skippedBecauseOfLicenseChangeSets = changeLog.getSkippedBecauseOfLicenseChangeSets();

        //
        // Filter the skipped list to remove changes which were:
        // Previously run
        // After the tag
        // After the count value
        //
        List filterDenied =
                denied.stream()
                        .filter(status -> status.getFilterResults()
                                .stream().anyMatch(result -> result.getFilter() != ShouldRunChangeSetFilter.class))
                        .collect(Collectors.toList());

        ShowSummaryGeneratorFactory showSummaryGeneratorFactory = Scope.getCurrentScope().getSingleton(ShowSummaryGeneratorFactory.class);
        ShowSummaryGenerator showSummaryGenerator = showSummaryGeneratorFactory.getShowSummaryGenerator();
        List additionalChangeSetStatus = showSummaryGenerator.getAllAdditionalChangeSetStatus(runChangeLogIterator);

        //
        // Only show the summary
        //
        UpdateSummaryDetails summaryDetails = showSummary(changeLog, statusVisitor, skippedChangeSets, skippedBecauseOfLicenseChangeSets, filterDenied, outputStream, showSummaryOutput, runChangeLogIterator, changeExecListener);
        summaryDetails.getSummary().setValue(showSummary.toString());
        boolean shouldPrintDetailTable = showSummary != UpdateSummaryEnum.SUMMARY && (!skippedChangeSets.isEmpty() || !denied.isEmpty() || !additionalChangeSetStatus.isEmpty());

        // Show the details too
        FilteredChanges filteredChanges = showDetailTable(skippedChangeSets, skippedBecauseOfLicenseChangeSets, filterDenied, outputStream, shouldPrintDetailTable, showSummaryOutput, additionalChangeSetStatus, runChangeLogIterator);
        summaryDetails.getSummary().setSkipped(filteredChanges.getMdcSkipCounts());
        summaryDetails.setSkipped(filteredChanges.getSkippedChangesetsMessage());
        try (MdcObject updateSummaryMdcObject = Scope.getCurrentScope().addMdcValue(MdcKey.UPDATE_SUMMARY, summaryDetails.getSummary())) {
            Scope.getCurrentScope().getLog(ShowSummaryUtil.class).info("Update summary generated");
        }
        return summaryDetails;
    }

    //
    // Show the details
    //
    private static FilteredChanges showDetailTable(List skippedChangeSets,
                                                   List skippedBecauseOfLicenseChangeSets,
                                                   List filterDenied,
                                                   OutputStream outputStream,
                                                   boolean shouldPrintDetailTable,
                                                   UpdateSummaryOutputEnum showSummaryOutput,
                                                   List additionalChangesets,
                                                   ChangeLogIterator runChangeLogIterator)
            throws IOException, LiquibaseException {
        String totalSkippedMdcKey = "totalSkipped";
        //
        // Nothing to do
        //
        if (filterDenied.isEmpty() && skippedChangeSets.isEmpty() && additionalChangesets.isEmpty()) {
            return new FilteredChanges(new TreeMap<>(Collections.singletonMap(totalSkippedMdcKey, 0)), new LinkedHashMap<>());
        }
        List columnHeaders = new ArrayList<>();
        columnHeaders.add("Changeset Info");
        columnHeaders.add("Reason Skipped");
        List> table = new ArrayList<>();
        table.add(columnHeaders);
        SortedMap mdcSkipCounts = new TreeMap<>();
        mdcSkipCounts.put(totalSkippedMdcKey, skippedChangeSets.size() + filterDenied.size());

        List finalList = createFinalStatusList(skippedChangeSets, skippedBecauseOfLicenseChangeSets, filterDenied, mdcSkipCounts);
        ShowSummaryGeneratorFactory showSummaryGeneratorFactory = Scope.getCurrentScope().getSingleton(ShowSummaryGeneratorFactory.class);
        ShowSummaryGenerator showSummaryGenerator = showSummaryGeneratorFactory.getShowSummaryGenerator();
        finalList.addAll(showSummaryGenerator.getAllAdditionalChangeSetStatus(runChangeLogIterator));

        finalList.sort((o1, o2) -> {
            ChangeSet c1 = o1.getChangeSet();
            ChangeSet c2 = o2.getChangeSet();
            int order1 = determineOrderInChangelog(c1);
            int order2 = determineOrderInChangelog(c2);
            return Integer.compare(order1, order2);
        });

        // Filtered because of labels or context
        Map filteredChangesets = new LinkedHashMap<>();
        List skippedMessages = new ArrayList<>();
        for (ChangeSetStatus changeSetStatus : finalList) {
            AtomicBoolean flag = new AtomicBoolean(true);
            StringBuilder builder = new StringBuilder();
            changeSetStatus.getFilterResults().forEach(filterResult -> {
                if (filterResult.getFilter() != null && !filterResult.getFilter().isAssignableFrom(ShouldNotCountAsSkipChangesetFilter.class)) {
                    String displayName = filterResult.getMdcName();
                    mdcSkipCounts.merge(displayName, 1, Integer::sum);
                }
                String skippedMessage = String.format("   '%s' : %s", changeSetStatus.getChangeSet().toString(), filterResult.getMessage());
                skippedMessages.add(skippedMessage);
                if (!flag.get()) {
                    builder.append(System.lineSeparator());
                }
                builder.append(filterResult.getMessage());
                flag.set(false);
            });
            List outputRow = new ArrayList<>();
            outputRow.add(changeSetStatus.getChangeSet().toString());
            outputRow.add(builder.toString());
            filteredChangesets.put(changeSetStatus.getChangeSet(), builder.toString());
            table.add(outputRow);
        }

        if (shouldPrintDetailTable) {
            switch (showSummaryOutput) {
                case CONSOLE:
                    printDetailTable(table, outputStream);
                    break;
                case LOG:
                    skippedMessages.forEach(ShowSummaryUtil::writeToLog);
                    break;
                default:
                    printDetailTable(table, outputStream);
                    skippedMessages.forEach(ShowSummaryUtil::writeToLog);
            }
        }
        return new FilteredChanges(mdcSkipCounts, filteredChangesets);
    }


    /**
     * Internal use only filter.
     */
    public interface ShouldNotCountAsSkipChangesetFilter extends ChangeSetFilter {

    }

    /**
     *
     * This class is internally used to generate the summary line
     *
     */
    private static class SkippedBecauseOfLicenseFilter implements ChangeSetFilter {
        public static final String MDC_NAME = "skippedBecauseOfLicense";
        public static final String DISPLAY_NAME = "Skipped because of license";

        @Override
        public ChangeSetFilterResult accepts(ChangeSet changeSet) {
            return null;
        }

        @Override
        public String getMdcName() {
            return ChangeSetFilter.super.getMdcName();
        }

        @Override
        public String getDisplayName() {
            return ChangeSetFilter.super.getDisplayName();
        }
    }
    private static void printDetailTable(List> table, OutputStream outputStream) throws IOException, LiquibaseException {
        List widths = new ArrayList<>();
        widths.add(60);
        widths.add(40);
        Writer writer = createOutputWriter(outputStream);
        TableOutput.formatOutput(table, widths, true, writer);
    }

    //
    // Create a final list of changesets to be displayed
    //
    private static List createFinalStatusList(List skippedChangeSets, List skippedBecauseOfLicenseChangeSets, List filterDenied, SortedMap mdcSkipCounts) {
        //
        // Add skipped during changelog parsing to the final list
        //
        List finalList = new ArrayList<>(filterDenied);
        skippedChangeSets.forEach(skippedChangeSet -> {
            String dbmsList = String.format("'%s'", StringUtils.join(skippedChangeSet.getDbmsSet(), ", "));
            String mismatchMessage = String.format("mismatched DBMS value of %s", dbmsList);
            ChangeSetStatus changeSetStatus = new ChangeSetStatus(skippedChangeSet);
            ChangeSetFilterResult filterResult = new ChangeSetFilterResult(false, mismatchMessage, DbmsChangeSetFilter.class, DbmsChangeSetFilter.MDC_NAME, DbmsChangeSetFilter.DISPLAY_NAME);
            changeSetStatus.setFilterResults(Collections.singleton(filterResult));
            finalList.add(changeSetStatus);
        });
        skippedBecauseOfLicenseChangeSets.forEach(skippedChangeSet -> {
            String mismatchMessage = "skipped because of license";
            ChangeSetStatus changeSetStatus = new ChangeSetStatus(skippedChangeSet);
            ChangeSetFilterResult filterResult =
               new ChangeSetFilterResult(false, mismatchMessage, SkippedBecauseOfLicenseFilter.class, SkippedBecauseOfLicenseFilter.MDC_NAME, SkippedBecauseOfLicenseFilter.DISPLAY_NAME);
            changeSetStatus.setFilterResults(Collections.singleton(filterResult));
            finalList.add(changeSetStatus);
        });
        return finalList;
    }

    //
    // Determine the change set's order in the changelog
    //
    private static int determineOrderInChangelog(ChangeSet changeSetToMatch) {
        DatabaseChangeLog changeLog = changeSetToMatch.getChangeLog();
        int order = 0;
        for (ChangeSet changeSet : changeLog.getChangeSets()) {
            if (changeSet == changeSetToMatch) {
                return order;
            }
            order++;
        }
        return -1;
    }

    //
    // Show the summary list
    //
    private static UpdateSummaryDetails showSummary(DatabaseChangeLog changeLog,
                                                    StatusVisitor statusVisitor,
                                                    List skippedChangeSets,
                                                    List skippedBecauseOfLicenseChangeSets,
                                                    List filterDenied,
                                                    OutputStream outputStream,
                                                    UpdateSummaryOutputEnum showSummaryOutput,
                                                    ChangeLogIterator runChangeLogIterator,
                                                    ChangeExecListener changeExecListener) throws LiquibaseException {
        StringBuilder builder = new StringBuilder();
        builder.append(System.lineSeparator());
        int skipped = skippedChangeSets.size();
        int skippedBecauseOfLicense = skippedBecauseOfLicenseChangeSets.size();
        int filtered = filterDenied.size();
        int totalAccepted = calculateAccepted(statusVisitor, changeExecListener, runChangeLogIterator);
        int totalPreviouslyRun = calculatePreviouslyRun(statusVisitor);
        int totalInChangelog = CollectionUtil.createIfNull(changeLog.getChangeSets()).size() + CollectionUtil.createIfNull(changeLog.getSkippedChangeSets()).size();
        UpdateSummary updateSummaryMdc = new UpdateSummary(null, totalAccepted, totalPreviouslyRun, null, totalInChangelog);

        String message = "UPDATE SUMMARY";
        builder.append(message);
        builder.append(System.lineSeparator());

        message = String.format("Run:                     %6d", totalAccepted);
        builder.append(message);
        builder.append(System.lineSeparator());

        message = String.format("Previously run:          %6d", totalPreviouslyRun);
        builder.append(message);
        builder.append(System.lineSeparator());

        message = String.format("Filtered out:            %6d", filtered + skipped + skippedBecauseOfLicense);
        builder.append(message);
        builder.append(System.lineSeparator());

        ShowSummaryGeneratorFactory showSummaryGeneratorFactory = Scope.getCurrentScope().getSingleton(ShowSummaryGeneratorFactory.class);
        ShowSummaryGenerator showSummaryGenerator = showSummaryGeneratorFactory.getShowSummaryGenerator();
        showSummaryGenerator.appendAdditionalSummaryMessages(builder, runChangeLogIterator);

        message = "-------------------------------";
        builder.append(message);
        builder.append(System.lineSeparator());

        message = String.format("Total change sets:       %6d", totalInChangelog);
        builder.append(message);
        builder.append(System.lineSeparator());
        builder.append(System.lineSeparator());

        final Map filterSummaryMap = new LinkedHashMap<>();
        List finalList = createFinalStatusList(skippedChangeSets, skippedBecauseOfLicenseChangeSets, filterDenied, null);
        finalList.forEach(status -> {
            status.getFilterResults().forEach(result -> {
                if (!result.isAccepted()) {
                    String displayName = result.getDisplayName();
                    filterSummaryMap.merge(displayName, 1, Integer::sum);
                }
            });
        });

        if (!filterSummaryMap.isEmpty()) {
            message = "FILTERED CHANGE SETS SUMMARY";
            builder.append(System.lineSeparator());
            builder.append(message);
            builder.append(System.lineSeparator());
            filterSummaryMap.forEach((filterDisplayName, count) -> {
                String filterSummaryDetailMessage = String.format("%-18s       %6d",
                        filterDisplayName + ":", count);
                builder.append(filterSummaryDetailMessage);
                builder.append(System.lineSeparator());
            });
            builder.append(System.lineSeparator());
        }

        String outputMessage = builder.toString();
        writeMessage(outputMessage, showSummaryOutput, outputStream);
        UpdateSummaryDetails updateSummaryDetails = new UpdateSummaryDetails();
        updateSummaryDetails.setSummary(updateSummaryMdc);
        updateSummaryDetails.setOutput(outputMessage);
        return updateSummaryDetails;
    }

    private static int calculatePreviouslyRun(StatusVisitor statusVisitor) {
        return (int) statusVisitor.getStatuses().stream().filter(
                s -> s.getFilterResults().stream().anyMatch(
                        fr -> fr.getFilter().isAssignableFrom(ShouldRunChangeSetFilter.class) && !fr.isAccepted() && fr.getMessage().equals(ShouldRunChangeSetFilter.CHANGESET_ALREADY_RAN_MESSAGE)
                )
        ).count();
    }

    /**
     * Calculate the accepted number of changesets.
     * The status visitor provides a list of changesets that are expected to execute, not the actual list of
     * executed changesets. We retain this code despite its inaccuracy for backwards compatibility, in case
     * a change exec listener is not provided.
     */
    private static int calculateAccepted(StatusVisitor statusVisitor, ChangeExecListener changeExecListener, ChangeLogIterator runChangeLogIterator) {
        int ran = statusVisitor.getChangeSetsToRun().size();
        if (changeExecListener instanceof DefaultChangeExecListener) {
            ran = ((DefaultChangeExecListener) changeExecListener).getDeployedChangeSets().size();
            ran -= (int) runChangeLogIterator.getExceptionChangeSets().stream().filter(changeSet -> BooleanUtils.isFalse(changeSet.getFailOnError())).count();
        }
        return ran;
    }

    //
    // Create a Writer to display the summary
    //
    private static Writer createOutputWriter(OutputStream outputStream) throws IOException {
        String charsetName = GlobalConfiguration.OUTPUT_FILE_ENCODING.getCurrentValue();
        return new OutputStreamWriter(outputStream, charsetName);
    }

    private static void writeMessage(String message, UpdateSummaryOutputEnum showSummaryOutput, OutputStream outputStream) throws LiquibaseException {
        switch (showSummaryOutput) {
            case CONSOLE:
                writeToOutput(outputStream, message);
                break;
            case LOG:
                writeToLog(message);
                break;
            default:
                writeToOutput(outputStream, message);
                writeToLog(message);
        }
    }

    private static void writeToOutput(OutputStream outputStream, String message) throws LiquibaseException {
        try {
            Writer writer = createOutputWriter(outputStream);
            writer.append(message);
            writer.flush();
        } catch (IOException ioe) {
            throw new LiquibaseException(ioe);
        }
    }

    private static void writeToLog(String message) {
        Stream.of(message.split(System.lineSeparator()))
                .filter(s -> !StringUtil.isWhitespace(s))
                .forEach(Scope.getCurrentScope().getLog(ShowSummaryUtil.class)::info);
    }

    @Data
    private static class FilteredChanges {
        private final SortedMap mdcSkipCounts;
        private final Map skippedChangesetsMessage;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy