com.google.copybara.util.Glob.java Source code

Java tutorial

Introduction

Here is the source code for com.google.copybara.util.Glob.java

Source

/*
 * Copyright (C) 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.copybara.util;

import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;

/**
 * A {@link PathMatcher} builder that creates a PathMatcher relative to a {@link Path}.
 *
 * <p>The returned {@link PathMatcher} returns true if any of the {@code paths} expressions match.
 * If {@code paths} is empty it will no match any file.
 */
@SkylarkModule(name = "glob", doc = "Glob returns a list of every file in the workdir that matches at least one"
        + " pattern in include and does not match any of the patterns in exclude.", category = SkylarkModuleCategory.BUILTIN)
public final class Glob {

    private final ImmutableList<String> include;
    @Nullable
    private final Glob exclude;

    public Glob(Iterable<String> include, @Nullable Glob exclude) {
        this.include = ImmutableList.copyOf(include);
        this.exclude = exclude;

        // Validate the paths so that they don't contain invalid patterns.
        for (String glob : include) {
            Preconditions.checkArgument(!glob.isEmpty(), "unexpected empty string in glob list");
            FileUtil.checkNormalizedRelative(glob);
            FileSystems.getDefault().getPathMatcher("glob:" + glob);
        }
    }

    /**
     * Creates a function {@link Glob} that when a {@link Path} is passed it returns a
     * {@link PathMatcher} relative to the path.
     *
     * @param include list of strings representing the globs to include/match
     * @param exclude list of strings representing the globs to exclude from the include set
     *
     * @throws IllegalArgumentException if any glob is not valid
     */
    public Glob(Iterable<String> include, Iterable<String> exclude) {
        this(ImmutableList.copyOf(include), Iterables.isEmpty(exclude) ? null : new Glob(exclude));
    }

    public Glob(Iterable<String> include) {
        this(include, (Glob) null);
    }

    public static final Glob ALL_FILES = new Glob(ImmutableList.of("**"));

    public PathMatcher relativeTo(Path path) {
        ImmutableList.Builder<PathMatcher> includeList = ImmutableList.builder();
        for (String path1 : include) {
            includeList.add(ReadablePathMatcher.relativeGlob(path, path1));
        }
        PathMatcher excludeMatcher = (exclude == null) ? FileUtil.anyPathMatcher(ImmutableList.<PathMatcher>of())
                : exclude.relativeTo(path);
        return new GlobPathMatcher(FileUtil.anyPathMatcher(includeList.build()), excludeMatcher);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("include", include).add("exclude", exclude).toString();
    }

    /**
     * Calculates a set of paths which recursively contain all files that could possibly match a file
     * in this glob. Generally, this returns the include paths but removing all segments that have a
     * metacharacter and following segments.
     *
     * <p>This can be used by an {@code Origin} or {@code Destination} implementation to determine
     * which directories to query from the repo. For instance, if the <em>include</em> paths are:
     *
     * <ul>
     *  <li>foo/bar.jar
     *  <li>foo/baz/**
     * </ul>
     *
     * This function will return a single string: {@code "foo"}.
     *
     * <p>If the include paths potentially include files in the root directory or use metacharacters
     * to specify the top level directory, a set with only the empty string is returned. For
     * instance, the following include globs will cause {@code [""]} (and no other string) to be
     * returned:
     *
     * <ul>
     *  <li>{@code *.java}
     *  <li>{@code {foo,bar}/baz/**}
     * <ul>
     *
     * <p>Note that in the case of {@code origin_files} or {@code destination_files}, the origin or
     * destination may give special meaning to the roots of the glob. For instance, the destination
     * may store metadata at {root}/INFO for every root. See the documentation of the destination or
     * origin you are using for more information.
     */
    public ImmutableSet<String> roots() {
        List<String> roots = new ArrayList<>();

        for (String includePath : this.include) {
            ArrayDeque<String> components = new ArrayDeque<>();
            for (String component : Splitter.on('/').split(includePath)) {
                components.add(unescape(component));
                if (isMeta(component)) {
                    break;
                }
            }
            components.removeLast();
            if (components.isEmpty()) {
                return ImmutableSet.of("");
            }
            roots.add(Joiner.on('/').join(components));
        }

        // Remove redundant roots - e.g. "foo" covers all paths that start with "foo/"
        Collections.sort(roots);
        int r = 0;
        while (r < roots.size() - 1) {
            if (roots.get(r + 1).startsWith(roots.get(r) + "/")) {
                roots.remove(r + 1);
            } else {
                r++;
            }
        }

        return ImmutableSet.copyOf(roots);
    }

    private String unescape(String pathComponent) {
        return pathComponent.replaceAll("\\\\(.)", "$1");
    }

    private boolean isMeta(String pathComponent) {
        int c = 0;
        while (c < pathComponent.length()) {
            switch (pathComponent.charAt(c)) {
            case '*':
            case '{':
            case '[':
            case '?':
                return true;
            case '\\':
                c++;
                break;
            }
            c++;
        }
        return false;
    }

    private class GlobPathMatcher implements PathMatcher {

        private final PathMatcher includeMatcher;
        private final PathMatcher excludeMatcher;

        GlobPathMatcher(PathMatcher includeMatcher, PathMatcher excludeMatcher) {
            this.includeMatcher = includeMatcher;
            this.excludeMatcher = excludeMatcher;
        }

        @Override
        public boolean matches(Path path) {
            return includeMatcher.matches(path) && !excludeMatcher.matches(path);
        }

        @Override
        public String toString() {
            return Glob.this.toString();
        }
    }
}