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

liquibase.changelog.DatabaseChangeLog Maven / Gradle / Ivy

There is a newer version: 4.30.0
Show newest version
package liquibase.changelog;

import liquibase.*;
import liquibase.change.visitor.ChangeVisitor;
import liquibase.change.visitor.ChangeVisitorFactory;
import liquibase.changelog.filter.ContextChangeSetFilter;
import liquibase.changelog.filter.DbmsChangeSetFilter;
import liquibase.changelog.filter.LabelChangeSetFilter;
import liquibase.changelog.visitor.ValidatingVisitor;
import liquibase.changeset.ChangeSetService;
import liquibase.changeset.ChangeSetServiceFactory;
import liquibase.database.Database;
import liquibase.database.DatabaseList;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.exception.*;
import liquibase.logging.Logger;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.MdcValue;
import liquibase.logging.mdc.customobjects.DuplicateChangesets;
import liquibase.logging.mdc.customobjects.MdcChangeset;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.ChangeLogParserConfiguration;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.precondition.Conditional;
import liquibase.precondition.Precondition;
import liquibase.precondition.core.PreconditionContainer;
import liquibase.resource.Resource;
import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.LiquibaseService;
import liquibase.util.FileUtil;
import liquibase.util.StringUtil;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;


/**
 * Encapsulates the information stored in the change log XML file.
 */
public class DatabaseChangeLog implements Comparable, Conditional {
    private static final ThreadLocal ROOT_CHANGE_LOG = new ThreadLocal<>();
    private static final ThreadLocal PARENT_CHANGE_LOG = new ThreadLocal<>();
    private static final Logger LOG = Scope.getCurrentScope().getLog(DatabaseChangeLog.class);
    private static final Pattern SLASH_PATTERN = Pattern.compile("^/");
    private static final Pattern DOUBLE_BACK_SLASH_PATTERN = Pattern.compile("\\\\");
    private static final Pattern NO_LETTER_PATTERN = Pattern.compile("^[a-zA-Z]:");
    public static final String SEEN_CHANGELOGS_PATHS_SCOPE_KEY = "SEEN_CHANGELOG_PATHS";

    private final PreconditionContainer preconditionContainer = new GlobalPreconditionContainer();
    private String physicalFilePath;
    private String logicalFilePath;
    private ObjectQuotingStrategy objectQuotingStrategy;

    private List changeVisitors = new ArrayList<>();
    private final List changeSets = new ArrayList<>();
    private final List skippedChangeSets = new ArrayList<>();
    private ChangeLogParameters changeLogParameters;

    private RuntimeEnvironment runtimeEnvironment;

    private DatabaseChangeLog rootChangeLog = ROOT_CHANGE_LOG.get();

    private DatabaseChangeLog parentChangeLog = PARENT_CHANGE_LOG.get();

    private ContextExpression contextFilter;

    private ContextExpression includeContextFilter;

    private Labels includeLabels;
    private boolean includeIgnore;

    public DatabaseChangeLog() {
    }

    public DatabaseChangeLog(String physicalFilePath) {
        this.physicalFilePath = physicalFilePath;
    }

    public void setRootChangeLog(DatabaseChangeLog rootChangeLog) {
        this.rootChangeLog = rootChangeLog;
    }

    public DatabaseChangeLog getRootChangeLog() {
        return (rootChangeLog != null) ? rootChangeLog : this;
    }

    public void setParentChangeLog(DatabaseChangeLog parentChangeLog) {
        this.parentChangeLog = parentChangeLog;
    }

    public DatabaseChangeLog getParentChangeLog() {
        return parentChangeLog;
    }

    public RuntimeEnvironment getRuntimeEnvironment() {
        return runtimeEnvironment;
    }

    public void setRuntimeEnvironment(RuntimeEnvironment runtimeEnvironment) {
        this.runtimeEnvironment = runtimeEnvironment;
    }

    @Override
    public PreconditionContainer getPreconditions() {
        return preconditionContainer;
    }

    @Override
    public void setPreconditions(PreconditionContainer precond) {
        this.preconditionContainer.addNestedPrecondition(precond);
    }


    public ChangeLogParameters getChangeLogParameters() {
        return changeLogParameters;
    }

    public void setChangeLogParameters(ChangeLogParameters changeLogParameters) {
        this.changeLogParameters = changeLogParameters;
    }

    public String getPhysicalFilePath() {
        return physicalFilePath;
    }

    public void setPhysicalFilePath(String physicalFilePath) {
        this.physicalFilePath = physicalFilePath;
    }

    public String getLogicalFilePath() {
        String returnPath = logicalFilePath;
        if (logicalFilePath == null) {
            returnPath = physicalFilePath;
        }
        if (returnPath == null) {
            return null;
        }
        String path = DOUBLE_BACK_SLASH_PATTERN.matcher(returnPath).replaceAll("/");
        return SLASH_PATTERN.matcher(path).replaceFirst("");
    }

    public void setLogicalFilePath(String logicalFilePath) {
        this.logicalFilePath = logicalFilePath;
    }

