content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You 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 org.apache.camel.quarkus.core.deployment.util;
  
  import java.io.IOException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.List;
  import java.util.Optional;
  import java.util.Set;
  import java.util.TreeSet;
  import java.util.function.Consumer;
  import java.util.function.Predicate;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  import org.apache.camel.quarkus.core.util.FileUtils;
  import org.apache.camel.util.AntPathMatcher;
  import org.apache.camel.util.ObjectHelper;
  import org.jboss.jandex.DotName;
  
  /**
   * A utility able to filter resource paths using Ant-like includes and excludes.
   */
  public class PathFilter {
      private static final String CLASS_SUFFIX = ".class";
      private static final int CLASS_SUFFIX_LENGTH = CLASS_SUFFIX.length();
  
      private final AntPathMatcher matcher = new AntPathMatcher();
      private final List<String> includePaths;
      private final List<String> includePatterns;
      private final List<String> excludePatterns;
      private final Predicate<String> stringPredicate;
  
      PathFilter(List<String> includePatterns, List<String> excludePatterns) {
          super();
          this.includePaths = includePatterns.stream().filter(path -> !isPattern(path)).collect(Collectors.toList());
          this.includePatterns = includePatterns;
          this.excludePatterns = excludePatterns;
  
          if (ObjectHelper.isEmpty(excludePatterns) && ObjectHelper.isEmpty(includePatterns)) {
              this.stringPredicate = path -> true;
          } else {
              this.stringPredicate = path -> {
                  path = sanitize(path);
                  // same logic as org.apache.camel.main.DefaultRoutesCollector so exclude
                  // take precedence over include
                  if (matchesAny(path, excludePatterns)) {
                      return false;
                  }
                  if (matchesAny(path, includePatterns)) {
                      return true;
                  }
                  return ObjectHelper.isEmpty(includePatterns);
              };
          }
      }
  
      public Predicate<String> asStringPredicate() {
          return stringPredicate;
      }
  
      public Predicate<DotName> asDotNamePredicate() {
          if (ObjectHelper.isEmpty(excludePatterns) && ObjectHelper.isEmpty(includePatterns)) {
              return dotName -> true;
          } else {
              return dotName -> stringPredicate.test(dotName.toString().replace('.', '/'));
          }
      }
  
      public Predicate<Path> asPathPredicate() {
          if (ObjectHelper.isEmpty(excludePatterns) && ObjectHelper.isEmpty(includePatterns)) {
              return path -> true;
          } else {
              return path -> stringPredicate.test(sanitize(path.toString()));
          }
      }
  
      public String[] scanClassNames(Stream<Path> archiveRootDirs) {
          final Set<String> selectedPaths = new TreeSet<>();
          archiveRootDirs.forEach(rootDir -> {
              try (Stream<Path> files = Files.walk(rootDir)) {
                  scanClassNames(rootDir, files, Files::isRegularFile, selectedPaths::add);
              } catch (IOException e) {
                  throw new RuntimeException("Could not walk " + rootDir, e);
              }
          });
          /* Let's add the paths without wildcards even if they did not match any Jandex class
           * A workaround for https://github.com/apache/camel-quarkus/issues/2969 */
          addNonPatternPaths(selectedPaths);
          return selectedPaths.toArray(new String[0]);
      }
  
      void addNonPatternPaths(final Set<String> selectedPaths) {
          if (!includePaths.isEmpty()) {
              for (String path : includePaths) {
                  if (!selectedPaths.contains(path) && !matchesAny(path, excludePatterns)) {
                      selectedPaths.add(path.replace('/', '.'));
                  }
              }
          }
      }
  
      void scanClassNames(Path rootPath, Stream<Path> pathStream, Predicate<Path> isRegularFile,
              Consumer<String> resultConsumer) {
          pathStream
                  .filter(isRegularFile)
                  .filter(path -> path.getFileName().toString().endsWith(CLASS_SUFFIX))
                  .map(rootPath::relativize)
                  .map(Path::toString)
                  .map(stringPath -> stringPath.substring(0, stringPath.length() - CLASS_SUFFIX_LENGTH))
                  .map(FileUtils::nixifyPath)
                  .filter(stringPredicate)
                  .map(slashClassName -> slashClassName.replace('/', '.'))
                  .forEach(resultConsumer::accept);
      }
  
      boolean matchesAny(String path, List<String> patterns) {
          for (String part : patterns) {
              if (matcher.match(part, path)) {
                  return true;
              }
          }
          return false;
      }
  
      static String sanitize(String path) {
          path = FileUtils.nixifyPath(path.trim());
          return (!path.isEmpty() && path.charAt(0) == '/')
                  ? path.substring(1)
                  : path;
      }
  
      static boolean isPattern(String path) {
          return path.indexOf('*') != -1 || path.indexOf('?') != -1;
      }
  
      public static class Builder {
          private List<String> includePatterns = new ArrayList<>();
          private List<String> excludePatterns = new ArrayList<>();
  
          public Builder patterns(boolean isInclude, Collection<String> patterns) {
              if (isInclude) {
                  include(patterns);
              } else {
                  exclude(patterns);
              }
              return this;
          }
  
          public Builder include(String pattern) {
              includePatterns.add(sanitize(pattern));
              return this;
          }
  
          public Builder include(Collection<String> patterns) {
              patterns.stream().forEach(this::include);
              return this;
          }
  
          public Builder include(Optional<? extends Collection<String>> patterns) {
              patterns.ifPresent(this::include);
              return this;
          }
  
          public Builder exclude(String pattern) {
              excludePatterns.add(sanitize(pattern));
              return this;
          }
  
          public Builder exclude(Collection<String> patterns) {
              patterns.stream().forEach(this::exclude);
              return this;
          }
  
          public Builder exclude(Optional<? extends Collection<String>> patterns) {
              patterns.ifPresent(this::exclude);
              return this;
          }
  
          public Builder combine(Builder other) {
              includePatterns.addAll(other.includePatterns);
              excludePatterns.addAll(other.excludePatterns);
              return this;
          }
  
          /**
           * @throws NullPointerException if this method is called more than once for the same {@link Builder} instance.
           * @return                      a new {@link PathFilter}
           */
          public PathFilter build() {
              final List<String> incl = includePatterns;
              includePatterns = null; // avoid leaking the collection trough reuse of the builder
              final List<String> excl = excludePatterns;
              excludePatterns = null; // avoid leaking the collection trough reuse of the builder
              return new PathFilter(incl, excl);
          }
  
      }
  
  }
  