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

com.github.protobufel.common.files.ContextResourcePathMatchers Maven / Gradle / Ivy

The newest version!
/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2017, David Tesler
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

package com.github.protobufel.common.files;

import com.github.protobufel.common.files.ContextPathMatchers.ContextHierarchicalMatcher;
import com.github.protobufel.common.files.ContextPathMatchers.HierarchicalMatcher;
import com.github.protobufel.common.files.HistoryCaches.SimpleHistoryCache;
import com.github.protobufel.common.files.PathContexts.PathContext;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

import java.io.File;
import java.nio.file.Path;
import java.util.*;

import static com.github.protobufel.common.files.Utils.isUnix;
import static com.github.protobufel.common.verifications.Verifications.*;

public final class ContextResourcePathMatchers {
  private ContextResourcePathMatchers() {}

  public static class CompositePathMatcher>
      implements ContextHierarchicalMatcher {
    private static final CompositePathMatcher EMPTY =
        new CompositePathMatcher>();
    private final List matchers;
    private final boolean allowDirs;
    private final boolean allowFiles;
    //private final transient SimpleHistoryCache cache;
    private final transient SimpleHistoryCache cache;

    private CompositePathMatcher() {
      matchers = assertNonNull(Collections.emptyList());
      this.cache = SimpleHistoryCache.emptyCache();
      this.allowDirs = false;
      this.allowFiles = false;
    }

    public CompositePathMatcher(final Iterable matchers) {
      this(matchers, Integer.MAX_VALUE);
    }

    public CompositePathMatcher(final Iterable matchers, final int cacheSize) {
      final @NonNull Set matcherSet = new HashSet();
      final @NonNull List matcherList = new ArrayList();
      boolean allowDirs = false;
      boolean allowFiles = false;

      for (E matcher : verifyNonNull(matchers)) {
        if (!verifyNonNull(matcher).isEmpty()) {
          matcherSet.add(matcher); // there should be no equal matchers!
          matcherList.add(matcher);

          if (!allowDirs && matcher.isAllowDirs()) {
            allowDirs = true;
          }

          if (!allowFiles && matcher.isAllowFiles()) {
            allowFiles = true;
          }
        }
      }

      this.allowDirs = allowDirs;
      this.allowFiles = allowFiles;
      @SuppressWarnings("null")
      final @NonNull List unmodifiableList = Collections.unmodifiableList(matcherList);
      this.matchers = unmodifiableList;
      this.cache = new SimpleHistoryCache(verifyArgument(cacheSize > 0, cacheSize));
    }

    @SuppressWarnings("unchecked")
    public static > CompositePathMatcher emptyInstance() {
      return (CompositePathMatcher) EMPTY;
    }

    @Override
    public boolean isAllowDirs() {
      return allowDirs;
    }

    @Override
    public boolean isAllowFiles() {
      return allowFiles;
    }

    @Override
    public String getPattern() {
      if (matchers.isEmpty()) {
        return "[]";
      }

      final StringBuilder sb = new StringBuilder("[");

      for (E matcher : matchers) {
        sb.append(matcher.getPattern()).append(",");
      }

      sb.setCharAt(sb.length() - 1, ']');
      @SuppressWarnings("null")
      final @NonNull String result = sb.toString();
      return result;
    }

    @Override
    public boolean matches(final T path, final PathContext context) {
      if (!allowFiles) {
        return false;
      }

      cache.adjustCache(context.currentDepth());
      final IterableFilter cacheData = getCacheData();

      for (int index : cacheData) {
        if (matchers.get(index).matches(path, context)) {
          return true;
        }
      }

      return false;
    }

    @Override
    public DirectoryMatchResult matchesDirectory(final T path, final PathContext context) {
      if ((this == EMPTY) || matchers.isEmpty()) {
        return DirectoryMatchResult.NO_MATCH;
      }

      cache.adjustCache(context.currentDepth());
      final IterableFilter cacheData = getCacheData();

      if (cacheData.isEmpty()) {
        return DirectoryMatchResult.NO_MATCH;
      }

      final IterableFilter.Builder cacheBuilder = cacheData.toBuilder();
      boolean skip = true;
      boolean matched = false;

      for (int index : cacheData) {
        final DirectoryMatchResult matchResult =
            matchers.get(index).matchesDirectory(path, context);

        if (matchResult.isMatched()) {
          matched = true;

          if (!matchResult.isSkip()) {
            // MATCH_CONTINUE
            skip = false;
            break;
          }

          cacheBuilder.clear(index);

          if (!skip) {
            // MATCH_CONTINUE
            break;
          }
        } else if (!matchResult.isSkip()) {
          skip = false;

          if (matched) {
            // MATCH_CONTINUE
            break;
          } else if (!allowDirs) {
            // exact match is not required, so we lazily stop searching further
            // NO_MATCH_CONTINUE
            break;
          }
        } else {
          // this matcher didn't match anyhow, so remove it, and continue searching for match
          cacheBuilder.clear(index);
        }
      }

      final DirectoryMatchResult result = DirectoryMatchResult.valueOf(matched, skip);

      if (!skip) {
        cache.push(cacheBuilder.build());
      }

      return result;
    }

    private IterableFilter getCacheData() {
      if (cache.isEmpty()) {
        return IterableFilter.builder().resetAll(matchers).build();
      } else {
        return assertNonNull(cache.peek());
      }
    }

    @Override
    public boolean isEmpty() {
      return (this == EMPTY) || matchers.isEmpty();
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + (allowDirs ? 1231 : 1237);
      result = prime * result + (allowFiles ? 1231 : 1237);
      result = prime * result + ((matchers == null) ? 0 : matchers.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (!(obj instanceof CompositePathMatcher)) {
        return false;
      }
      final CompositePathMatcher other = (CompositePathMatcher) obj;
      if (allowDirs != other.allowDirs) {
        return false;
      }
      if (allowFiles != other.allowFiles) {
        return false;
      }
      if (!matchers.equals(other.matchers)) {
        return false;
      }
      return true;
    }

    @Override
    public String toString() {
      @SuppressWarnings("null")
      @NonNull
      String result =
          new StringBuilder()
              .append("CompositePathMatcher [matchers=")
              .append(matchers)
              .append(", allowDirs=")
              .append(allowDirs)
              .append(", allowFiles=")
              .append(allowFiles)
              .append("]")
              .toString();
      return result;
    }
  }

  public static class FileSetPathMatcher implements HierarchicalMatcher {
    private static final FileSetPathMatcher EMPTY = new FileSetPathMatcher();
    private final List> includes;
    private final List> excludes;
    private final T dir;
    private final boolean allowDirs;
    private final boolean allowFiles;
    private final transient SimpleHistoryCache cache;

    private FileSetPathMatcher() {
      @SuppressWarnings("null")
      final @NonNull List> emptyList = Collections.emptyList();
      this.includes = emptyList;
      this.excludes = emptyList;
      @SuppressWarnings("unchecked")
      final T t = (T) new Object(); // okay because only used for the EMPTY singleton
      this.dir = t;
      this.allowDirs = false;
      this.allowFiles = false;
      this.cache = SimpleHistoryCache.emptyCache();
    }

    public FileSetPathMatcher(
        final Collection includes,
        final Collection excludes,
        final T dir,
        final boolean allowDirs,
        final boolean allowFiles) {
      this(includes, excludes, dir, allowDirs, allowFiles, Integer.MAX_VALUE);
    }

    public FileSetPathMatcher(
        final Collection includes,
        final Collection excludes,
        final T dir,
        final boolean allowDirs,
        final boolean allowFiles,
        final int cacheSize) {
      verifyCondition((allowDirs || allowFiles), "allowDirs and allowFiles cannot be both false");
      this.cache =
          new SimpleHistoryCache(verifyArgument(cacheSize > 0, cacheSize));
      this.allowDirs = allowDirs;
      this.allowFiles = allowFiles;
      this.dir = verifyNonNull(dir);
      @SuppressWarnings({"unchecked", "null"})
      final @NonNull Class pathType = (Class) dir.getClass();
      @SuppressWarnings("null")
      final @NonNull List> emptyList = Collections.emptyList();

      if (verifyNonNull(includes).isEmpty()) {
        this.includes = emptyList;
      } else {
        final List> includesList =
            new ArrayList>(includes.size());

        for (String include : includes) {
          includesList.add(
              ContextPathMatchers.getHierarchicalMatcher(
                  verifyNonNull(include), isUnix(), allowDirs, allowFiles, pathType));
        }

        this.includes = assertNonNull(Collections.unmodifiableList(includesList));
      }

      if (verifyNonNull(excludes).isEmpty()) {
        this.excludes = emptyList;
      } else {
        final List> excludesList =
            new ArrayList>(excludes.size());

        for (String exclude : excludes) {
          excludesList.add(
              ContextPathMatchers.getHierarchicalMatcher(
                  verifyNonNull(exclude), isUnix(), allowDirs, allowFiles, pathType));
        }

        this.excludes = assertNonNull(Collections.unmodifiableList(excludesList));
      }
    }

    @SuppressWarnings("unchecked")
    public static  FileSetPathMatcher emptyInstance() {
      return (FileSetPathMatcher) EMPTY;
    }

    public static > BType builder(final T dir) {
      return new Builder(dir).self();
    }

    private List> getIncludes() {
      return includes;
    }

    private List> getExcludes() {
      return excludes;
    }

    public > BType newBuilder() {
      return new Builder(dir).self();
    }

    public > BType toBuilder() {
      return new Builder(this).self();
    }

    @Override
    public String getPattern() {
      final StringBuilder sb = new StringBuilder("[filter:[");

      if (includes.isEmpty()) {
        sb.append("]");
      } else {
        for (HierarchicalMatcher include : includes) {
          sb.append(include.getPattern()).append(",");
        }

        sb.setCharAt(sb.length() - 1, ']');
      }

      sb.append(", excludes:[");

      if (excludes.isEmpty()) {
        sb.append("]");
      } else {
        for (HierarchicalMatcher exclude : excludes) {
          sb.append(exclude.getPattern()).append(",");
        }

        sb.setCharAt(sb.length() - 1, ']');
      }

      @SuppressWarnings("null")
      final @NonNull String result = sb.append("]").toString();
      return result;
    }

    @Override
    public String toString() {
      return "FileSetPathMatcher [filter="
          + includes
          + ", excludes="
          + excludes
          + ", dir="
          + dir
          + ", allowDirs="
          + allowDirs
          + ", allowFiles="
          + allowFiles
          + "]";
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + (allowDirs ? 1231 : 1237);
      result = prime * result + (allowFiles ? 1231 : 1237);
      result = prime * result + ((dir == null) ? 0 : dir.hashCode());
      result = prime * result + ((excludes == null) ? 0 : excludes.hashCode());
      result = prime * result + ((includes == null) ? 0 : includes.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (!(obj instanceof FileSetPathMatcher)) {
        return false;
      }

      try {
        final FileSetPathMatcher other = (FileSetPathMatcher) obj;

        if (allowDirs != other.allowDirs) {
          return false;
        }
        if (allowFiles != other.allowFiles) {
          return false;
        }
        if (!dir.equals(other.dir)) {
          return false;
        }
        if (!excludes.equals(other.excludes)) {
          return false;
        }
        if (!includes.equals(other.includes)) {
          return false;
        }
        return true;
      } catch (Exception e) {
        return false;
      }
    }

    @Override
    public boolean isEmpty() {
      return (this == EMPTY) || includes.isEmpty();
    }

    public T getDir() {
      return dir;
    }

    public boolean isAllowDirs() {
      return allowDirs;
    }

    public boolean isAllowFiles() {
      return allowFiles;
    }

    protected SimpleHistoryCache getCache() {
      return cache;
    }

    @Override
    public boolean matches(final T path, final PathContext context) {
      if (!allowFiles) {
        return false;
      }

      return matchesResolved(context.resolvePath(dir, path), context);
    }

    @Override
    public boolean matchesResolved(
        final @Nullable String sanitizedPath, final PathContext context) {
      if (!allowFiles) {
        return false;
      }

      cache.adjustCache(context.currentDepth());

      boolean matched = false;
      final FileSetCacheData cacheData = getCacheData();

      if (cacheData.getIncludes().isEmpty()) {
        return false;
      }

      for (int index : cacheData.getIncludes()) {
        final HierarchicalMatcher include = includes.get(index);

        if (include.matchesResolved(sanitizedPath, context)) {
          matched = true;
          break;
        }
      }

      if (!matched) {
        return false;
      }

      if (cacheData.getExcludes().isEmpty()) {
        return true;
      }

      for (int index : cacheData.getExcludes()) {
        final HierarchicalMatcher exclude = includes.get(index);

        if (exclude.matchesResolved(sanitizedPath, context)) {
          return false;
        }
      }

      return true;
    }

    @Override
    public DirectoryMatchResult matchesDirectory(final T path, final PathContext context) {
      if (this == EMPTY) {
        return DirectoryMatchResult.NO_MATCH;
      }

      if (context.resolvePath(path, dir) != null) {
        //this a parent of dir
        return DirectoryMatchResult.NO_MATCH_CONTINUE;
      }

      return matchesResolvedDirectory(
          context.resolvePath(dir, path), context.getSeparator(path), context);
    }

    @Override
    public DirectoryMatchResult matchesResolvedDirectory(
        final @Nullable String sanitizedPath,
        final @Nullable String separator,
        final PathContext context) {
      if ((this == EMPTY) || (sanitizedPath == null) || (separator == null)) {
        return DirectoryMatchResult.NO_MATCH;
      }

      cache.adjustCache(context.currentDepth());

      /*      if (sanitizedPath.isEmpty()) {
        return DirectoryMatchResult.NO_MATCH_CONTINUE;
      }
       */

      final FileSetCacheData cacheData = getCacheData();
      final FileSetCacheData.Builder cacheBuilder = cacheData.newBuilder();

      if (!cacheData.getExcludes().isEmpty()) {
        for (int index : cacheData.getExcludes()) {
          final HierarchicalMatcher exclude = excludes.get(index);
          final DirectoryMatchResult matchResult =
              exclude.matchesResolvedDirectory(sanitizedPath, separator, context);

          if (matchResult.isMatched()) {
            return DirectoryMatchResult.NO_MATCH;
          } else if (!matchResult.isSkip()) {
            cacheBuilder.setExclude(index);
          }
        }
      }

      boolean skip = true;
      boolean matched = false;
      cacheBuilder.includesFrom(cacheData);

      if (!cacheData.getIncludes().isEmpty()) {
        for (int index : cacheData.getIncludes()) {
          final HierarchicalMatcher include = includes.get(index);
          final DirectoryMatchResult matchResult =
              include.matchesResolvedDirectory(sanitizedPath, separator, context);

          if (matchResult.isMatched()) {
            matched = true;

            if (!matchResult.isSkip()) {
              // MATCH_CONTINUE
              skip = false;
              break;
            }

            cacheBuilder.clearInclude(index);

            if (!skip) {
              // MATCH_CONTINUE
              break;
            }
          } else if (!matchResult.isSkip()) {
            skip = false;

            if (matched) {
              // MATCH_CONTINUE
              break;
            } else if (!allowDirs) {
              // exact match is not required, so we lazily stop searching further
              // NO_MATCH_CONTINUE
              break;
            }
          } else {
            // this include didn't match anyhow, so remove it, and continue searching for match
            cacheBuilder.clearInclude(index);
          }
        }
      }

      final DirectoryMatchResult result = DirectoryMatchResult.valueOf(matched, skip);

      // if (result != DirectoryMatchResult.NO_MATCH) {
      if (!skip) {
        cache.push(cacheBuilder.build());
      }

      return result;
    }

    private FileSetCacheData getCacheData() {
      if (cache.isEmpty()) {
        return FileSetCacheData.builder().setIncludes(includes).setExcludes(excludes).build();
      } else {
        return assertNonNull(cache.peek());
      }
    }

    public static class Builder> {
      private final LinkedHashSet includes;
      private final LinkedHashSet excludes;
      private T dir;
      private boolean allowDirs;
      private boolean allowFiles;

      public Builder(final T dir) {
        this.includes = new LinkedHashSet();
        this.excludes = new LinkedHashSet();
        this.dir = dir;
        this.allowDirs = false;
        this.allowFiles = true;
      }

      public Builder(final FileSetPathMatcher original) {
        this(original.getDir());
        from(original);
      }

      public Builder(final BType original) {
        this(original.dir());
        from(original);
      }

      @SuppressWarnings("unchecked")
      protected BType self() {
        return (BType) this;
      }

      public BType from(final FileSetPathMatcher original) {
        verifyNonNull(original);
        this.includes.clear();
        this.excludes.clear();
        addAllMatchers(this.includes, original.getIncludes());
        addAllMatchers(this.excludes, original.getExcludes());
        this.dir = original.getDir();
        this.allowDirs = original.isAllowDirs();
        this.allowFiles = original.isAllowFiles();
        return self();
      }

      public BType from(final BType original) {
        verifyNonNull(original);
        this.includes.clear();
        this.excludes.clear();
        this.includes.addAll(original.includes());
        this.excludes.addAll(original.excludes());
        this.dir = original.dir();
        this.allowDirs = original.isAllowDirs();
        this.allowFiles = original.isAllowFiles();
        return self();
      }

      private LinkedHashSet addAllMatchers(
          final LinkedHashSet target,
          final Iterable> source) {
        for (HierarchicalMatcher include : verifyNonNull(source)) {
          target.add(verifyNonNull(include).getPattern());
        }

        return target;
      }

      //TODO should this be replaced by addAllIncludesAndExcludes, as the rest is murky!
      @SuppressWarnings("unused")
      @Deprecated
      private BType combine(final FileSetPathMatcher other) {
        addAllMatchers(this.includes, other.getIncludes());
        addAllMatchers(this.excludes, other.getExcludes());
        verifyCondition(Objects.equals(this.dir, other.getDir()), "directories must be equal");
        //this.dir = other.dir;
        this.allowDirs = this.allowDirs || other.isAllowDirs();
        this.allowFiles = this.allowFiles || other.isAllowFiles();
        return self();
      }

      public BType addIncludesExcludesFrom(final FileSetPathMatcher other) {
        addAllMatchers(this.includes, other.getIncludes());
        addAllMatchers(this.excludes, other.getExcludes());
        return self();
      }

      @SuppressWarnings("null")
      public Set includes() {
        return Collections.unmodifiableSet(includes);
      }

      @SuppressWarnings("null")
      public Set excludes() {
        return Collections.unmodifiableSet(excludes);
      }

      public BType clearExcludes() {
        excludes.clear();
        return self();
      }

      public BType removeExclude(final String exclude) {
        excludes.remove(exclude);
        return self();
      }

      public BType removeExcludes(final Collection excludes) {
        this.excludes.removeAll(excludes);
        return self();
      }

      public BType clearIncludes() {
        includes.clear();
        return self();
      }

      public BType removeInclude(final String include) {
        includes.remove(include);
        return self();
      }

      public BType removeIncludes(final Collection includes) {
        this.includes.removeAll(includes);
        return self();
      }

      public BType addExcludes(final Iterable excludes) {
        return addAll(this.excludes, excludes);
      }

      public BType addExclude(final String exclude) {
        this.excludes.add(verifyNonNull(exclude));
        return self();
      }

      public BType addIncludes(final Iterable includes) {
        return addAll(this.includes, includes);
      }

      public BType addInclude(final String include) {
        this.includes.add(verifyNonNull(include));
        return self();
      }

      private BType addAll(final Collection target, final Iterable source) {
        for (String include : verifyNonNull(source)) {
          target.add(verifyNonNull(include));
        }

        return self();
      }

      public T dir() {
        return dir;
      }

      @SuppressWarnings({"unchecked", "null"})
      protected T convertPath(String dir) {
        if (this.dir instanceof Path) {
          return (T) ((Path) this.dir).getFileSystem().getPath(dir);
        } else if (this.dir instanceof File) {
          return (T) new File(dir);
        } else if (this.dir instanceof String) {
          return (T) dir;
        } else {
          throw new UnsupportedOperationException();
        }
      }

      public BType dir(final String dir) {
        this.dir = convertPath(verifyNonNull(dir));
        return self();
      }

      public BType dir(final T dir) {
        this.dir = verifyNonNull(dir);
        return self();
      }

      public boolean isAllowDirs() {
        return allowDirs;
      }

      public BType allowDirs(boolean allowDirs) {
        this.allowDirs = allowDirs;
        return self();
      }

      public boolean isAllowFiles() {
        return allowFiles;
      }

      public BType allowFiles(boolean allowFiles) {
        this.allowFiles = allowFiles;
        return self();
      }

      public FileSetPathMatcher build() {
        if (includes.isEmpty()) {
          @SuppressWarnings("unchecked")
          final FileSetPathMatcher empty = (FileSetPathMatcher) EMPTY;
          return empty;
        }

        final FileSetPathMatcher result =
            new FileSetPathMatcher(
                includes, excludes, verifyNonNull(dir), allowDirs, allowFiles);
        return result;
      }
    }
  }

  protected static final class FileSetCacheData {
    private final IterableFilter includes;
    private final IterableFilter excludes;

    private FileSetCacheData(IterableFilter includes, IterableFilter excludes) {
      this.includes = new IterableFilter(includes);
      this.excludes = new IterableFilter(excludes);
    }

    private FileSetCacheData(BitSet includes, BitSet excludes) {
      this.includes = new IterableFilter(includes);
      this.excludes = new IterableFilter(excludes);
    }

    public static Builder builder() {
      return new Builder();
    }

    public IterableFilter getIncludes() {
      return includes;
    }

    public IterableFilter getExcludes() {
      return excludes;
    }

    public Builder newBuilder() {
      return new Builder();
    }

    public Builder toBuilder() {
      return new Builder().from(this);
    }

    @Override
    public String toString() {
      @SuppressWarnings("null")
      @NonNull
      String result =
          new StringBuilder()
              .append("FileSetCacheData [includes=")
              .append(includes)
              .append(", excludes=")
              .append(excludes)
              .append("]")
              .toString();
      return result;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((excludes == null) ? 0 : excludes.hashCode());
      result = prime * result + ((includes == null) ? 0 : includes.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (!(obj instanceof FileSetCacheData)) {
        return false;
      }
      FileSetCacheData other = (FileSetCacheData) obj;
      if (!excludes.equals(other.excludes)) {
        return false;
      }
      if (!includes.equals(other.includes)) {
        return false;
      }
      return true;
    }

    public static class Builder {
      private IterableFilter.Builder includes;
      private IterableFilter.Builder excludes;

      private Builder() {
        this.includes = IterableFilter.builder();
        this.excludes = IterableFilter.builder();
      }

      public Builder from(final Builder other) {
        this.includes = IterableFilter.builder().from(other.includes);
        this.excludes = IterableFilter.builder().from(other.excludes);
        return this;
      }

      public Builder from(final FileSetCacheData other) {
        this.includes = IterableFilter.builder().from(other.includes);
        this.excludes = IterableFilter.builder().from(other.excludes);
        return this;
      }

      public Builder includesFrom(final FileSetCacheData other) {
        this.includes = IterableFilter.builder().from(other.includes);
        return this;
      }

      public Builder excludesFrom(final FileSetCacheData other) {
        this.excludes = IterableFilter.builder().from(other.excludes);
        return this;
      }

      public Builder setExclude(final int index) {
        excludes.set(index);
        return this;
      }

      public Builder setInclude(final int index) {
        includes.set(index);
        return this;
      }

      public Builder setAllIncludes(final Iterable values) {
        includes.resetAll(values);
        return this;
      }

      public Builder setAllExcludes(final Iterable values) {
        excludes.resetAll(values);
        return this;
      }

      public Builder setIncludes(final Iterable values) {
        includes.reset(values);
        return this;
      }

      public Builder setExcludes(final Iterable values) {
        excludes.reset(values);
        return this;
      }

      public Builder clearIncludes() {
        includes.clear();
        return this;
      }

      public Builder clearExcludes() {
        excludes.clear();
        return this;
      }

      public Builder clearExclude(final int index) {
        excludes.clear(index);
        return this;
      }

      public Builder clearInclude(final int index) {
        includes.clear(index);
        return this;
      }

      public FileSetCacheData build() {
        return new FileSetCacheData(includes.build(), excludes.build());
      }
    }
  }

  protected static final class IterableFilter implements Iterable {
    private final BitSet filter;

    private IterableFilter() {
      this.filter = new BitSet();
    }

    private IterableFilter(final BitSet filter) {
      this.filter = compactClone(verifyNonNull(filter));
    }

    private IterableFilter(final IterableFilter other) {
      this.filter = compactClone(verifyNonNull(other.filter));
    }

    @SuppressWarnings("null")
    protected static final BitSet compactClone(final BitSet original) {
      return BitSet.valueOf(original.toLongArray());
    }

    public static Builder builder() {
      return new Builder();
    }

    public Builder newBuilder() {
      return new Builder();
    }

    public Builder toBuilder() {
      return new Builder().from(this);
    }

    public int size() {
      return filter.length();
    }

    public boolean isEmpty() {
      return filter.isEmpty();
    }

    @Override
    public Iterator iterator() {
      return new FilteredIterator(filter);
    }

    @Override
    public String toString() {
      @SuppressWarnings("null")
      final @NonNull String result =
          new StringBuilder()
              .append("IterableFilter [filter=")
              .append(filter)
              .append("]")
              .toString();
      return result;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((filter == null) ? 0 : filter.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (!(obj instanceof IterableFilter)) {
        return false;
      }
      IterableFilter other = (IterableFilter) obj;
      if (!filter.equals(other.filter)) {
        return false;
      }
      return true;
    }

    public static class Builder {
      private BitSet filter;

      private Builder() {
        this.filter = new BitSet();
      }

      public Builder from(final Builder other) {
        this.filter = compactClone(other.filter);
        return this;
      }

      public Builder from(final IterableFilter other) {
        this.filter = compactClone(other.filter);
        return this;
      }

      public Builder set(final int index) {
        filter.set(index);
        return this;
      }

      public Builder reset(final int size, boolean value) {
        filter.set(0, size, value);
        return this;
      }

      public Builder resetAll(final Iterable values) {
        int size = 0;

        if (values instanceof Collection) {
          size = ((Collection) values).size();
        } else {
          for (@SuppressWarnings("unused") Object value : values) {
            size++;
          }
        }

        return reset(size, true);
      }

      public Builder reset(final Iterable values) {
        if (values instanceof Collection) {
          final Collection col = (Collection) values;
          filter = new BitSet(col.size());

          if (col.isEmpty()) {
            return this;
          }
        }

        int i = 0;

        for (Object value : values) {
          filter.set(i++, (value != null));
        }

        return this;
      }

      public Builder clear() {
        filter.clear();
        return this;
      }

      public Builder clear(final int index) {
        filter.clear(index);
        return this;
      }

      public boolean isEmpty() {
        return filter.isEmpty();
      }

      public IterableFilter build() {
        return new IterableFilter(filter);
      }
    }

    private static final class FilteredIterator implements Iterator {
      private final BitSet filter;
      private int current;

      public FilteredIterator(final BitSet filter) {
        this.filter = verifyNonNull(filter);
        this.current = filter.nextSetBit(0);
      }

      @Override
      public boolean hasNext() {
        return (current >= 0);
      }

      @Override
      public Integer next() {
        if (current < 0) {
          throw new NoSuchElementException();
        }

        @SuppressWarnings("null")
        final @NonNull Integer result = Integer.valueOf(current);
        current = filter.nextSetBit(current + 1);
        return result;
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    }
  }
}