    public String getFilePath() {
        if (logicalFilePath == null) {
            return physicalFilePath;
        } else {
            return getLogicalFilePath();
        }
    }

    public ObjectQuotingStrategy getObjectQuotingStrategy() {
        return objectQuotingStrategy;
    }

    public void setObjectQuotingStrategy(ObjectQuotingStrategy objectQuotingStrategy) {
        this.objectQuotingStrategy = objectQuotingStrategy;
    }

    /**
     * @deprecated use {@link #getContextFilter()}
     */
    public ContextExpression getContexts() {
        return getContextFilter();
    }

    /**
     * @deprecated use {@link #setContextFilter(ContextExpression)}
     */
    public void setContexts(ContextExpression contexts) {
        setContextFilter(contexts);
    }

    public ContextExpression getContextFilter() {
        return contextFilter;
    }

    public void setContextFilter(ContextExpression contextFilter) {
        this.contextFilter = contextFilter;
    }

    public ContextExpression getIncludeContextFilter() {
        return includeContextFilter;
    }

    /**
     * @deprecated Correct version is {@link #setIncludeLabels(Labels)}. Kept for backwards compatibility.
     */
    public void setIncludeLabels(LabelExpression labels) {
        this.includeLabels = new Labels(labels.toString());
    }

    public void setIncludeLabels(Labels labels) {
        this.includeLabels = labels;
    }

    public Labels getIncludeLabels() {
        return includeLabels;
    }

    public void setIncludeIgnore(boolean ignore) {
        this.includeIgnore = ignore;
    }

    public boolean isIncludeIgnore() {
        return this.includeIgnore;
    }

    /**
     * @deprecated use {@link #setIncludeContextFilter(ContextExpression)}
     */
    public void setIncludeContexts(ContextExpression includeContexts) {
        setIncludeContextFilter(includeContexts);
    }

    public void setIncludeContextFilter(ContextExpression includeContextFilter) {
        this.includeContextFilter = includeContextFilter;
    }

    @Override
    public String toString() {
        return getFilePath();
    }

    @Override
    public int compareTo(DatabaseChangeLog o) {
        return getFilePath().compareTo(o.getFilePath());
    }

    public List getChangeVisitors(){
        return changeVisitors;
    }
    public ChangeSet getChangeSet(String path, String author, String id) {
        final List possibleChangeSets = getChangeSets(path, author, id);
        if (possibleChangeSets.isEmpty()) {
            return null;
        }
        return possibleChangeSets.get(0);
    }

    public List getChangeSets(String path, String author, String id) {
        final ArrayList changeSetsToReturn = new ArrayList<>();
        final String normalizedPath = normalizePath(path);
        if (normalizedPath != null) {
            for (ChangeSet changeSet : this.changeSets) {
                if (changeSet.getAuthor().equalsIgnoreCase(author) && changeSet.getId().equalsIgnoreCase(id) && isDbmsMatch(changeSet.getDbmsSet())) {
                    final String changesetNormalizedPath = normalizePath(changeSet.getFilePath());
                    if (changesetNormalizedPath != null && changesetNormalizedPath.equalsIgnoreCase(normalizedPath)) {
                        changeSetsToReturn.add(changeSet);
                    }
                }
            }
        }
        return changeSetsToReturn;
    }

    public List getChangeSets() {
        return changeSets;
    }

    public List getSkippedChangeSets() {
        return skippedChangeSets;
    }

