This is an automated email from the ASF dual-hosted git repository.

claude pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/creadur-rat.git


The following commit(s) were added to refs/heads/master by this push:
     new 08d67164 RAT-98:  Converted to FileProcessorBuilder design (#439)
08d67164 is described below

commit 08d671640d3cf40de34942438fc6537bb8e2d927
Author: Claude Warren <[email protected]>
AuthorDate: Fri Feb 7 18:44:47 2025 +0100

    RAT-98:  Converted to FileProcessorBuilder design (#439)
    
    * Converted to FileProcessorBuilder design
    
    * Minor changes during review
    
    * removed unused method
    
    ---------
    
    Co-authored-by: P. Ottlinger <[email protected]>
---
 .../rat/config/exclusion/ExclusionProcessor.java   |  15 +-
 .../rat/config/exclusion/StandardCollection.java   |  37 ++--
 .../AbstractFileProcessorBuilder.java              | 232 +++++++++++++++++++++
 ...noreProcessor.java => BazaarIgnoreBuilder.java} |  16 +-
 ...CVSFileProcessor.java => CVSIgnoreBuilder.java} |  24 ++-
 .../fileProcessors/DescendingFileProcessor.java    | 121 -----------
 .../exclusion/fileProcessors/GitFileProcessor.java |  70 -------
 .../exclusion/fileProcessors/GitIgnoreBuilder.java | 105 ++++++++++
 ...HgIgnoreProcessor.java => HgIgnoreBuilder.java} |  34 ++-
 .../src/main/java/org/apache/rat/help/Help.java    |   2 +-
 .../config/exclusion/StandardCollectionTest.java   |   2 +-
 .../fileProcessors/AbstractIgnoreBuilderTest.java  | 118 +++++++++++
 .../AbstractIgnoreProcessorTest.java               |  62 ------
 ...essorTest.java => BazaarIgnoreBuilderTest.java} |  22 +-
 ...rocessorTest.java => CVSIgnoreBuilderTest.java} |  21 +-
 .../DescendingFileProcessorTest.java               |  70 -------
 .../fileProcessors/GitFileProcessorTest.java       | 118 -----------
 .../fileProcessors/GitIgnoreBuilderTest.java       | 138 ++++++++++++
 ...ProcessorTest.java => HgIgnoreBuilderTest.java} |  32 +--
 .../commandLine.txt                                |   0
 .../expected-message.txt                           |   0
 .../{RAT_355 => GitIgnoreBuilderTest}/pom.xml      |   0
 .../resources/GitIgnoreBuilderTest/src/.gitignore  |   7 +
 .../src/README.txt                                 |   0
 .../GitIgnoreBuilderTest/src/dir1/.gitignore       |   3 +
 .../src/dir1/dir1.md                               |   0
 .../src/dir1/dir1.txt}                             |   0
 .../src/dir1/file1.log}                            |   0
 .../src/dir2/dir2.md}                              |   0
 .../src/dir2/dir2.txt                              |   0
 .../src/dir3/dir3.log}                             |   0
 .../src/dir3/file3.log                             |   0
 .../src/invoker.properties                         |   0
 .../dir1.md => GitIgnoreBuilderTest/src/root.md}   |   0
 .../verify.groovy                                  |   0
 35 files changed, 705 insertions(+), 544 deletions(-)

diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
index d5b3ebe4..7f908750 100644
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
@@ -209,15 +209,12 @@ public class ExclusionProcessor {
     private List<MatcherSet> extractFileProcessors(final DocumentName basedir) 
{
         final List<MatcherSet> fileProcessorList = new ArrayList<>();
         for (StandardCollection sc : fileProcessors) {
-            final Set<String> names = new HashSet<>();
-            sc.fileProcessor().map(fp -> fp.apply(basedir)).forEachRemaining(n 
-> n.forEach(names::add));
-            MatcherSet.Builder builder = new MatcherSet.Builder();
-            Set<String> matching = new HashSet<>();
-            Set<String> notMatching = new HashSet<>();
-            MatcherSet.Builder.segregateList(matching, notMatching, names);
-            builder.addIncluded(basedir.resolve(sc.name()), notMatching);
-            builder.addExcluded(basedir.resolve(sc.name()), matching);
-            fileProcessorList.add(builder.build());
+            ExtendedIterator<List<MatcherSet>> iter =  
sc.fileProcessorBuilder().map(builder -> builder.build(basedir));
+            if (iter.hasNext()) {
+                iter.forEachRemaining(fileProcessorList::addAll);
+            } else {
+                DefaultLog.getInstance().debug(String.format("%s does not have 
a fileProcessor.", sc));
+            }
         }
         return fileProcessorList;
     }
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
index d2c59707..62c17972 100644
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
@@ -28,10 +28,11 @@ import java.util.List;
 import java.util.Set;
 import java.util.function.Predicate;
 
-import org.apache.rat.config.exclusion.fileProcessors.BazaarIgnoreProcessor;
-import org.apache.rat.config.exclusion.fileProcessors.CVSFileProcessor;
-import org.apache.rat.config.exclusion.fileProcessors.GitFileProcessor;
-import org.apache.rat.config.exclusion.fileProcessors.HgIgnoreProcessor;
+import 
org.apache.rat.config.exclusion.fileProcessors.AbstractFileProcessorBuilder;
+import org.apache.rat.config.exclusion.fileProcessors.BazaarIgnoreBuilder;
+import org.apache.rat.config.exclusion.fileProcessors.CVSIgnoreBuilder;
+import org.apache.rat.config.exclusion.fileProcessors.GitIgnoreBuilder;
+import org.apache.rat.config.exclusion.fileProcessors.HgIgnoreBuilder;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.document.DocumentNameMatcher;
 import org.apache.rat.utils.ExtendedIterator;
@@ -55,7 +56,7 @@ public enum StandardCollection {
      * The files and directories created by a Bazaar source code control based 
tool.
      */
     BAZAAR("The files and directories created by a Bazaar source code control 
based tool.",
-            Arrays.asList("**/.bzr/**", "**/.bzrignore"), null, new 
BazaarIgnoreProcessor()),
+            Arrays.asList("**/.bzr/**", "**/.bzrignore"), null, new 
BazaarIgnoreBuilder()),
     /**
      * The files and directories created by a Bitkeeper source code control 
based tool.
      */
@@ -74,7 +75,7 @@ public enum StandardCollection {
                     "**/*.orig", "**/*.rej", "**/.del-*",
                     "**/*.a", "**/*.old", "**/*.o", "**/*.obj", "**/*.so", 
"**/*.exe",
                     "**/*.Z", "**/*.elc", "**/*.ln", "**/core"),
-            null, new CVSFileProcessor()),
+            null, new CVSIgnoreBuilder()),
     /**
      * The files and directories created by a DARCS source code control based 
tool.
      */
@@ -93,7 +94,7 @@ public enum StandardCollection {
     GIT("The files and directories created by GIT source code control to 
support GIT, also processes files listed in '.gitignore'.",
             Arrays.asList("**/.git/**", "**/.gitignore"),
             null,
-            new GitFileProcessor()
+            new GitIgnoreBuilder()
     ),
     /**
      * The hidden directories. Directories with names that start with {@code .}
@@ -135,9 +136,9 @@ public enum StandardCollection {
     IDEA("The files and directories created by an IDEA IDE based tool.",
             Arrays.asList("**/*.iml", "**/*.ipr", "**/*.iws", "**/.idea/**"), 
null, null),
     /**
-     * The .DS_Store files on Mac computers.
+     * The {@code .DS_Store} files on Mac computers.
      */
-    MAC("The .DS_Store files Mac computers.",
+    MAC("The .DS_Store files on Mac computers.",
             Collections.singletonList("**/.DS_Store"), null, null),
     /**
      * The files and directories created by Maven build system based project.
@@ -156,7 +157,7 @@ public enum StandardCollection {
      * The files and directories created by a Mercurial source code control 
based tool.
      */
     MERCURIAL("The files and directories created by a Mercurial source code 
control based tool.",
-            Arrays.asList("**/.hg/**", "**/.hgignore"), null, new 
HgIgnoreProcessor()),
+            Arrays.asList("**/.hg/**", "**/.hgignore"), null, new 
HgIgnoreBuilder()),
     /**
      * The set of miscellaneous files generally left by editors and the like.
      */
@@ -213,17 +214,17 @@ public enum StandardCollection {
     private final Collection<String> patterns;
     /** A document name matcher supplier to create a document name matcher. 
May be null */
     private final DocumentNameMatcher staticDocumentNameMatcher;
-    /** The FileProcessor to process the exclude file associated with this 
exclusion. May be null. */
-    private final FileProcessor fileProcessor;
+    /** The AbstractFileProcessorBuilder to process the exclude file 
associated with this exclusion. May be {@code null}. */
+    private final AbstractFileProcessorBuilder fileProcessorBuilder;
     /** The description of this collection */
     private final String desc;
 
     StandardCollection(final String desc, final Collection<String> patterns, 
final DocumentNameMatcher documentNameMatcher,
-                       final FileProcessor fileProcessor) {
+                       final AbstractFileProcessorBuilder 
fileProcessorBuilder) {
         this.desc = desc;
         this.patterns = patterns == null ? Collections.emptyList() : new 
HashSet<>(patterns);
         this.staticDocumentNameMatcher = documentNameMatcher;
-        this.fileProcessor = fileProcessor;
+        this.fileProcessorBuilder = fileProcessorBuilder;
     }
 
     /**
@@ -277,11 +278,11 @@ public enum StandardCollection {
      *
      * @return the fileProcessor if it exists, {@code null} otherwise.
      */
-    public ExtendedIterator<FileProcessor> fileProcessor() {
-        List<FileProcessor> lst = new ArrayList<>();
+    public ExtendedIterator<AbstractFileProcessorBuilder> 
fileProcessorBuilder() {
+        List<AbstractFileProcessorBuilder> lst = new ArrayList<>();
         for (StandardCollection sc : getCollections()) {
-            if (sc.fileProcessor != null) {
-                lst.add(sc.fileProcessor);
+            if (sc.fileProcessorBuilder != null) {
+                lst.add(sc.fileProcessorBuilder);
             }
         }
         return ExtendedIterator.create(lst.iterator());
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/AbstractFileProcessorBuilder.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/AbstractFileProcessorBuilder.java
new file mode 100644
index 00000000..9e33aeab
--- /dev/null
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/AbstractFileProcessorBuilder.java
@@ -0,0 +1,232 @@
+/*
+ * 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.rat.config.exclusion.fileProcessors;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.apache.commons.io.filefilter.DirectoryFileFilter;
+import org.apache.commons.io.filefilter.NameFileFilter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.rat.config.exclusion.ExclusionUtils;
+import org.apache.rat.config.exclusion.MatcherSet;
+import org.apache.rat.config.exclusion.plexus.MatchPattern;
+import org.apache.rat.config.exclusion.plexus.MatchPatterns;
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.document.DocumentNameMatcher;
+
+/**
+ * Creates a List of {@link MatcherSet}s that represent the inclusions and 
exclusions of this file processor.
+ * <p>
+ *     By default this processor:
+ * </p>
+ * <ul>
+ *     <li>Creates a list of levels that correspond to the depth of the 
directories where the specific include/exclude file is located.
+ *     Directory depth is relative to the initially discovered include/exclude 
file.</li>
+ *     <li>A MatcherSet is created for each include/exclude file located, and 
the MatcherSet is added to the proper level.</li>
+ *     <li>During the build:
+ *     <ul>
+ *         <li>Each level creates a MatcherSet for the level.</li>
+ *         <li>The MatcherSet for each level is returned in reverse order 
(deepest first). This ensures that most include/exclude
+ *         files will be properly handled.</li>
+ *     </ul></li>
+ *  </ul>
+ */
+public abstract class AbstractFileProcessorBuilder {
+    /** A String format pattern to print a regex string */
+    protected static final String REGEX_FMT = "%%regex[%s]";
+    /** The name of the file being processed */
+    protected final String fileName;
+    /** The predicate that will return {@code false} for any comment line in 
the file. */
+    protected final Predicate<String> commentFilter;
+    /** the collection of level builders */
+    private final SortedMap<Integer, LevelBuilder> levelBuilders;
+    /** If {@code true} then the processor file name will be included in the 
list of files to ignore */
+    private final boolean includeProcessorFile;
+
+    /**
+     * Constructor for multiple comment prefixes.
+     * @param fileName The name of the file being read.
+     * @param commentPrefixes the collection of comment prefixes.
+     */
+    protected AbstractFileProcessorBuilder(final String fileName, final 
Iterable<String> commentPrefixes, final boolean includeProcessorFile) {
+        this(fileName, commentPrefixes == null ? StringUtils::isNotBlank : 
ExclusionUtils.commentFilter(commentPrefixes), includeProcessorFile);
+    }
+
+    /**
+     * Constructor for single comment prefix
+     * @param fileName The name of the file to process.
+     * @param commentPrefix the comment prefix
+     */
+    protected AbstractFileProcessorBuilder(final String fileName, final String 
commentPrefix, final boolean includeProcessorFile) {
+        this(fileName, commentPrefix == null ? null : 
Collections.singletonList(commentPrefix), includeProcessorFile);
+    }
+
+    /**
+     * Constructor for single comment prefix
+     * @param fileName The name of the file to process.
+     * @param commentFilter the comment prefix filter.
+     */
+    protected AbstractFileProcessorBuilder(final String fileName, final 
Predicate<String> commentFilter, final boolean includeProcessorFile) {
+        this.levelBuilders = new TreeMap<>();
+        this.fileName = fileName;
+        this.commentFilter = commentFilter;
+        this.includeProcessorFile = includeProcessorFile;
+    }
+
+    /**
+     * Creates the MatcherSet from each level and returns them in a list in 
reverse order.
+     * @return a list of MatcherSet
+     */
+
+    private List<MatcherSet> createMatcherSetList() {
+        List<Integer> keys = new ArrayList<>(levelBuilders.keySet());
+        keys.sort((a, b) -> -1 * Integer.compare(a, b));
+        return keys.stream().map(key -> 
levelBuilders.get(key).asMatcherSet()).collect(Collectors.toList());
+    }
+
+    /**
+     * Builder the list of MatcherSet that define the inclusions/exclusions 
for the file processor.
+     * @param root the directory against which name resolution should be made.
+     * @return the List of MatcherSet that represent this file processor.
+     */
+    public final List<MatcherSet> build(final DocumentName root) {
+        if (includeProcessorFile) {
+            String name = String.format("**/%s", fileName);
+            String pattern = ExclusionUtils.qualifyPattern(root, name);
+            MatcherSet matcherSet = new MatcherSet.Builder()
+                    .addExcluded(new DocumentNameMatcher(name, 
MatchPatterns.from(root.getDirectorySeparator(), 
Collections.singletonList(pattern)), root))
+            .build();
+            LevelBuilder levelBuilder = levelBuilders.computeIfAbsent(0, k -> 
new LevelBuilder());
+            levelBuilder.add(matcherSet);
+        }
+
+        checkDirectory(0, root, root, new NameFileFilter(fileName));
+
+        List<MatcherSet> result = levelBuilders.size() == 1 ? 
Collections.singletonList(levelBuilders.get(0).asMatcherSet())
+            : createMatcherSetList();
+        levelBuilders.clear();
+        return result;
+    }
+
+    /**
+     * Process by reading the file, creating a MatcherSet, and adding it to the
+     * matcherSets.
+     * @param matcherSetConsumer the consumer to add the custom matcher sets 
to.
+     * @param root The root against which to resolve names.
+     * @param documentName the file to read.
+     * @return A matcher set based on the strings in the file.
+     */
+    protected MatcherSet process(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName root, final DocumentName documentName) {
+        final MatcherSet.Builder matcherSetBuilder = new MatcherSet.Builder();
+        final List<String> iterable = new ArrayList<>();
+        ExclusionUtils.asIterator(documentName.asFile(), commentFilter)
+                .map(entry -> modifyEntry(matcherSetConsumer, documentName, 
entry).orElse(null))
+                .filter(Objects::nonNull)
+                .map(entry -> ExclusionUtils.qualifyPattern(documentName, 
entry))
+                .forEachRemaining(iterable::add);
+
+        Set<String> included = new HashSet<>();
+        Set<String> excluded = new HashSet<>();
+        MatcherSet.Builder.segregateList(excluded, included, iterable);
+        DocumentName displayName = 
DocumentName.builder(root).setName(documentName.getName()).build();
+        matcherSetBuilder.addExcluded(displayName, excluded);
+        matcherSetBuilder.addIncluded(displayName, included);
+        return matcherSetBuilder.build();
+    }
+
+    /**
+     * Process the directory tree looking for files that match the filter. 
Call {@link #process} on any matching file.
+     * @param level the level being precessed
+     * @param root the directory against which names should be resolved.
+     * @param directory The name of the directory to process.
+     * @param fileFilter the filter to detect processable files with.
+     */
+    private void checkDirectory(final int level, final DocumentName root, 
final DocumentName directory, final FileFilter fileFilter) {
+        File dirFile = directory.asFile();
+        for (File file : listFiles(dirFile, fileFilter)) {
+            LevelBuilder levelBuilder = levelBuilders.computeIfAbsent(level, k 
-> new LevelBuilder());
+            levelBuilder.add(process(levelBuilder::add, root, 
DocumentName.builder(file).build()));
+        }
+        for (File dir : listFiles(dirFile, DirectoryFileFilter.DIRECTORY)) {
+            checkDirectory(level + 1, root, 
DocumentName.builder(dir).setBaseName(directory.getBaseName()).build(), 
fileFilter);
+        }
+    }
+
+    /**
+     * Allows modification of the file entry to match the {@link MatchPattern} 
format.
+     * Default implementation returns the @{code entry} argument.
+     * @param documentName the name of the document that the file was read 
from.
+     * @param entry the entry from that document.
+     * @return the modified string or an empty Optional to skip the string.
+     */
+    protected Optional<String> modifyEntry(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName documentName, final String entry) {
+        return Optional.of(entry);
+    }
+
+    /**
+     * Create a list of files by applying the filter to the specified 
directory.
+     * @param dir the directory.
+     * @param filter the filter.
+     * @return an array of files. May be empty but will not be null.
+     */
+    protected File[] listFiles(final File dir, final FileFilter filter) {
+        File[] result = dir.listFiles(filter);
+        return result == null ? new File[0] : result;
+    }
+
+    /**
+     * Manages the merging of {@link MatcherSet}s for the specified level.
+     */
+    private static class LevelBuilder {
+        /**
+         * The list of MatcherSets that this builder produced.
+         */
+        private final MatcherSet.Builder builder = new MatcherSet.Builder();
+
+        /**
+         * Adds a MatcherSet to this level.
+         * @param matcherSet the matcher set to add.
+         */
+        public void add(final MatcherSet matcherSet) {
+            matcherSet.includes().ifPresent(builder::addIncluded);
+            matcherSet.excludes().ifPresent(builder::addExcluded);
+        }
+
+        /**
+         * Constructs the MatcherSet for this level.
+         * @return the MatcherSet.
+         */
+        public MatcherSet asMatcherSet() {
+            return builder.build();
+        }
+    }
+}
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreProcessor.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreBuilder.java
similarity index 71%
rename from 
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreProcessor.java
rename to 
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreBuilder.java
index 65217c37..2be97f98 100644
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreProcessor.java
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreBuilder.java
@@ -18,6 +18,10 @@
  */
 package org.apache.rat.config.exclusion.fileProcessors;
 
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.apache.rat.config.exclusion.MatcherSet;
 import org.apache.rat.document.DocumentName;
 
 import static java.lang.String.format;
@@ -25,21 +29,21 @@ import static java.lang.String.format;
 /**
  * A processor for {@code .bzrignore} files.
  */
-public final class BazaarIgnoreProcessor extends DescendingFileProcessor {
+public final class BazaarIgnoreBuilder extends AbstractFileProcessorBuilder {
     /**
      * Constructor.
      */
-    public BazaarIgnoreProcessor() {
-        super(".bzrignore", "#");
+    public BazaarIgnoreBuilder() {
+        super(".bzrignore", "#", true);
     }
 
     @Override
-    public String modifyEntry(final DocumentName baseName, final String entry) 
{
+    public Optional<String> modifyEntry(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName baseName, final String entry) {
         if (entry.startsWith("RE:")) {
             String line = entry.substring("RE:".length()).trim();
             String pattern = line.startsWith("^") ? line.substring(1) : line;
-            return format(REGEX_FMT, pattern);
+            return Optional.of(format(REGEX_FMT, pattern));
         }
-        return entry;
+        return Optional.of(entry);
     }
 }
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessor.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/CVSIgnoreBuilder.java
similarity index 63%
rename from 
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessor.java
rename to 
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/CVSIgnoreBuilder.java
index a1f6582c..8f0fc5ef 100644
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessor.java
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/CVSIgnoreBuilder.java
@@ -19,39 +19,45 @@
 package org.apache.rat.config.exclusion.fileProcessors;
 
 import java.io.File;
-import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.rat.config.exclusion.ExclusionUtils;
+import org.apache.rat.config.exclusion.MatcherSet;
 import org.apache.rat.document.DocumentName;
 
 /**
  * A file processor for the {@code .csvignore} file.
+ * @see <a 
href="https://www.gnu.org/software/trans-coord/manual/cvs/html_node/cvsignore.html#cvsignore";>Ignoring
 files via cvsignore</a>
+ * <p>
+ *     The patterns found in {@code .csvignore} are only valid for the 
directory that contains them, not for any subdirectories.
+ * </p>
  */
-public class CVSFileProcessor extends DescendingFileProcessor {
+public class CVSIgnoreBuilder extends AbstractFileProcessorBuilder {
     /**
      * The constructor.
      */
-    public CVSFileProcessor() {
-        super(".cvsignore", (String) null);
+    public CVSIgnoreBuilder() {
+        super(".cvsignore", (String) null, true);
     }
 
     @Override
-    protected List<String> process(final DocumentName documentName) {
+    protected MatcherSet process(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName root, final DocumentName documentName) {
         final File dir = new File(documentName.getName());
-        List<String> result = new ArrayList<>();
+        Set<String> result = new HashSet<>();
         Iterator<String> iter = ExclusionUtils.asIterator(dir, 
StringUtils::isNotBlank);
         while (iter.hasNext()) {
             String line = iter.next();
             String[] parts = line.split("\\s+");
             for (String part : parts) {
                 if (!part.isEmpty()) {
-                    result.add(this.localizePattern(documentName, part));
+                    result.add(ExclusionUtils.qualifyPattern(documentName, 
part));
                 }
             }
         }
-        return result;
+        return new MatcherSet.Builder().addExcluded(documentName, 
result).build();
     }
 }
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/DescendingFileProcessor.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/DescendingFileProcessor.java
deleted file mode 100644
index 438f0075..00000000
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/DescendingFileProcessor.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.rat.config.exclusion.fileProcessors;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.function.Predicate;
-
-import org.apache.commons.io.filefilter.DirectoryFileFilter;
-import org.apache.commons.io.filefilter.NameFileFilter;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.rat.config.exclusion.ExclusionUtils;
-import org.apache.rat.config.exclusion.FileProcessor;
-import org.apache.rat.document.DocumentName;
-
-/**
- * A FileProcessor that assumes the files contain the already formatted 
strings and just need to be
- * localized for the fileName.
- */
-public class DescendingFileProcessor extends FileProcessor {
-    /** The name of the file being processed */
-    private final String fileName;
-    /** The predicate that will return {@code false} for any comment line in 
the file. */
-    protected final Predicate<String> commentFilter;
-
-    /**
-     * Constructor.
-     * @param fileName The name of the file to process.
-     * @param commentPrefix the comment prefix
-     */
-    public DescendingFileProcessor(final String fileName, final String 
commentPrefix) {
-        this(fileName, commentPrefix == null ? null : 
Collections.singletonList(commentPrefix));
-    }
-
-    /**
-     * Constructor.
-     * @param fileName name of the file to process
-     * @param commentPrefixes a collection of comment prefixes.
-     */
-    public DescendingFileProcessor(final String fileName, final 
Iterable<String> commentPrefixes) {
-        super();
-        this.fileName = fileName;
-        // null prefixes = check prefix may not be blank.
-        this.commentFilter = commentPrefixes == null ? StringUtils::isNotBlank 
: ExclusionUtils.commentFilter(commentPrefixes);
-    }
-
-    /**
-     * Process by reading the file and return a list of properly formatted 
patterns.
-     * The default implementation does the following:
-     * <ul>
-     *     <li>reads lines from the file specified by documentName</li>
-     *     <li>modifies those entries by calling {@link 
FileProcessor#modifyEntry(DocumentName, String)}</li>
-     *     <li>further modifies the entry by calling {@link 
FileProcessor#localizePattern(DocumentName, String)}</li>
-     *     <li>retrieving the name from the resulting DocumentName</li>
-     * </ul>
-     * @param documentName the file to read.
-     * @return the list of properly formatted patterns
-     */
-    protected List<String> process(final DocumentName documentName) {
-        return ExclusionUtils.asIterator(new File(documentName.getName()), 
commentFilter)
-                .map(entry -> this.modifyEntry(documentName, entry))
-                .filter(Objects::nonNull)
-                .map(entry -> this.localizePattern(documentName, entry))
-                .addTo(new ArrayList<>());
-    }
-
-    /**
-     * Create a list of files by applying the filter to the specified 
directory.
-     * @param dir the directory.
-     * @param filter the filter.
-     * @return an array of files. May be empty but will not be null.
-     */
-    private File[] listFiles(final File dir, final FileFilter filter) {
-        File[] result = dir.listFiles(filter);
-        return result == null ? new File[0] : result;
-    }
-
-    /**
-     * Process the directory tree looking for files that match the filter. 
Process any matching file and return
-     * a list of fully qualified patterns.
-     * @param directory The name of the directory to process.
-     * @param fileFilter the filter to detect processable files with.
-     * @return the list of fully qualified file patterns.
-     */
-    protected List<String> checkDirectory(final DocumentName directory, final 
FileFilter fileFilter) {
-        List<String> fileNames = new ArrayList<>();
-        File dirFile = new File(directory.getName());
-        for (File f : listFiles(dirFile, fileFilter)) {
-            
fileNames.addAll(process(DocumentName.builder(f).setBaseName(directory.getBaseName()).build()));
-        }
-        for (File dir : listFiles(dirFile, DirectoryFileFilter.DIRECTORY)) {
-            fileNames.addAll(checkDirectory(DocumentName.builder(dir).build(), 
fileFilter));
-        }
-        return fileNames;
-    }
-
-    @Override
-    public List<String> apply(final DocumentName dir) {
-       return checkDirectory(dir, new NameFileFilter(fileName));
-    }
-}
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/GitFileProcessor.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/GitFileProcessor.java
deleted file mode 100644
index 8cf2d096..00000000
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/GitFileProcessor.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.rat.config.exclusion.fileProcessors;
-
-import java.io.File;
-
-import org.apache.rat.config.exclusion.plexus.MatchPatterns;
-import org.apache.rat.document.DocumentName;
-import org.apache.rat.document.DocumentNameMatcher;
-
-/**
- * Processes the .gitignore file.
- * @see <a href='https://git-scm.com/docs/gitignore'>.gitignore 
documentation</a>
- */
-public class GitFileProcessor extends DescendingFileProcessor {
-
-    /**
-     * Constructs a file processor that processes a .gitignore file and 
ignores any lines starting with "#".
-     */
-    public GitFileProcessor() {
-        super(".gitignore", "#");
-    }
-
-    @Override
-    public String modifyEntry(final DocumentName documentName, final String 
entry) {
-        // An optional prefix "!" which negates the pattern;
-        boolean prefix = entry.startsWith("!");
-        String pattern = prefix || entry.startsWith("\\#") || 
entry.startsWith("\\!") ? entry.substring(1) : entry;
-
-        // If there is a separator at the beginning or middle (or both) of the 
pattern, then
-        // the pattern is relative to the directory level of the particular 
.gitignore file itself.
-        // Otherwise, the pattern may also match at any level below the 
.gitignore level.
-        int slashPos = pattern.indexOf("/");
-        // no slash or at end already
-        if (slashPos == -1 || slashPos == pattern.length() - 1) {
-            pattern = "**/" + pattern;
-        }
-        if (slashPos == 0) {
-            pattern = pattern.substring(1);
-        }
-        // If there is a separator at the end of the pattern then the pattern 
will only match directories,
-        // otherwise the pattern can match both files and directories.
-        if (pattern.endsWith("/")) {
-            pattern = pattern.substring(0, pattern.length() - 1);
-            String name = prefix ? "!" + pattern : pattern;
-            DocumentName matcherPattern = 
DocumentName.builder(documentName).setName(name.replace("/", 
documentName.getDirectorySeparator()))
-                            .build();
-            customMatchers.add(DocumentNameMatcher.and(new 
DocumentNameMatcher("isDirectory", File::isDirectory),
-                    new DocumentNameMatcher(name, 
MatchPatterns.from(matcherPattern.localized("/")))));
-            return null;
-        }
-        return prefix ? "!" + pattern : pattern;
-    }
-}
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/GitIgnoreBuilder.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/GitIgnoreBuilder.java
new file mode 100644
index 00000000..cf4e9fb9
--- /dev/null
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/GitIgnoreBuilder.java
@@ -0,0 +1,105 @@
+/*
+ * 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.rat.config.exclusion.fileProcessors;
+
+import java.io.File;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.apache.rat.config.exclusion.ExclusionUtils;
+import org.apache.rat.config.exclusion.MatcherSet;
+import org.apache.rat.config.exclusion.plexus.MatchPatterns;
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.document.DocumentNameMatcher;
+
+import static org.apache.rat.config.exclusion.ExclusionUtils.NEGATION_PREFIX;
+
+/**
+ * Processes the {@code .gitignore} file.
+ * @see <a href='https://git-scm.com/docs/gitignore'>.gitignore 
documentation</a>
+ */
+public class GitIgnoreBuilder extends AbstractFileProcessorBuilder {
+    /** The name of the file we read from */
+    private static final String IGNORE_FILE = ".gitignore";
+    /** The comment prefix */
+    private static final String COMMENT_PREFIX = "#";
+    /** An escaped comment in the .gitignore file.  (Not a comment) */
+    private static final String ESCAPED_COMMENT = "\\#";
+    /** An escaped negation in the .gitignore file. (Not a negation) */
+    private static final String ESCAPED_NEGATION = "\\!";
+    /** The slash string */
+    private static final String SLASH = "/";
+
+    /**
+     * Constructs a file processor that processes a {@code .gitignore} file 
and ignores any lines starting with {@value #COMMENT_PREFIX}.
+     */
+    public GitIgnoreBuilder() {
+        super(IGNORE_FILE, COMMENT_PREFIX, true);
+    }
+
+    /**
+     * Convert the string entry.
+     * If the string ends with a slash an {@link DocumentNameMatcher#and} is 
constructed from a directory check and the file
+     * name matcher.  In this case an empty Optional is returned.
+     * If the string starts with {@value ExclusionUtils#NEGATION_PREFIX} then 
the entry is placed in the include list, otherwise
+     * the entry is placed in the exclude list and the name of the check 
returned.
+     * @param documentName The name of the document being processed.
+     * @param entry The entry from the document
+     * @return and Optional containing the name of the matcher.
+     */
+    @Override
+    protected Optional<String> modifyEntry(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName documentName, final String entry) {
+        // An optional prefix "!" which negates the pattern;
+        boolean prefix = entry.startsWith(NEGATION_PREFIX);
+        String pattern = prefix || entry.startsWith(ESCAPED_COMMENT) || 
entry.startsWith(ESCAPED_NEGATION) ?
+                entry.substring(1) : entry;
+
+        // If there is a separator at the beginning or middle (or both) of the 
pattern, then
+        // the pattern is relative to the directory level of the particular 
.gitignore file itself.
+        // Otherwise, the pattern may also match at any level below the 
.gitignore level.
+        int slashPos = pattern.indexOf(SLASH);
+        // no slash or at end already
+        if (slashPos == -1 || slashPos == pattern.length() - 1) {
+            pattern = "**/" + pattern;
+        }
+        if (slashPos == 0) {
+            pattern = pattern.substring(1);
+        }
+        // If there is a separator at the end of the pattern then the pattern 
will only match directories,
+        // otherwise the pattern can match both files and directories.
+        if (pattern.endsWith(SLASH)) {
+            pattern = pattern.substring(0, pattern.length() - 1);
+            String name = prefix ? NEGATION_PREFIX + pattern : pattern;
+            DocumentName matcherPattern = 
DocumentName.builder(documentName).setName(name.replace(SLASH, 
documentName.getDirectorySeparator()))
+                    .build();
+            DocumentNameMatcher matcher = DocumentNameMatcher.and(new 
DocumentNameMatcher("isDirectory", File::isDirectory),
+                    new DocumentNameMatcher(name, 
MatchPatterns.from(matcherPattern.localized(documentName.getDirectorySeparator()))));
+
+            MatcherSet.Builder builder = new MatcherSet.Builder();
+            if (prefix) {
+                builder.addIncluded(matcher);
+            } else {
+                builder.addExcluded(matcher);
+            }
+            matcherSetConsumer.accept(builder.build());
+            return Optional.empty();
+        }
+        return Optional.of(prefix ? NEGATION_PREFIX + pattern : pattern);
+    }
+}
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreProcessor.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreBuilder.java
similarity index 59%
rename from 
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreProcessor.java
rename to 
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreBuilder.java
index d88e60c5..1698d85b 100644
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreProcessor.java
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreBuilder.java
@@ -18,19 +18,23 @@
  */
 package org.apache.rat.config.exclusion.fileProcessors;
 
-import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.rat.config.exclusion.MatcherSet;
 import org.apache.rat.document.DocumentName;
 
 import static java.lang.String.format;
 
 /**
  * A processor for the {@code .hgignore} files.
+ * @see <a href="https://wiki.mercurial-scm.org/.hgignore";>Mecurial how to 
ignore files</a>
+ * @see <a href="https://www.selenic.com/mercurial/hgignore.5.html";>syntax for 
Mercurial ignore files</a>
  */
-public final class HgIgnoreProcessor extends DescendingFileProcessor {
+public final class  HgIgnoreBuilder extends AbstractFileProcessorBuilder {
     /**
      * The state enumeration for the processor. When processing the file the 
processor changes
      * syntax state depending on the input.
@@ -48,30 +52,38 @@ public final class HgIgnoreProcessor extends 
DescendingFileProcessor {
     private Syntax state;
 
     /**
-     * Constructs the .hgignore processor.
+     * Constructs the {@code .hgignore} processor.
      */
-    public HgIgnoreProcessor() {
-        super(".hgignore", "#");
+    public HgIgnoreBuilder() {
+        super(".hgignore", "#", true);
         state = Syntax.REGEXP;
     }
 
     @Override
-    protected List<String> process(final DocumentName baseName) {
+    protected MatcherSet process(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName root, final DocumentName documentName) {
         state = Syntax.REGEXP;
-        return super.process(baseName);
+        return super.process(matcherSetConsumer, root, documentName);
     }
 
     @Override
-    public String modifyEntry(final DocumentName baseName, final String entry) 
{
+    public Optional<String> modifyEntry(final Consumer<MatcherSet> 
matcherSetConsumer, final DocumentName documentName, final String entry) {
         Matcher m = SYNTAX_CHECK.matcher(entry.toLowerCase(Locale.ROOT));
         if (m.matches()) {
             state = Syntax.valueOf(m.group(1).toUpperCase());
-            return null;
+            return Optional.empty();
         }
+        /*
+         Neither glob nor regexp patterns are rooted. A glob-syntax pattern of 
the form *.c will match a file ending in .c
+         in any directory, and a regexp pattern of the form \.c$ will do the 
same. To root a regexp pattern, start it with ^.
+         */
         if (state == Syntax.REGEXP) {
             String pattern = entry.startsWith("^") ? entry.substring(1) : ".*" 
+ entry;
-            return format(REGEX_FMT, pattern);
+            return Optional.of(format(REGEX_FMT, pattern));
+        } else {
+            if (entry.startsWith("*")) {
+                return Optional.of("**/" + entry);
+            }
         }
-        return entry;
+        return Optional.of(entry);
     }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/help/Help.java 
b/apache-rat-core/src/main/java/org/apache/rat/help/Help.java
index 06202375..8623c175 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/help/Help.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/help/Help.java
@@ -85,7 +85,7 @@ public final class Help extends AbstractHelp {
             helpFormatter.printWrapped(writer, helpFormatter.getWidth(), 
helpFormatter.getLeftPadding() + HELP_PADDING + HELP_PADDING,
                     argumentPadding + "Provides a path matcher: " + 
sc.hasStaticDocumentNameMatcher());
             helpFormatter.printWrapped(writer, helpFormatter.getWidth(), 
helpFormatter.getLeftPadding() + HELP_PADDING + HELP_PADDING,
-                    argumentPadding + "Provides a file processor: " + 
sc.fileProcessor().hasNext());
+                    argumentPadding + "Provides a file processor: " + 
sc.fileProcessorBuilder().hasNext());
         }
         writer.println("\nA path matcher will match specific information about 
the file.");
         writer.println("\nA file processor will process the associated 
\"ignore\" file for include and exclude directives");
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/StandardCollectionTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/StandardCollectionTest.java
index 5a9186f8..9f62c6fc 100644
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/StandardCollectionTest.java
+++ 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/StandardCollectionTest.java
@@ -36,7 +36,7 @@ public class StandardCollectionTest {
     @ParameterizedTest
     @MethodSource("collectionData")
     public void testState(StandardCollection scm, boolean hasFileProcessor, 
boolean hasPathMatchSupplier, boolean hasPatterns) {
-        assertEquals(hasFileProcessor, scm.fileProcessor().hasNext(), () -> 
scm.name() + " FileProcessor state wrong.");
+        assertEquals(hasFileProcessor, scm.fileProcessorBuilder().hasNext(), 
() -> scm.name() + " MatcherSet state wrong.");
         assertEquals(hasPathMatchSupplier, scm.hasStaticDocumentNameMatcher(), 
() -> scm.name() + " PathMatcherSupplier state wrong.");
         assertEquals(hasPatterns, !scm.patterns().isEmpty(), () -> scm.name() 
+ " patterns state wrong.");
     }
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/AbstractIgnoreBuilderTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/AbstractIgnoreBuilderTest.java
new file mode 100644
index 00000000..09c330f8
--- /dev/null
+++ 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/AbstractIgnoreBuilderTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.rat.config.exclusion.fileProcessors;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Stream;
+import org.apache.rat.config.exclusion.MatcherSet;
+import org.apache.rat.config.exclusion.plexus.SelectorUtils;
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.document.DocumentNameMatcher;
+import org.apache.rat.document.DocumentNameMatcherTest;
+import org.apache.rat.document.FSInfoTest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import org.junit.jupiter.params.provider.Arguments;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The base class for FileProcessor builder tests.
+ * Provides supporting methods for creating test files and for validating 
results.
+ */
+public class AbstractIgnoreBuilderTest {
+
+    @TempDir
+    protected Path tmpPath;
+    protected DocumentName baseName;
+
+    @BeforeEach
+    protected void setup() throws IOException {
+        baseName = DocumentName.builder(tmpPath.toFile()).build();
+    }
+
+    @AfterEach
+    @EnabledOnOs(OS.WINDOWS)
+    void reset() {
+        baseName = null;
+    }
+
+    /**
+     * This method is a known workaround for
+     * {@link <a href="https://github.com/junit-team/junit5/issues/2811";>junit 
5 issue #2811</a> }.
+     */
+    @AfterEach
+    @EnabledOnOs(OS.WINDOWS)
+    void cleanUp() {
+        System.gc();
+    }
+
+    /**
+     * Writes a text file to the baseDir directory.
+     * @param name the name of the file.
+     * @param lines the lines to write to the file.
+     * @return the File that was written.
+     * @throws IOException if file cannot be created.
+     */
+    protected File writeFile(String name, Iterable<String> lines) throws 
IOException {
+        File file = new File(tmpPath.toFile(), name);
+        try (PrintWriter writer = new PrintWriter(new FileWriter(file))) {
+            lines.forEach(writer::println);
+        }
+        return file;
+    }
+
+    /**
+     * Asserts the correctness of the excluder. An excluder returns false if 
the document name is matched.
+     * @param builder An FileProcessorBuilder that will create the excluder.
+     * @param matching the matching strings.
+     * @param notMatching the non-matching strings.
+     */
+    protected void assertCorrect(AbstractFileProcessorBuilder builder, 
Iterable<String> matching, Iterable<String> notMatching) {
+        assertCorrect(builder.build(baseName), baseName, matching, 
notMatching);
+    }
+
+    /**
+     * Asserts the correctness of the excluder. An excluder returns false if 
the document name is matched.
+     * @param matcherSets the list of matchers to create the 
DocumentNameMatcher from.
+     * @param baseDir the base directory for the excluder test.
+     * @param matching the matching strings.
+     * @param notMatching the non-matching strings.
+     */
+    protected void assertCorrect(List<MatcherSet> matcherSets, DocumentName 
baseDir, Iterable<String> matching, Iterable<String> notMatching) {
+        DocumentNameMatcher excluder = 
MatcherSet.merge(matcherSets).createMatcher();
+        for (String name : matching) {
+            DocumentName docName = 
baseDir.resolve(SelectorUtils.extractPattern(name, 
baseDir.getDirectorySeparator()));
+            assertThat(excluder.matches(docName)).as(() -> 
DocumentNameMatcherTest.processDecompose(excluder, docName)).isFalse();
+        }
+        for (String name : notMatching) {
+            DocumentName docName = 
baseDir.resolve(SelectorUtils.extractPattern(name, 
baseDir.getDirectorySeparator()));
+            assertThat(excluder.matches(docName)).as(() -> 
DocumentNameMatcherTest.processDecompose(excluder, docName)).isTrue();
+        }
+    }
+}
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/AbstractIgnoreProcessorTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/AbstractIgnoreProcessorTest.java
deleted file mode 100644
index 741a1ab4..00000000
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/AbstractIgnoreProcessorTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.rat.config.exclusion.fileProcessors;
-
-import org.apache.rat.document.DocumentName;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.condition.EnabledOnOs;
-import org.junit.jupiter.api.condition.OS;
-import org.junit.jupiter.api.io.TempDir;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-
-public class AbstractIgnoreProcessorTest {
-
-    @TempDir
-    protected File baseDir;
-    protected DocumentName baseName;
-
-    @BeforeEach
-    public void setup() {
-        baseName = DocumentName.builder(baseDir).build();
-    }
-
-    /**
-     * This method is a known workaround for
-     * {@link <a href="https://github.com/junit-team/junit5/issues/2811";>junit 
5 issue #2811</a> }.
-     */
-    @AfterEach
-    @EnabledOnOs(OS.WINDOWS)
-    void cleanUp() {
-        System.gc();
-    }
-
-    protected File writeFile(String name, Iterable<String> lines) throws 
IOException {
-        File file = new File(baseDir, name);
-        try (PrintWriter writer = new PrintWriter(new FileWriter(file))) {
-            lines.forEach(writer::println);
-        }
-        return file;
-    }
-
-}
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessorTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreBuilderTest.java
similarity index 58%
copy from 
apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessorTest.java
copy to 
apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreBuilderTest.java
index 7900fd7e..dc3c352c 100644
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessorTest.java
+++ 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/BazaarIgnoreBuilderTest.java
@@ -18,32 +18,26 @@
  */
 package org.apache.rat.config.exclusion.fileProcessors;
 
-import java.util.ArrayList;
-
-import org.apache.rat.utils.ExtendedIterator;
 import org.junit.jupiter.api.Test;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class CVSFileProcessorTest extends AbstractIgnoreProcessorTest {
+public class BazaarIgnoreBuilderTest extends AbstractIgnoreBuilderTest {
 
     @Test
     public void processExampleFileTest() throws IOException {
         String[] lines = {
-                "thingone thingtwo", System.lineSeparator(), "one_fish", 
"two_fish", "", "red_* blue_*"};
+                "# a comment", "*.elc", "*.pyc", "*~", System.lineSeparator(),
+                "# switch to regexp syntax.",  "RE:^\\.pc" };
 
-        List<String> expected = 
ExtendedIterator.create(Arrays.asList("thingone", "thingtwo", "one_fish", 
"two_fish", "red_*", "blue_*").iterator())
-                .map(s -> new File(baseDir, s).getPath()).addTo(new 
ArrayList<>());
+        List<String> matching = Arrays.asList("test.elc", "test.pyc", 
"test.thing~", ".pc");
+        List<String> notMatching = Arrays.asList("test.foo", ".pc/stuff", 
"subidr/test.elc");
 
-        writeFile(".cvsignore", Arrays.asList(lines));
+        writeFile(".bzrignore", Arrays.asList(lines));
 
-        CVSFileProcessor processor = new CVSFileProcessor();
-        List<String> actual = processor.apply(baseName);
-        assertEquals(expected, actual);
+        assertCorrect(new BazaarIgnoreBuilder(), matching, notMatching);
     }
+
 }
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessorTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSIgnoreBuilderTest.java
similarity index 65%
rename from 
apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessorTest.java
rename to 
apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSIgnoreBuilderTest.java
index 7900fd7e..7a0d37dc 100644
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSFileProcessorTest.java
+++ 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/CVSIgnoreBuilderTest.java
@@ -18,32 +18,29 @@
  */
 package org.apache.rat.config.exclusion.fileProcessors;
 
-import java.util.ArrayList;
-
-import org.apache.rat.utils.ExtendedIterator;
+import org.apache.rat.config.exclusion.MatcherSet;
+import org.apache.rat.document.DocumentNameMatcher;
 import org.junit.jupiter.api.Test;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class CVSFileProcessorTest extends AbstractIgnoreProcessorTest {
+public class CVSIgnoreBuilderTest extends AbstractIgnoreBuilderTest {
 
     @Test
     public void processExampleFileTest() throws IOException {
         String[] lines = {
                 "thingone thingtwo", System.lineSeparator(), "one_fish", 
"two_fish", "", "red_* blue_*"};
 
-        List<String> expected = 
ExtendedIterator.create(Arrays.asList("thingone", "thingtwo", "one_fish", 
"two_fish", "red_*", "blue_*").iterator())
-                .map(s -> new File(baseDir, s).getPath()).addTo(new 
ArrayList<>());
+        List<String> matching = Arrays.asList("thingone", "thingtwo", 
"one_fish", "two_fish", "red_fish", "blue_fish");
+        List<String> notMatching = Arrays.asList("thing", "two", "fish_red", 
"subdir/two_fish", "subdir/red_fish", "subdir/blue_fish");
 
         writeFile(".cvsignore", Arrays.asList(lines));
 
-        CVSFileProcessor processor = new CVSFileProcessor();
-        List<String> actual = processor.apply(baseName);
-        assertEquals(expected, actual);
+        CVSIgnoreBuilder processor = new CVSIgnoreBuilder();
+        DocumentNameMatcher matcher = 
MatcherSet.merge(processor.build(baseName)).createMatcher();
+
+        assertCorrect(new CVSIgnoreBuilder(), matching, notMatching);
     }
 }
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/DescendingFileProcessorTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/DescendingFileProcessorTest.java
deleted file mode 100644
index e3343c1d..00000000
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/DescendingFileProcessorTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.rat.config.exclusion.fileProcessors;
-
-import java.util.ArrayList;
-import org.apache.rat.document.DocumentName;
-import org.apache.rat.utils.ExtendedIterator;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class DescendingFileProcessorTest extends AbstractIgnoreProcessorTest {
-
-    @Test
-    public void singleDirectoryTest() throws IOException {
-        String[] lines = {"*.ext", "fname.*", "**/fname.ext"};
-
-        List<String> expected = 
ExtendedIterator.create(Arrays.asList(lines).iterator())
-                .map(s -> new File(baseDir, s).getPath()).addTo(new 
ArrayList<>());
-
-        writeFile("test.txt", Arrays.asList(lines));
-        DocumentName documentName = DocumentName.builder(baseDir).build();
-
-        DescendingFileProcessor processor = new 
DescendingFileProcessor("test.txt", "#");
-        List<String> actual = processor.apply(baseName);
-        assertThat(actual).isEqualTo(expected);
-    }
-
-    @Test
-    public void layeredDirectoryTest() throws IOException {
-        String[] lines = {"*.ext", "fname.*", "**/fname.ext"};
-
-        List<String> expected = 
ExtendedIterator.create(Arrays.asList(lines).iterator())
-                .map(s -> new File(baseDir, s).getPath()).addTo(new 
ArrayList<>());
-
-        writeFile("test.txt", Arrays.asList(lines));
-
-        File subdir = new File(baseDir, "subdir");
-        assertThat(subdir.mkdirs()).as("Could not make subdirectory").isTrue();
-
-        writeFile("subdir/test.txt", Collections.singletonList("foo.*"));
-        expected.add(new File(subdir, "foo.*").getPath());
-
-        DescendingFileProcessor processor = new 
DescendingFileProcessor("test.txt", "#");
-        List<String> actual = processor.apply(baseName);
-        assertThat(actual).isEqualTo(expected);
-    }
-}
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/GitFileProcessorTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/GitFileProcessorTest.java
deleted file mode 100644
index 3166b4ca..00000000
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/GitFileProcessorTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.rat.config.exclusion.fileProcessors;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.stream.Stream;
-import org.apache.rat.document.DocumentName;
-import org.apache.rat.document.DocumentNameMatcher;
-import org.apache.rat.utils.ExtendedIterator;
-import org.junit.Ignore;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class GitFileProcessorTest extends AbstractIgnoreProcessorTest {
-
-    @Test
-    public void processExampleFileTest() throws IOException {
-        String[] lines = {
-                "# somethings",
-                "!thingone", "thing*", System.lineSeparator(),
-                "# some fish",
-                "**/fish", "*_fish",
-                "# some colorful directories",
-                "red/", "blue/*/"};
-
-        List<String> expected = 
ExtendedIterator.create(Arrays.asList("**/thing*", "**/fish", 
"**/*_fish").iterator())
-                .map(s -> new File(baseDir, s).getPath()).addTo(new 
ArrayList<>());
-        expected.add(0, "!"+new File(baseDir, "**/thingone").getPath());
-        // "thingone",
-        writeFile(".gitignore", Arrays.asList(lines));
-
-        GitFileProcessor processor = new GitFileProcessor();
-        List<String> actual = processor.apply(baseName);
-        assertThat(actual).isEqualTo(expected);
-
-        actual.clear();
-        processor.customDocumentNameMatchers().forEach(x -> 
actual.add(x.toString()));
-        expected.clear();
-        ExtendedIterator.create(Arrays.asList("**/red", "blue/*").iterator())
-                .map(s -> String.format("and(isDirectory, %s)", s))
-                        .forEachRemaining(expected::add);
-        assertThat(actual).isEqualTo(expected);
-    }
-
-    // see https://git-scm.com/docs/gitignore
-    @ParameterizedTest
-    @MethodSource("modifyEntryData")
-    public void modifyEntryTest(String source, String expected) {
-        GitFileProcessor underTest = new GitFileProcessor();
-        DocumentName testName = 
DocumentName.builder().setName("GitFileProcessorTest").setBaseName("testDir").build();
-        if (source.endsWith("/")) {
-            assertThat(underTest.modifyEntry(testName, 
source)).isEqualTo(null);
-            Iterator<DocumentNameMatcher> iter = 
underTest.customDocumentNameMatchers().iterator();
-            assertThat(iter).hasNext();
-            
assertThat(iter.next().toString()).isEqualTo(String.format("and(isDirectory, 
%s)", expected));
-        } else {
-            assertThat(underTest.modifyEntry(testName, 
source)).isEqualTo(expected);
-            
assertThat(underTest.customDocumentNameMatchers().iterator().hasNext()).isFalse();
-        }
-    }
-
-    private static Stream<Arguments> modifyEntryData() {
-        List<Arguments> lst = new ArrayList<>();
-
-        lst.add(Arguments.of("\\#filename", "**/#filename"));
-
-        lst.add(Arguments.of("!#filename", "!**/#filename"));
-        lst.add(Arguments.of("\\#filename", "**/#filename"));
-        lst.add(Arguments.of("!#filename", "!**/#filename"));
-        lst.add(Arguments.of("/filename", "filename"));
-        lst.add(Arguments.of("file/name", "file/name"));
-        lst.add(Arguments.of("/file/name", "file/name"));
-        lst.add(Arguments.of("filename", "**/filename"));
-        lst.add(Arguments.of("filename/", "**/filename"));
-        lst.add(Arguments.of("/filename/", "filename"));
-
-        return lst.stream();
-    }
-
-    @Test
-    @Ignore("RAT-335 ")
-    public void test_RAT_335() {
-        GitFileProcessor underTest = new GitFileProcessor();
-        URL url = 
GitFileProcessorTest.class.getClassLoader().getResource("RAT_355/src/");
-        File file = new File(url.getFile());
-
-        DocumentName documentName = 
DocumentName.builder(file).setBaseName(File.separator).build();
-        List<String> lst = underTest.apply(documentName);
-        System.out.println(lst);
-    }
-}
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/GitIgnoreBuilderTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/GitIgnoreBuilderTest.java
new file mode 100644
index 00000000..60f73442
--- /dev/null
+++ 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/GitIgnoreBuilderTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.rat.config.exclusion.fileProcessors;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.apache.rat.config.exclusion.MatcherSet;
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.document.DocumentNameMatcher;
+import org.apache.rat.document.FSInfoTest;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GitIgnoreBuilderTest extends AbstractIgnoreBuilderTest {
+
+    @Test
+    public void processExampleFileTest() throws IOException {
+        try {
+            String[] lines = {
+                    "# somethings",
+                    "!thingone", "thing*", System.lineSeparator(),
+                    "# some fish",
+                    "**/fish", "*_fish",
+                    "# some colorful directories",
+                    "red/", "blue/*/"};
+            List<String> matches = Arrays.asList("some/things", "some/fish", 
"another/red_fish");
+
+            List<String> notMatches = Arrays.asList("some/thingone", 
"thingone");
+
+            writeFile(".gitignore", Arrays.asList(lines));
+
+            assertCorrect(new GitIgnoreBuilder(), matches, notMatches);
+        } finally {
+            System.getProperties().remove("FSInfo");
+        }
+    }
+
+    // see https://git-scm.com/docs/gitignore
+    @ParameterizedTest
+    @MethodSource("modifyEntryData")
+    public void modifyEntryTest(DocumentName.FSInfo fsInfo, String source, 
String expected) {
+        GitIgnoreBuilder underTest = new GitIgnoreBuilder();
+        DocumentName testName = 
DocumentName.builder(fsInfo).setName("GitIgnoreBuilderTest").setBaseName("testDir").build();
+        List<MatcherSet> matcherSets = new ArrayList<>();
+        Optional<String> entry = underTest.modifyEntry(matcherSets::add, 
testName, source);
+
+        if (source.endsWith("/")) {
+            assertThat(entry).isNotPresent();
+            assertThat(matcherSets).hasSize(1);
+            DocumentNameMatcher matcher = matcherSets.get(0).createMatcher();
+            assertThat(matcher.toString()).isEqualTo(expected);
+        } else {
+            assertThat(entry.get()).isEqualTo(expected);
+        }
+    }
+
+    private static Stream<Arguments> modifyEntryData() {
+        List<Arguments> lst = new ArrayList<>();
+        for (DocumentName.FSInfo fsInfo : FSInfoTest.TEST_SUITE) {
+            lst.add(Arguments.of(fsInfo, "\\#filename", "**/#filename"));
+
+            lst.add(Arguments.of(fsInfo, "!#filename", "!**/#filename"));
+            lst.add(Arguments.of(fsInfo, "\\#filename", "**/#filename"));
+            lst.add(Arguments.of(fsInfo, "!#filename", "!**/#filename"));
+            lst.add(Arguments.of(fsInfo, "/filename", "filename"));
+            lst.add(Arguments.of(fsInfo, "file/name", "file/name"));
+            lst.add(Arguments.of(fsInfo, "/file/name", "file/name"));
+            lst.add(Arguments.of(fsInfo, "filename", "**/filename"));
+            lst.add(Arguments.of(fsInfo, "filename/", "not(and(isDirectory, 
**/filename))"));
+            lst.add(Arguments.of(fsInfo, "/filename/", "not(and(isDirectory, 
filename))"));
+            // inclusion by itself becomes nothing.
+            lst.add(Arguments.of(fsInfo, "!filename/", "TRUE"));
+        }
+        return lst.stream();
+    }
+
+    private void assertMatches(DocumentName documentName, DocumentNameMatcher 
matcher, String[] matching, String[] notMatching) {
+        for (String test : matching) {
+            DocumentName name = documentName.resolve(test);
+            assertThat(matcher.matches(name)).as(test).isTrue();
+        }
+        for (String test: notMatching) {
+            DocumentName name = documentName.resolve(test);
+            assertThat(matcher.matches(name)).as(test).isFalse();
+        }
+    }
+
+    @Test
+    public void test_RAT_335() {
+        GitIgnoreBuilder underTest = new GitIgnoreBuilder();
+        URL url = 
GitIgnoreBuilderTest.class.getClassLoader().getResource("GitIgnoreBuilderTest/src/");
+        File file = new File(url.getFile());
+
+        DocumentName documentName = DocumentName.builder(file).build();
+        List<MatcherSet> matcherSets = underTest.build(documentName);
+        DocumentNameMatcher matcher = 
MatcherSet.merge(matcherSets).createMatcher();
+
+        DocumentName candidate = DocumentName.builder()
+                
.setName("/home/claude/apache/creadur-rat/apache-rat-core/target/test-classes/GitIgnoreBuilderTest/src/dir1/file1.log")
+                
.setBaseName("home/claude/apache/creadur-rat/apache-rat-core/target/test-classes/GitIgnoreBuilderTest/src/").build();
+        System.out.println("Decomposition for "+candidate);
+
+        assertThat(matcher.toString()).isEqualTo("matcherSet(or('included 
dir1/.gitignore', 'included .gitignore'), or('excluded dir1/.gitignore', 
**/.gitignore, 'excluded .gitignore'))");
+
+        List<String> notMatching = Arrays.asList("README.txt", "dir1/dir1.md", 
"dir2/dir2.txt", "dir3/file3.log", "dir1/file1.log");
+
+        List<String> matching = Arrays.asList(".gitignore", "root.md", 
"dir1/.gitignore", "dir1/dir1.txt",  "dir2/dir2.md", "dir3/dir3.log");
+
+        assertCorrect(matcherSets, documentName.getBaseDocumentName(), 
matching, notMatching);
+    }
+}
diff --git 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreProcessorTest.java
 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreBuilderTest.java
similarity index 53%
rename from 
apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreProcessorTest.java
rename to 
apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreBuilderTest.java
index 3085792c..d91a157e 100644
--- 
a/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreProcessorTest.java
+++ 
b/apache-rat-core/src/test/java/org/apache/rat/config/exclusion/fileProcessors/HgIgnoreBuilderTest.java
@@ -18,49 +18,37 @@
  */
 package org.apache.rat.config.exclusion.fileProcessors;
 
-import java.util.ArrayList;
-import org.apache.rat.utils.ExtendedIterator;
 import org.junit.jupiter.api.Test;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 
-import static java.lang.String.format;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class HgIgnoreProcessorTest extends AbstractIgnoreProcessorTest {
+public class HgIgnoreBuilderTest extends AbstractIgnoreBuilderTest {
 
     @Test
     public void processExampleFileTest() throws IOException {
         String[] lines = {
-        "# use glob syntax.", "syntax: glob", "*.elc", "*.pyc", "*~", 
System.lineSeparator(),
-            "# switch to regexp syntax.", "syntax: regexp", "^\\.pc/" };
+        "# use glob syntax.", "syntax: glob", "*.elc", "*.pyc", "*~", "*.c", 
System.lineSeparator(),
+            "# switch to regexp syntax.", "syntax: regexp", "^\\.pc" };
 
-        List<String> expected = ExtendedIterator.create(Arrays.asList("*.elc", 
"*.pyc", "*~").iterator())
-                .map(s -> new File(baseDir, s).getPath()).addTo(new 
ArrayList<>());
-        expected.add(format("%%regex[\\Q%s%s\\E%s]", baseDir.getPath(), 
File.separatorChar, "\\.pc/"));
+        List<String> matching = Arrays.asList("test.elc", "test.pyc", 
"subdir/test.pyc", "test.thing~", ".pc", "foo.c", "/subdir/foo.c" );
+        List<String> notMatching = Arrays.asList("test.foo", 
"test.thing~/subdir", ".pc/subdir");
 
         writeFile(".hgignore", Arrays.asList(lines));
 
-        HgIgnoreProcessor processor = new HgIgnoreProcessor();
-        List<String> actual = processor.apply(baseName);
-        assertEquals(expected, actual);
+        assertCorrect(new HgIgnoreBuilder(), matching, notMatching);
     }
 
     @Test
     public void processDefaultFileTest() throws IOException {
-        String[] lines = {"^[A-Z]*\\.txt", "[0-9]*\\.txt"};
+        String[] lines = {"^[A-Z]*\\.txt", "[0-9]+\\.txt"};
 
-        List<String> expected = 
ExtendedIterator.create(Arrays.asList("[A-Z]*\\.txt", 
".*[0-9]*\\.txt").iterator())
-                .map(s -> format("%%regex[\\Q%s%s\\E%s]", baseDir.getPath(), 
File.separatorChar, s))
-                .addTo(new ArrayList<>());
+        List<String> matching = Arrays.asList("ABIGNAME.txt", "endsIn9.txt");
+        List<String> notMatching = Arrays.asList("asmallName.txt", 
"endsin.doc");
 
         writeFile(".hgignore", Arrays.asList(lines));
 
-        HgIgnoreProcessor processor = new HgIgnoreProcessor();
-        List<String> actual = processor.apply(baseName);
-        assertEquals(expected, actual);
+        assertCorrect(new HgIgnoreBuilder(), matching, notMatching);
     }
 }
diff --git a/apache-rat-core/src/test/resources/RAT_355/commandLine.txt 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/commandLine.txt
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/commandLine.txt
rename to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/commandLine.txt
diff --git a/apache-rat-core/src/test/resources/RAT_355/expected-message.txt 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/expected-message.txt
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/expected-message.txt
rename to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/expected-message.txt
diff --git a/apache-rat-core/src/test/resources/RAT_355/pom.xml 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/pom.xml
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/pom.xml
rename to apache-rat-core/src/test/resources/GitIgnoreBuilderTest/pom.xml
diff --git 
a/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/.gitignore 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/.gitignore
new file mode 100644
index 00000000..8855fa80
--- /dev/null
+++ b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/.gitignore
@@ -0,0 +1,7 @@
+*.md
+
+# This makes it ignore dir3/dir3.log and dir3/file3.log
+*.log
+
+# This makes it "unignore" dir3/file3.log
+!file*.log
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/README.txt 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/README.txt
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/src/README.txt
rename to apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/README.txt
diff --git 
a/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/.gitignore 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/.gitignore
new file mode 100644
index 00000000..26fd5c95
--- /dev/null
+++ 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/.gitignore
@@ -0,0 +1,3 @@
+*.txt
+!dir1.md
+file1.log
\ No newline at end of file
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/dir1.md
similarity index 100%
copy from apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md
copy to apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/dir1.md
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir2/dir2.txt 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/dir1.txt
similarity index 100%
copy from apache-rat-core/src/test/resources/RAT_355/src/dir2/dir2.txt
copy to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/dir1.txt
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir3/file3.log 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/file1.log
similarity index 100%
copy from apache-rat-core/src/test/resources/RAT_355/src/dir3/file3.log
copy to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir1/file1.log
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir2/dir2.md
similarity index 100%
copy from apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md
copy to apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir2/dir2.md
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir2/dir2.txt 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir2/dir2.txt
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/src/dir2/dir2.txt
rename to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir2/dir2.txt
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir3/dir3.log
similarity index 100%
copy from apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md
copy to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir3/dir3.log
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir3/file3.log 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir3/file3.log
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/src/dir3/file3.log
rename to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/dir3/file3.log
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/invoker.properties 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/invoker.properties
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/src/invoker.properties
rename to 
apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/invoker.properties
diff --git a/apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/root.md
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/src/dir1/dir1.md
rename to apache-rat-core/src/test/resources/GitIgnoreBuilderTest/src/root.md
diff --git a/apache-rat-core/src/test/resources/RAT_355/verify.groovy 
b/apache-rat-core/src/test/resources/GitIgnoreBuilderTest/verify.groovy
similarity index 100%
rename from apache-rat-core/src/test/resources/RAT_355/verify.groovy
rename to apache-rat-core/src/test/resources/GitIgnoreBuilderTest/verify.groovy

Reply via email to