    public void addChangeSet(ChangeSet changeSet) {
        if (changeSet.getRunOrder() == null) {
            ListIterator it = this.changeSets.listIterator(this.changeSets.size());
            boolean added = false;
            while (it.hasPrevious() && !added) {
                if (!"last".equals(it.previous().getRunOrder())) {
                    it.next();
                    it.add(changeSet);
                    added = true;
                }
            }
            if (!added) {
                it.add(changeSet);
            }

        } else if ("first".equals(changeSet.getRunOrder())) {
            ListIterator it = this.changeSets.listIterator();
            boolean added = false;
            while (it.hasNext() && !added) {
                if (!"first".equals(it.next().getRunOrder())) {
                    it.previous();
                    it.add(changeSet);
                    added = true;
                }
            }
            if (!added) {
                this.changeSets.add(changeSet);
            }
        } else if ("last".equals(changeSet.getRunOrder())) {
            this.changeSets.add(changeSet);
        } else {
            throw new UnexpectedLiquibaseException("Unknown runOrder: " + changeSet.getRunOrder());
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if ((o == null) || (getClass() != o.getClass())) {
            return false;
        }

        DatabaseChangeLog that = (DatabaseChangeLog) o;

        return getFilePath().equals(that.getFilePath());

    }

    @Override
    public int hashCode() {
        return getFilePath().hashCode();
    }

    public void validate(Database database, String... contexts) throws LiquibaseException {
        this.validate(database, new Contexts(contexts), new LabelExpression());
    }

    public void validate(Database database, Contexts contexts, LabelExpression labelExpression)
            throws LiquibaseException {

        database.setObjectQuotingStrategy(objectQuotingStrategy);

        ChangeLogIterator logIterator = new ChangeLogIterator(
                this,
                new DbmsChangeSetFilter(database),
                new ContextChangeSetFilter(contexts),
                new LabelChangeSetFilter(labelExpression)
        );

        ValidatingVisitor validatingVisitor = new ValidatingVisitor(database.getRanChangeSetList());
        validatingVisitor.validate(database, this);
        logIterator.run(validatingVisitor, new RuntimeEnvironment(database, contexts, labelExpression));

        final Logger log = Scope.getCurrentScope().getLog(getClass());
        for (String message : validatingVisitor.getWarnings().getMessages()) {
            log.warning(message);
        }

        if (!validatingVisitor.validationPassed()) {
            Scope.getCurrentScope().addMdcValue(MdcKey.DEPLOYMENT_OUTCOME, MdcValue.COMMAND_FAILED);
            List duplicateChangesetsMdc = validatingVisitor.getDuplicateChangeSets().stream().map(MdcChangeset::fromChangeset).collect(Collectors.toList());
            Scope.getCurrentScope().addMdcValue(MdcKey.DUPLICATE_CHANGESETS, new DuplicateChangesets(duplicateChangesetsMdc));
            Scope.getCurrentScope().getLog(getClass()).info("Change failed validation!");
            throw new ValidationFailedException(validatingVisitor);
        }
    }

    public ChangeSet  getChangeSet(RanChangeSet ranChangeSet) {
        final ChangeSet changeSet = getChangeSet(ranChangeSet.getChangeLog(), ranChangeSet.getAuthor(), ranChangeSet.getId());
        if (changeSet != null) {
            changeSet.setStoredFilePath(ranChangeSet.getStoredChangeLog());
        }
        return changeSet;
    }

    public List getChangeSets(RanChangeSet ranChangeSet) {
        List changesets = getChangeSets(ranChangeSet.getChangeLog(), ranChangeSet.getAuthor(), ranChangeSet.getId());
        changesets.forEach(c -> c.setStoredFilePath(ranChangeSet.getStoredChangeLog()));
        return changesets;
    }

    public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException, SetupException {
        setLogicalFilePath(parsedNode.getChildValue(null, "logicalFilePath", String.class));

        String context = parsedNode.getChildValue(null, "contextFilter", String.class);
        if (context == null) {
            context = parsedNode.getChildValue(null, "context", String.class);
        }

        setContextFilter(new ContextExpression(context));
        String nodeObjectQuotingStrategy = parsedNode.getChildValue(null, "objectQuotingStrategy", String.class);
        if (nodeObjectQuotingStrategy != null) {
            setObjectQuotingStrategy(ObjectQuotingStrategy.valueOf(nodeObjectQuotingStrategy));
        }
        for (ParsedNode childNode : parsedNode.getChildren()) {
            handleChildNode(childNode, resourceAccessor, new HashMap<>());
        }
    }

    protected void expandExpressions(ParsedNode parsedNode) throws UnknownChangeLogParameterException {
        if (changeLogParameters == null) {
            return;
        }
        try {
            Object value = parsedNode.getValue();
            if ((value instanceof String)) {
                parsedNode.setValue(changeLogParameters.expandExpressions(parsedNode.getValue(String.class), this));
            }

            List children = parsedNode.getChildren();
            if (children != null) {
                for (ParsedNode child : children) {
                    expandExpressions(child);
                }
            }
        } catch (ParsedNodeException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    protected void handleChildNode(ParsedNode node, ResourceAccessor resourceAccessor)
            throws ParsedNodeException, SetupException {
        handleChildNode(node, resourceAccessor, new HashMap<>());
    }

    protected void handleChildNode(ParsedNode node, ResourceAccessor resourceAccessor, Map nodeScratch)
            throws ParsedNodeException, SetupException {
        expandExpressions(node);
        String nodeName = node.getName();
        switch (nodeName) {
            case "changeSet":
                if (isDbmsMatch(node.getChildValue(null, "dbms", String.class))) {
                    this.addChangeSet(createChangeSet(node, resourceAccessor));
                } else {
                    handleSkippedChangeSet(node);
                }
                break;
            case "modifyChangeSets":
                ModifyChangeSets modifyChangeSets = createModifyChangeSets(node);
                nodeScratch = new HashMap<>();
                nodeScratch.put("modifyChangeSets", modifyChangeSets);
                for (ParsedNode modifyChildNode : node.getChildren()) {
                    handleChildNode(modifyChildNode, resourceAccessor, nodeScratch);
                }
                nodeScratch.remove("modifyChangeSets");
                break;
            case "include": {
                String path = node.getChildValue(null, "file", String.class);
                if (path == null) {
                    throw new UnexpectedLiquibaseException("No 'file' attribute on 'include'");
                }
                path = path.replace('\\', '/');
                Scope.getCurrentScope().addMdcValue(MdcKey.CHANGELOG_FILE, path);
                ContextExpression includeContextFilter = new ContextExpression(node.getChildValue(null, "contextFilter", String.class));
                if (includeContextFilter.isEmpty()) {
                    includeContextFilter = new ContextExpression(node.getChildValue(null, "context", String.class));
                }
                Labels labels = new Labels(node.getChildValue(null, "labels", String.class));
                Boolean ignore = node.getChildValue(null, "ignore", Boolean.class);
                try {
                    include(path,
                            node.getChildValue(null, "relativeToChangelogFile", false),
                            node.getChildValue(null, "errorIfMissing", true),
                            resourceAccessor,
                            includeContextFilter,
                            labels,
                            ignore,
                            OnUnknownFileFormat.FAIL,
                            (ModifyChangeSets) nodeScratch.get("modifyChangeSets"));
                } catch (LiquibaseException e) {
                    throw new SetupException(e);
                }
                break;
            }
            case "includeAll": {
                String path = node.getChildValue(null, "path", String.class);
                String resourceFilterDef = node.getChildValue(null, "filter", String.class);
                if (resourceFilterDef == null) {
                    resourceFilterDef = node.getChildValue(null, "resourceFilter", String.class);
                }
                IncludeAllFilter resourceFilter = null;
                if (resourceFilterDef != null) {
                    try {
                        resourceFilter = (IncludeAllFilter) Class.forName(resourceFilterDef).getConstructor().newInstance();
                    } catch (ReflectiveOperationException e) {
                        throw new SetupException(e);
                    }
                }

                String resourceComparatorDef = node.getChildValue(null, "resourceComparator", String.class);
                Comparator resourceComparator = null;
                if (resourceComparatorDef == null) {
                    resourceComparator = getStandardChangeLogComparator();
                } else {
                    try {
                        resourceComparator = (Comparator) Class.forName(resourceComparatorDef).getConstructor().newInstance();
                    } catch (ReflectiveOperationException e) {
                        //take default comparator
                        Scope.getCurrentScope().getLog(getClass()).info("no resourceComparator defined - taking default " +
                                "implementation");
                        resourceComparator = getStandardChangeLogComparator();
                    }
                }

                ContextExpression includeContextFilter = new ContextExpression(node.getChildValue(null, "contextFilter", String.class));
                if (includeContextFilter.isEmpty()) {
                    includeContextFilter = new ContextExpression(node.getChildValue(null, "context", String.class));
                }
                Labels labels = new Labels(node.getChildValue(null, "labels", String.class));
                Boolean ignore = node.getChildValue(null, "ignore", Boolean.class);
                if (ignore == null) {
                    ignore = false;
                }
                includeAll(path, node.getChildValue(null, "relativeToChangelogFile", false), resourceFilter,
                        node.getChildValue(null, "errorIfMissingOrEmpty", true),
                        resourceComparator,
                        resourceAccessor,
                        includeContextFilter,
                        labels,
                        ignore,
                        node.getChildValue(null, "minDepth", 1),
                        node.getChildValue(null, "maxDepth", Integer.MAX_VALUE),
                        node.getChildValue(null, "endsWithFilter", ""),
                        (ModifyChangeSets) nodeScratch.get("modifyChangeSets"));
                break;
            }
            case "preConditions": {
                PreconditionContainer parsedContainer = new PreconditionContainer();
                parsedContainer.load(node, resourceAccessor);
                this.preconditionContainer.addNestedPrecondition(parsedContainer);

                break;
            }
            case "removeChangeSetProperty": {
                List childNodes = node.getChildren();
                Optional changeNode = childNodes.stream().filter(n -> n.getName().equalsIgnoreCase("change")).findFirst();
                if(changeNode.isPresent()){
                    ChangeVisitor changeVisitor = ChangeVisitorFactory.getInstance().create((String) changeNode.get().getValue());
                    if(changeVisitor != null){
                        changeVisitor.load(node, resourceAccessor);
                        if(DatabaseList.definitionMatches(changeVisitor.getDbms(), changeLogParameters.getDatabase(), false)) {
                            //add changeVisitor to this changeLog only if the running database matches with one of the removeChangeSetProperty's dbms
                            getChangeVisitors().add(changeVisitor);
                        }
                    }
                }

                break;
            }
            case "property": {
                try {
                    String contextFilter = node.getChildValue(null, "contextFilter", String.class);
                    if (StringUtil.isEmpty(contextFilter)) {
                        contextFilter = node.getChildValue(null, "context", String.class);
                    }
                    String dbms = node.getChildValue(null, "dbms", String.class);
                    String labels = node.getChildValue(null, "labels", String.class);
                    Boolean global = node.getChildValue(null, "global", Boolean.class);
                    if (global == null) {
                        // okay behave like liquibase < 3.4 and set global == true
                        global = true;
                    }

                    String file = node.getChildValue(null, "file", String.class);
                    Boolean relativeToChangelogFile = node.getChildValue(null, "relativeToChangelogFile", Boolean.FALSE);
                    Boolean errorIfMissing = node.getChildValue(null, "errorIfMissing", Boolean.TRUE);
                    Resource resource;

                    if (file == null) {
                        // direct referenced property, no file
                        String name = node.getChildValue(null, "name", String.class);
                        String value = node.getChildValue(null, "value", String.class);

                        this.changeLogParameters.set(name, value, contextFilter, labels, dbms, global, this);
                    } else {
                        // get relative path if specified
                        if (relativeToChangelogFile) {
                            resource = resourceAccessor.get(this.getPhysicalFilePath()).resolveSibling(file);
                        } else {
                            resource = resourceAccessor.get(file);
                        }

                        // read properties from the file
                        Properties props = new Properties();
                        if (!resource.exists()) {
                            if (errorIfMissing) {
                                throw new UnexpectedLiquibaseException(FileUtil.getFileNotFoundMessage(file));
                            } else {
                                Scope.getCurrentScope().getLog(getClass()).warning(FileUtil.getFileNotFoundMessage(file));
                            }
                        } else {
                            try (InputStream propertiesStream = resource.openInputStream()) {
                                props.load(propertiesStream);

                                for (Map.Entry entry : props.entrySet()) {
                                    this.changeLogParameters.set(
                                            entry.getKey().toString(),
                                            entry.getValue().toString(),
                                            contextFilter,
                                            labels,
                                            dbms,
                                            global,
                                            this
                                    );
                                }
                            }
                        }
                    }
                } catch (IOException e) {
                    throw new ParsedNodeException(e);
                }

                break;
            }
            default:
                // we want to exclude child nodes that are not changesets or the other things
                // and avoid failing when encountering "child" nodes of the databaseChangeLog which are just
                // XML node attributes (like schemaLocation). If you don't understand, remove the if and run the tests
                // and look at the error output or review the "node" object here with a debugger.
                if (node.getChildren() != null && !node.getChildren().isEmpty()) {
                    throw new ParsedNodeException("Unexpected node found under databaseChangeLog: " + nodeName);
                }
        }
    }

    private ModifyChangeSets createModifyChangeSets(ParsedNode node) throws ParsedNodeException {
        ChangeSetServiceFactory factory = ChangeSetServiceFactory.getInstance();
        ChangeSetService service = factory.createChangeSetService();
        return service.createModifyChangeSets(node);
    }

    //
    // Handle a mismatched DBMS attribute, if necessary
    //
    private void handleSkippedChangeSet(ParsedNode node) throws ParsedNodeException {
        if (node.getChildValue(null, "dbms", String.class) == null) {
            return;
        }
        String id = node.getChildValue(null, "id", String.class);
        String author = node.getChildValue(null, "author", String.class);
        String filePath = StringUtil.trimToNull(node.getChildValue(null, "logicalFilePath", String.class));
        if (filePath == null) {
            filePath = getFilePath();
        } else {
            filePath = filePath.replace("\\\\", "/").replaceFirst("^/", "");
        }
        String dbmsList = node.getChildValue(null, "dbms", String.class);
        ChangeSet skippedChangeSet =
                new ChangeSet(id, author, false, false, filePath, null, dbmsList, this);
        skippedChangeSets.add(skippedChangeSet);
    }

    public boolean isDbmsMatch(String dbmsList) {
        return isDbmsMatch(DatabaseList.toDbmsSet(dbmsList));
    }

    public boolean isDbmsMatch(Set dbmsSet) {
        return dbmsSet == null
                || changeLogParameters == null
                || changeLogParameters.getValue("database.typeName", this) == null
                || DatabaseList.definitionMatches(dbmsSet, changeLogParameters.getValue("database.typeName", this).toString(), true);
    }

    /**
     * @deprecated use {@link DatabaseChangeLog#includeAll(String, boolean, IncludeAllFilter, boolean, Comparator, ResourceAccessor, ContextExpression, Labels, boolean, int, int)}
     */
    @Deprecated
    public void includeAll(String pathName,
                           boolean isRelativeToChangelogFile,
                           IncludeAllFilter resourceFilter,
                           boolean errorIfMissingOrEmpty,
                           Comparator resourceComparator,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           LabelExpression labelExpression,
                           boolean ignore)
            throws SetupException {
        Labels labels = null;
        if (labelExpression != null && !labelExpression.isEmpty()) {
            labels = new Labels(labelExpression.toString());
        }
        includeAll(pathName,
                isRelativeToChangelogFile,
                resourceFilter,
                errorIfMissingOrEmpty,
                resourceComparator,
                resourceAccessor,
                includeContextFilter,
                labels,
                ignore,
                0,
                Integer.MAX_VALUE);
    }

    public void includeAll(String pathName,
                           boolean isRelativeToChangelogFile,
                           IncludeAllFilter resourceFilter,
                           boolean errorIfMissingOrEmpty,
                           Comparator resourceComparator,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           Labels labels,
                           boolean ignore,
                           int minDepth,
                           int maxDepth)
            throws SetupException {
        includeAll(pathName, isRelativeToChangelogFile, resourceFilter, errorIfMissingOrEmpty, resourceComparator,
                   resourceAccessor, includeContextFilter, labels, ignore, minDepth, maxDepth, "", new ModifyChangeSets(null, null));
    }

    @Deprecated
    public void includeAll(String pathName,
                           boolean isRelativeToChangelogFile,
                           IncludeAllFilter resourceFilter,
                           boolean errorIfMissingOrEmpty,
                           Comparator resourceComparator,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           Labels labels,
                           boolean ignore,
                           int minDepth,
                           int maxDepth,
                           ModifyChangeSets modifyChangeSets)
            throws SetupException {
        includeAll(pathName, isRelativeToChangelogFile, resourceFilter, errorIfMissingOrEmpty, resourceComparator,
                resourceAccessor, includeContextFilter, labels, ignore, minDepth, maxDepth, "", modifyChangeSets);
    }

    public void includeAll(String pathName,
                           boolean isRelativeToChangelogFile,
                           IncludeAllFilter resourceFilter,
                           boolean errorIfMissingOrEmpty,
                           Comparator resourceComparator,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           Labels labels,
                           boolean ignore,
                           int minDepth,
                           int maxDepth,
                           String endsWithFilter,
                           ModifyChangeSets modifyChangeSets)
            throws SetupException {
        if (pathName == null) {
            throw new SetupException("No path attribute for includeAll");
        }
        SortedSet resources =
                findResources(pathName, isRelativeToChangelogFile, resourceFilter, errorIfMissingOrEmpty, resourceComparator, resourceAccessor, minDepth, maxDepth, endsWithFilter);
        if (resources.isEmpty() && errorIfMissingOrEmpty) {
            throw new SetupException(
                    "Could not find directory or directory was empty for includeAll '" + pathName + "'");
        }
        try {
            Set seenChangelogPaths = Scope.getCurrentScope().get(SEEN_CHANGELOGS_PATHS_SCOPE_KEY, new HashSet<>());
            Scope.child(Collections.singletonMap(SEEN_CHANGELOGS_PATHS_SCOPE_KEY, seenChangelogPaths), () -> {
                for (Resource resource : resources) {
                    Scope.getCurrentScope().getLog(getClass()).info("Reading resource: " + resource);
                    include(resource.getPath(), false, errorIfMissingOrEmpty, resourceAccessor, includeContextFilter,
                            labels, ignore, OnUnknownFileFormat.WARN, modifyChangeSets);
                }
            });
        } catch (Exception e) {
            throw new SetupException(e);
        }
    }

    @Deprecated
    public SortedSet findResources(
            String pathName,
            boolean isRelativeToChangelogFile,
            IncludeAllFilter resourceFilter,
            boolean errorIfMissingOrEmpty,
            Comparator resourceComparator,
            ResourceAccessor resourceAccessor,
            int minDepth,
            int maxDepth) throws SetupException {
        return this.findResources(pathName, isRelativeToChangelogFile, resourceFilter, errorIfMissingOrEmpty, resourceComparator,
                resourceAccessor, minDepth, maxDepth, "");
    }

    public SortedSet findResources(
                               String pathName,
                               boolean isRelativeToChangelogFile,
                               IncludeAllFilter resourceFilter,
                               boolean errorIfMissingOrEmpty,
                               Comparator resourceComparator,
                               ResourceAccessor resourceAccessor,
                               int minDepth,
                               int maxDepth,
                               String endsWithFilter
                               ) throws SetupException {
        try {
            if (pathName == null) {
                throw new SetupException("No path attribute for findResources");
            }

            String relativeTo = null;
            if (isRelativeToChangelogFile) {
                relativeTo = this.getPhysicalFilePath();
            }

            ResourceAccessor.SearchOptions searchOptions = initializeAndSetMinAndMaxDepth(minDepth, maxDepth);
            searchOptions.setTrimmedEndsWithFilter(endsWithFilter);

            List unsortedResources = null;
            Set seenChangelogPaths = Scope.getCurrentScope().get(SEEN_CHANGELOGS_PATHS_SCOPE_KEY, new HashSet<>());
            try {
                String path = fixPath(pathName, resourceAccessor, relativeTo);

                if (Boolean.TRUE.equals(ChangeLogParserConfiguration.ERROR_ON_CIRCULAR_INCLUDE_ALL.getCurrentValue())) {
                    if (seenChangelogPaths.contains(path)) {
                        throw new SetupException("Circular reference detected in '" + path + "'. Set " + ChangeLogParserConfiguration.ERROR_ON_CIRCULAR_INCLUDE_ALL.getKey() + " if you'd like to ignore this error.");
                    }
                }
                seenChangelogPaths.add(path);
                LOG.fine("includeAll for " + pathName);
                LOG.fine("Using file opener for includeAll: " + resourceAccessor.toString());

                unsortedResources = resourceAccessor.search(path, searchOptions);
            } catch (IOException e) {
                if (errorIfMissingOrEmpty) {
                    throw new IOException(String.format("Could not find/read changelogs from %s directory", pathName));
                }
            }
            SortedSet resources = new TreeSet<>((o1, o2) -> resourceComparator.compare(o1.getPath(), o2.getPath()));
            if (unsortedResources != null) {
                for (Resource resourcePath : unsortedResources) {
                    if ((resourceFilter == null) || resourceFilter.include(resourcePath.getPath())) {
                        resources.add(resourcePath);
                    }
                }
            }

            if (resources.isEmpty() && errorIfMissingOrEmpty) {
                throw new SetupException(
                        "Could not find directory, directory was empty, or no changelogs matched the provided search criteria for includeAll '" + pathName + "'");
            }
            return resources;
        } catch (IOException e) {
            throw new SetupException(e);
        }
    }

    private String fixPath(String pathName, ResourceAccessor resourceAccessor, String relativeTo) throws IOException {
        String path;
        if (relativeTo == null) {
            path = pathName;
        } else {
            path = resourceAccessor.get(relativeTo).resolveSibling(pathName).getPath();
            path = normalizePath(path);
        }

        if(path != null) {
            path = path.replace("\\", "/");
            if (StringUtil.isNotEmpty(path) && !(path.endsWith("/"))) {
                path = path + '/';
            }
        }
        return path;
    }

    /**
     * @deprecated use {@link DatabaseChangeLog#include(String, boolean, boolean, ResourceAccessor, ContextExpression, Labels, Boolean, OnUnknownFileFormat)}
     */
    @Deprecated
    public boolean include(String fileName,
                           boolean isRelativePath,
                           boolean errorIfMissing,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           LabelExpression labelExpression,
                           Boolean ignore,
                           boolean logEveryUnknownFileFormat)
            throws LiquibaseException {
        Labels labels = null;
        if (labelExpression != null && !labelExpression.isEmpty()) {
            labels = new Labels(labelExpression.getLabels());
        }

        return include(fileName,
                isRelativePath,
                errorIfMissing,
                resourceAccessor,
                includeContextFilter,
                labels,
                ignore,
                logEveryUnknownFileFormat);
    }

    /**
     * @deprecated use {@link DatabaseChangeLog#include(String, boolean, boolean, ResourceAccessor, ContextExpression, Labels, Boolean, OnUnknownFileFormat)}
     */
    public boolean include(String fileName,
                           boolean isRelativePath,
                           boolean errorIfMissing,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           Labels labels,
                           Boolean ignore,
                           boolean logEveryUnknownFileFormat)
            throws LiquibaseException {
        return include(fileName, isRelativePath, errorIfMissing, resourceAccessor, includeContextFilter, labels, ignore, logEveryUnknownFileFormat ? OnUnknownFileFormat.WARN : OnUnknownFileFormat.SKIP);
    }

    public boolean include(String fileName,
                           boolean isRelativePath,
                           boolean errorIfMissing,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           Labels labels,
                           Boolean ignore,
                           OnUnknownFileFormat onUnknownFileFormat)
            throws LiquibaseException {
        return include(fileName, isRelativePath, errorIfMissing, resourceAccessor, includeContextFilter, labels, ignore, onUnknownFileFormat, new ModifyChangeSets(null, null));
    }

    public boolean include(String fileName,
                           boolean isRelativePath,
                           boolean errorIfMissing,
                           ResourceAccessor resourceAccessor,
                           ContextExpression includeContextFilter,
                           Labels labels,
                           Boolean ignore,
                           OnUnknownFileFormat onUnknownFileFormat,
                           ModifyChangeSets modifyChangeSets)
            throws LiquibaseException {
        if (".svn".equalsIgnoreCase(fileName) || "cvs".equalsIgnoreCase(fileName)) {
            return false;
        }

        if (isRelativePath) {
            try {
                fileName = resourceAccessor.get(this.getPhysicalFilePath()).resolveSibling(fileName).getPath();
                fileName = normalizePath(Paths.get(fileName).toString());
            } catch (IOException e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }
        DatabaseChangeLog changeLog;
        try {
            DatabaseChangeLog rootChangeLog = ROOT_CHANGE_LOG.get();
            if (rootChangeLog == null) {
                ROOT_CHANGE_LOG.set(this);
            }
            DatabaseChangeLog parentChangeLog = PARENT_CHANGE_LOG.get();
            PARENT_CHANGE_LOG.set(this);
            try {
                if (!resourceAccessor.get(fileName).exists()) {
                    if (ChangeLogParserConfiguration.ON_MISSING_INCLUDE_CHANGELOG.getCurrentValue().equals(ChangeLogParserConfiguration.MissingIncludeConfiguration.WARN)
                            || !errorIfMissing) {
                        Scope.getCurrentScope().getLog(getClass()).warning(FileUtil.getFileNotFoundMessage(fileName));
                        return false;
                    } else {
                        throw new ChangeLogParseException(FileUtil.getFileNotFoundMessage(fileName));
                    }
                }
                ChangeLogParser parser = ChangeLogParserFactory.getInstance().getParser(fileName, resourceAccessor);
                changeLog = parser.parse(fileName, changeLogParameters, resourceAccessor);
                changeLog.setIncludeContextFilter(includeContextFilter);
                changeLog.setIncludeLabels(labels);
                changeLog.setIncludeIgnore(ignore != null ? ignore.booleanValue() : false);
            } finally {
                if (rootChangeLog == null) {
                    ROOT_CHANGE_LOG.remove();
                }
                if (parentChangeLog == null) {
                    PARENT_CHANGE_LOG.remove();
                } else {
                    PARENT_CHANGE_LOG.set(parentChangeLog);
                }
            }
        } catch (UnknownChangelogFormatException e) {
            if (onUnknownFileFormat == OnUnknownFileFormat.FAIL) {
                throw e;
            }
            // This matches only an extension, but filename can be a full path, too. Is it right?
            boolean matchesFileExtension = StringUtil.trimToEmpty(fileName).matches("\\.\\w+$");
            if (matchesFileExtension || onUnknownFileFormat == OnUnknownFileFormat.WARN) {
                Scope.getCurrentScope().getLog(getClass()).warning(
                        "included file " + fileName + "/" + fileName + " is not a recognized file type"
                );
            }
            return false;
        } catch (IOException e) {
            throw new LiquibaseException(e.getMessage(), e);
        }
        PreconditionContainer preconditions = changeLog.getPreconditions();
        if (preconditions != null) {
            if (null == this.getPreconditions()) {
                this.setPreconditions(new PreconditionContainer());
            }
            this.getPreconditions().addNestedPrecondition(preconditions);
        }
        for (ChangeSet changeSet : changeLog.getChangeSets()) {
            if (modifyChangeSets != null) {
                modifyChangeSets(modifyChangeSets, changeSet);
            }
            addChangeSet(changeSet);
        }
        skippedChangeSets.addAll(changeLog.getSkippedChangeSets());

        return true;
    }

    private void modifyChangeSets(ModifyChangeSets modifyChangeSets, ChangeSet changeSet) {
        ChangeSetServiceFactory factory = ChangeSetServiceFactory.getInstance();
        ChangeSetService service = factory.createChangeSetService();
        service.modifyChangeSets(changeSet, modifyChangeSets);
    }

    protected ChangeSet createChangeSet(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        ChangeSetServiceFactory factory = ChangeSetServiceFactory.getInstance();
        ChangeSetService service = factory.createChangeSetService();
        ChangeSet changeSet = service.createChangeSet(this);
        changeSet.setChangeLogParameters(this.getChangeLogParameters());
        changeSet.load(node, resourceAccessor);
        return changeSet;
    }

    protected Comparator getStandardChangeLogComparator() {
        return Comparator.comparing(o -> o.replace("WEB-INF/classes/", ""));
    }

    public static String normalizePath(String filePath) {
        if (filePath == null) {
            return null;
        }

        if (filePath.startsWith("classpath:")) {
            filePath = filePath.substring("classpath:".length());
        }

        if (filePath.contains("\\")) {
            filePath = filePath.replace("\\", "/");
        }

        while (filePath.contains("//")) {
            filePath = filePath.replace("//", "/");
        }

        if (filePath.contains("/./")) {
            filePath = filePath.replace("/./", "/");
        }

        if (filePath.indexOf(":") == 1) {
            filePath = NO_LETTER_PATTERN.matcher(filePath).replaceFirst("");
        }

        if (filePath.startsWith("./")) {
            filePath = filePath.substring(1);
        }

        if (filePath.startsWith("/")) {
            filePath = filePath.substring(1);
        }

        return filePath;
    }

    public void clearCheckSums() {
        for (ChangeSet changeSet : getChangeSets()) {
            changeSet.clearCheckSum();
        }
    }

    /**
     * Holder for the PreconditionContainer for this changelog, plus any nested changelogs.
     */
    @LiquibaseService(skip = true)
    private static class GlobalPreconditionContainer extends PreconditionContainer {

        /**
         * This container should always be TEST because it may contain a mix of containers which may or may not get tested during update-sql
         */
        @Override
        public OnSqlOutputOption getOnSqlOutput() {
            return OnSqlOutputOption.TEST;
        }

        @Override
        public void addNestedPrecondition(Precondition precondition) {
            super.addNestedPrecondition(precondition);
        }
    }

    /**
     * Controls what to do when including a file with a format that isn't recognized by a changelog parser.
     */
    public enum OnUnknownFileFormat {

        /**
         * Silently skip unknown files.
         */
        SKIP,

        /**
         * Log a warning about the file not being in a recognized format, but continue on
         */
        WARN,

        /**
         * Fail parsing with an error
         */
        FAIL
    }

    /**
     * Initialize and set min/max depth values validating maxDepth cannot be a lower value than minDepth
     *
     * @param minDepth
     * @param maxDepth
     * @return ResourceAccessor.SearchOptions
     * @throws SetupException in case maxDepth is less than minDepth
     */
    private ResourceAccessor.SearchOptions initializeAndSetMinAndMaxDepth(int minDepth, int maxDepth) throws SetupException {
        ResourceAccessor.SearchOptions searchOptions = new ResourceAccessor.SearchOptions();
        try {
            if (maxDepth < minDepth) {
                throw new IllegalArgumentException("maxDepth argument must be greater than minDepth");
            }

            searchOptions.setMinDepth(minDepth);
            searchOptions.setMaxDepth(maxDepth);
        } catch (IllegalArgumentException e) {
            throw new SetupException("Error in includeAll setup: " + e.getMessage(), e);
        }
        return searchOptions;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy