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 e7397ffe RAT-98: Add matcher set  (#430)
e7397ffe is described below

commit e7397ffecdb8704a7139886c87217aaf8dc60bb2
Author: Claude Warren <[email protected]>
AuthorDate: Tue Feb 4 15:29:56 2025 +0100

    RAT-98: Add matcher set  (#430)
    
    * added working directory
    
    * Added FSInfo to handle file system differences
    
    * fixed pattern match
    
    * added more descriptive failure messages
    
    * added file converter + test
    
    * fixed file list walker
    
    * implemented matcher set in ExclusionProcessor
    
    ---------
    
    Co-authored-by: P. Ottlinger <[email protected]>
---
 .../rat/config/exclusion/ExclusionProcessor.java   | 217 ++++++++++++---------
 .../rat/config/exclusion/ExclusionUtils.java       |  66 +++++--
 .../apache/rat/config/exclusion/MatcherSet.java    | 178 +++++++++++++++++
 .../rat/config/exclusion/StandardCollection.java   |  38 ++--
 4 files changed, 384 insertions(+), 115 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 c1de611f..d5b3ebe4 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
@@ -24,8 +24,8 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.stream.Collectors;
 
-import org.apache.rat.config.exclusion.plexus.MatchPatterns;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.document.DocumentNameMatcher;
 import org.apache.rat.utils.DefaultLog;
@@ -85,10 +85,8 @@ public class ExclusionProcessor {
      * @return this
      */
     public ExclusionProcessor addIncludedPatterns(final Iterable<String> 
patterns) {
-        List<String> lst = new ArrayList<>();
-        patterns.forEach(lst::add);
-        DefaultLog.getInstance().info(format("Including patterns: %s", 
String.join(", ", lst)));
-        includedPatterns.addAll(lst);
+        DefaultLog.getInstance().info(format("Including patterns: %s", 
String.join(", ", patterns)));
+        patterns.forEach(includedPatterns::add);
         resetLastMatcher();
         return this;
     }
@@ -140,10 +138,8 @@ public class ExclusionProcessor {
      * @return this
      */
     public ExclusionProcessor addExcludedPatterns(final Iterable<String> 
patterns) {
-        List<String> lst = new ArrayList<>();
-        patterns.forEach(lst::add);
-        DefaultLog.getInstance().info(format("Excluding patterns: %s", 
String.join(", ", lst)));
-        excludedPatterns.addAll(lst);
+        DefaultLog.getInstance().info(format("Excluding patterns: %s", 
String.join(", ", patterns)));
+        patterns.forEach(excludedPatterns::add);
         resetLastMatcher();
         return this;
     }
@@ -175,22 +171,6 @@ public class ExclusionProcessor {
         return this;
     }
 
-    /**
-     * Adds to lists of qualified file patterns. Non-matching patterns start 
with a {@code !}.
-     * @param matching the list to put matching file patterns into.
-     * @param notMatching the list to put non-matching files patterns into.
-     * @param patterns the patterns to match.
-     */
-    private void segregateList(final Set<String> matching, final Set<String> 
notMatching,
-                               final Iterable<String> patterns) {
-        if (patterns.iterator().hasNext()) {
-            
ExtendedIterator.create(patterns.iterator()).filter(ExclusionUtils.MATCH_FILTER).forEachRemaining(matching::add);
-            
ExtendedIterator.create(patterns.iterator()).filter(ExclusionUtils.NOT_MATCH_FILTER)
-                    .map(s -> s.substring(1))
-                    .forEachRemaining(notMatching::add);
-        }
-    }
-
     /**
      * Creates a Document name matcher that will return {@code false} on any
      * document that is excluded.
@@ -204,84 +184,137 @@ public class ExclusionProcessor {
         if (lastMatcher == null || !basedir.equals(lastMatcherBaseDir)) {
             lastMatcherBaseDir = basedir;
 
-            final Set<String> incl = new TreeSet<>();
-            final Set<String> excl = new TreeSet<>();
-            final List<DocumentNameMatcher> inclMatchers = new ArrayList<>();
-
             // add the file processors
-            for (StandardCollection sc : fileProcessors) {
-                ExtendedIterator<FileProcessor> iter =  sc.fileProcessor();
-                if (iter.hasNext()) {
-                    iter.forEachRemaining(fp -> {
-                        segregateList(excl, incl, fp.apply(basedir));
-                        
fp.customDocumentNameMatchers().forEach(inclMatchers::add);
-                    });
-                } else {
-                    DefaultLog.getInstance().info(String.format("%s does not 
have a fileProcessor.", sc));
-                }
-            }
+            final List<MatcherSet> matchers = extractFileProcessors(basedir);
+            final MatcherSet.Builder fromCommandLine = new 
MatcherSet.Builder();
+            DocumentName.Builder nameBuilder = 
DocumentName.builder(basedir).setBaseName(basedir);
+            extractPatterns(nameBuilder, fromCommandLine);
+            extractCollectionPatterns(nameBuilder, fromCommandLine);
+            extractCollectionMatchers(fromCommandLine);
+            extractPaths(fromCommandLine);
+            matchers.add(fromCommandLine.build());
 
-            // add the standard patterns
-            segregateList(incl, excl, new 
FileProcessor(includedPatterns).apply(basedir));
-            segregateList(excl, incl, new 
FileProcessor(excludedPatterns).apply(basedir));
+            lastMatcher = MatcherSet.merge(matchers).createMatcher();
+            DefaultLog.getInstance().debug(format("Created matcher set for 
%s%n%s", basedir.getName(),
+                    lastMatcher));
+        }
+        return lastMatcher;
+    }
 
-            // add the collection patterns
-            for (StandardCollection sc : includedCollections) {
-                Set<String> patterns = sc.patterns();
-                if (patterns.isEmpty()) {
-                    DefaultLog.getInstance().info(String.format("%s does not 
have a defined collection for inclusion.", sc));
-                } else {
-                    segregateList(incl, excl, new 
FileProcessor(sc.patterns()).apply(basedir));
-                }
-            }
-            for (StandardCollection sc : excludedCollections) {
-                Set<String> patterns = sc.patterns();
-                if (patterns.isEmpty()) {
-                    DefaultLog.getInstance().info(String.format("%s does not 
have a defined collection for exclusion.", sc));
-                } else {
-                    segregateList(excl, incl, new 
FileProcessor(sc.patterns()).apply(basedir));
-                }
-            }
+    /**
+     * Extracts the file processors from {@link #fileProcessors}.
+     * @param basedir The directory to base the file processors on.
+     * @return a list of MatcherSets that are created for each {@link 
#fileProcessors} entry.
+     */
+    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());
+        }
+        return fileProcessorList;
+    }
 
-            // add the matchers
-            ExtendedIterator.create(includedCollections.iterator())
-                    .map(StandardCollection::staticDocumentNameMatcher)
-                    .filter(Objects::nonNull)
-                    .forEachRemaining(inclMatchers::add);
+    /**
+     * Converts the pattern to use the directory separator specified by the 
document name and localises it for
+     * exclusion processing.
+     * @param documentName The document name to adjust the pattern against.
+     * @param pattern the pattern.
+     * @return the prepared pattern.
+     */
+    private String preparePattern(final DocumentName documentName, final 
String pattern) {
+        return ExclusionUtils.qualifyPattern(documentName,
+                        ExclusionUtils.convertSeparator(pattern, "/", 
documentName.getDirectorySeparator()));
+    }
 
-            List<DocumentNameMatcher> exclMatchers = 
ExtendedIterator.create(excludedCollections.iterator())
-                    .map(StandardCollection::staticDocumentNameMatcher)
-                    .filter(Objects::nonNull)
-                    .addTo(new ArrayList<>());
+    /**
+     * Extracts {@link #includedPatterns} and {@link #excludedPatterns} into 
the specified matcherBuilder.
+     * @param nameBuilder The name builder for the pattern. File names are 
resolved against the generated name.
+     * @param matcherBuilder the MatcherSet.Builder to add the patterns to.
+     */
+    private void extractPatterns(final DocumentName.Builder nameBuilder, final 
MatcherSet.Builder matcherBuilder) {
+        DocumentName name = nameBuilder.setName("Patterns").build();
+        if (!excludedPatterns.isEmpty()) {
+            matcherBuilder.addExcluded(name, excludedPatterns.stream()
+                    .map(s -> preparePattern(name, s))
+                    .collect(Collectors.toSet()));
+        }
+        if (!includedPatterns.isEmpty()) {
+            matcherBuilder.addIncluded(name, includedPatterns.stream()
+                    .map(s -> preparePattern(name, 
s)).collect(Collectors.toSet()));
+        }
+    }
 
-            if (!incl.isEmpty()) {
-                inclMatchers.add(new DocumentNameMatcher("included patterns", 
MatchPatterns.from(basedir.getDirectorySeparator(), incl), basedir));
+    /**
+     * Extracts {@link #includedCollections} and {@link #excludedCollections} 
patterns into the specified matcherBuilder.
+     * @param nameBuilder the name builder for the pattern names.
+     * @param matcherBuilder the MatcherSet.Builder to add the collections to.
+     */
+    private void extractCollectionPatterns(final DocumentName.Builder 
nameBuilder, final MatcherSet.Builder matcherBuilder) {
+        final Set<String> incl = new TreeSet<>();
+        final Set<String> excl = new TreeSet<>();
+        for (StandardCollection sc : includedCollections) {
+            Set<String> patterns = sc.patterns();
+            if (patterns.isEmpty()) {
+                DefaultLog.getInstance().debug(String.format("%s does not have 
a defined collection for inclusion.", sc));
+            } else {
+                MatcherSet.Builder.segregateList(incl, excl, sc.patterns());
             }
-            if (!excl.isEmpty()) {
-                exclMatchers.add(new DocumentNameMatcher("excluded patterns", 
MatchPatterns.from(basedir.getDirectorySeparator(), excl), basedir));
+        }
+        for (StandardCollection sc : excludedCollections) {
+            Set<String> patterns = sc.patterns();
+            if (patterns.isEmpty()) {
+                DefaultLog.getInstance().debug(String.format("%s does not have 
a defined collection for exclusion.", sc));
+            } else {
+                MatcherSet.Builder.segregateList(excl, incl, sc.patterns());
             }
+        }
+        DocumentName name = nameBuilder.setName("Collections").build();
+        matcherBuilder
+                .addExcluded(name, excl.stream().map(s -> 
preparePattern(name.getBaseDocumentName(), s)).collect(Collectors.toSet()))
+                .addIncluded(name, incl.stream().map(s -> 
preparePattern(name.getBaseDocumentName(), s)).collect(Collectors.toSet()));
+    }
 
-            if (!includedPaths.isEmpty()) {
-                for (DocumentNameMatcher matcher : includedPaths) {
-                    DefaultLog.getInstance().info(format("Including path 
matcher %s", matcher));
-                    inclMatchers.add(matcher);
-                }
-            }
-            if (!excludedPaths.isEmpty()) {
-                for (DocumentNameMatcher matcher : excludedPaths) {
-                    DefaultLog.getInstance().info(format("Excluding path 
matcher %s", matcher));
-                    exclMatchers.add(matcher);
-                }
-            }
+    /**
+     * Extracts {@link #includedCollections} and {@link #excludedCollections} 
matchers into the specified matcherBuilder.
+     * @param matcherBuilder the MatcherSet.Builder to add the collections to.
+     */
+    private void extractCollectionMatchers(final MatcherSet.Builder 
matcherBuilder) {
+        ExtendedIterator.create(includedCollections.iterator())
+                .map(StandardCollection::staticDocumentNameMatcher)
+                .filter(Objects::nonNull)
+                .forEachRemaining(matcherBuilder::addIncluded);
 
-            lastMatcher = DocumentNameMatcher.MATCHES_ALL;
-            if (!exclMatchers.isEmpty()) {
-                lastMatcher = 
DocumentNameMatcher.not(DocumentNameMatcher.or(exclMatchers));
-                if (!inclMatchers.isEmpty()) {
-                    lastMatcher = 
DocumentNameMatcher.or(DocumentNameMatcher.or(inclMatchers), lastMatcher);
-                }
+        ExtendedIterator.create(excludedCollections.iterator())
+                .map(StandardCollection::staticDocumentNameMatcher)
+                .filter(Objects::nonNull)
+                .forEachRemaining(matcherBuilder::addExcluded);
+    }
+
+    /**
+     * Extracts {@link #includedPaths} and {@link #excludedPaths} patterns 
into the specified matcherBuilder.
+     * @param matcherBuilder the MatcherSet.Builder to add the collections to.
+     */
+    private void extractPaths(final MatcherSet.Builder matcherBuilder) {
+        if (!includedPaths.isEmpty()) {
+            for (DocumentNameMatcher matcher : includedPaths) {
+                DefaultLog.getInstance().info(format("Including path matcher 
%s", matcher));
+                matcherBuilder.addIncluded(matcher);
+            }
+        }
+        if (!excludedPaths.isEmpty()) {
+            for (DocumentNameMatcher matcher : excludedPaths) {
+                DefaultLog.getInstance().info(format("Excluding path matcher 
%s", matcher));
+                matcherBuilder.addExcluded(matcher);
             }
         }
-        return lastMatcher;
     }
+
 }
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionUtils.java
 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionUtils.java
index 7932db4f..ef2bab1b 100644
--- 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionUtils.java
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionUtils.java
@@ -34,9 +34,13 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.LineIterator;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.rat.ConfigurationException;
+import org.apache.rat.config.exclusion.plexus.MatchPattern;
+import org.apache.rat.config.exclusion.plexus.SelectorUtils;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.document.DocumentNameMatcher;
+import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.ExtendedIterator;
+import org.apache.rat.utils.Log;
 
 import static java.lang.String.format;
 
@@ -48,11 +52,14 @@ public final class ExclusionUtils {
     /** The list of comment prefixes that are used to filter comment lines.  */
     public static final List<String> COMMENT_PREFIXES = Arrays.asList("#", 
"##", "//", "/**", "/*");
 
-    /** A predicate that filters out lines that do NOT start with "!" */
-    public static final Predicate<String> NOT_MATCH_FILTER = s -> 
s.startsWith("!");
+    /** Prefix used to negate a given pattern. */
+    public static final String NEGATION_PREFIX = "!";
 
-    /** A predicate that filters out lines that start with "!" */
-    public static final Predicate<String> MATCH_FILTER = s -> 
!s.startsWith("!");
+    /** A predicate that filters out lines that do NOT start with {@link 
#NEGATION_PREFIX}. */
+    public static final Predicate<String> NOT_MATCH_FILTER = s -> 
s.startsWith(NEGATION_PREFIX);
+
+    /** A predicate that filters out lines that start with {@link 
#NEGATION_PREFIX}. */
+    public static final Predicate<String> MATCH_FILTER = 
NOT_MATCH_FILTER.negate();
 
     private ExclusionUtils() {
         // do not instantiate
@@ -112,7 +119,20 @@ public final class ExclusionUtils {
      * @return a FileFilter.
      */
     public static FileFilter asFileFilter(final DocumentName parent, final 
DocumentNameMatcher nameMatcher) {
-        return file -> 
nameMatcher.matches(DocumentName.builder(file).setBaseName(parent.getBaseName()).build());
+        return file -> {
+            DocumentName candidate = 
DocumentName.builder(file).setBaseName(parent.getBaseName()).build();
+            boolean result = nameMatcher.matches(candidate);
+            Log log = DefaultLog.getInstance();
+            if (log.isEnabled(Log.Level.DEBUG)) {
+                log.debug(format("FILTER TEST for %s -> %s", file, result));
+                if (!result) {
+                    List< DocumentNameMatcher.DecomposeData> data = 
nameMatcher.decompose(candidate);
+                    log.debug("Decomposition for " + candidate);
+                    data.forEach(log::debug);
+                }
+            }
+            return result;
+        };
     }
 
     /**
@@ -171,13 +191,12 @@ public final class ExclusionUtils {
     }
 
     /**
-     * Returns {@code true} if the file name represents a hidden file.
-     * @param f the file to check.
-     * @return {@code true} if it is the name of a hidden file.
+     * Returns {@code true} if the filename represents a hidden file
+     * @param fileName the file to check.
+     * @return true if it is the name of a hidden file.
      */
-    public static boolean isHidden(final File f) {
-        String s = f.getName();
-        return s.startsWith(".") && !(s.equals(".") || s.equals(".."));
+    public static boolean isHidden(final String fileName) {
+        return fileName.startsWith(".") && !(fileName.equals(".") || 
fileName.equals(".."));
     }
 
     private static void verifyFile(final File file) {
@@ -186,6 +205,31 @@ public final class ExclusionUtils {
         }
     }
 
+    /**
+     * Modifies the {@link MatchPattern} formatted {@code pattern} argument by 
expanding the pattern and
+     * by adjusting the pattern to include the basename from the {@code 
documentName} argument.
+     * @param documentName the name of the file being read.
+     * @param pattern the pattern to format.
+     * @return the completely formatted pattern
+     */
+    public static String qualifyPattern(final DocumentName documentName, final 
String pattern) {
+        boolean prefix = pattern.startsWith(NEGATION_PREFIX);
+        String workingPattern = prefix ? pattern.substring(1) : pattern;
+        String normalizedPattern = 
SelectorUtils.extractPattern(workingPattern, 
documentName.getDirectorySeparator());
+
+        StringBuilder sb = new StringBuilder(prefix ? NEGATION_PREFIX : "");
+        if (SelectorUtils.isRegexPrefixedPattern(workingPattern)) {
+            sb.append(SelectorUtils.REGEX_HANDLER_PREFIX)
+                    .append("\\Q").append(documentName.getBaseName())
+                    .append(documentName.getDirectorySeparator())
+                    .append("\\E").append(normalizedPattern)
+                    .append(SelectorUtils.PATTERN_HANDLER_SUFFIX);
+        } else {
+            
sb.append(documentName.getBaseDocumentName().resolve(normalizedPattern).getName());
+        }
+        return sb.toString();
+    }
+
     /**
      * Tokenizes the string based on the directory separator.
      * @param source the source to tokenize.
diff --git 
a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/MatcherSet.java 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/MatcherSet.java
new file mode 100644
index 00000000..5224fa6f
--- /dev/null
+++ 
b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/MatcherSet.java
@@ -0,0 +1,178 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.rat.config.exclusion.plexus.MatchPattern;
+import org.apache.rat.config.exclusion.plexus.MatchPatterns;
+import org.apache.rat.config.exclusion.plexus.SelectorUtils;
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.document.DocumentNameMatcher;
+
+import static org.apache.rat.document.DocumentNameMatcher.MATCHES_NONE;
+
+/**
+ * The file processor reads the file specified in the DocumentName.
+ * It must return a list of fully qualified strings for the {@link 
MatchPattern} to process.
+ * It may return either Ant or Regex style strings, or a mixture of both.
+ * See {@link SelectorUtils} for a description of the formats.
+ * It may also generate custom DocumentNameMatchers which are added to the 
customMatchers instance variable.
+ */
+public interface MatcherSet {
+    Optional<DocumentNameMatcher> includes();
+    Optional<DocumentNameMatcher> excludes();
+
+    default String getDescription() {
+        return String.format("MatcherSet: include [%s] exclude [%s]", 
includes().orElse(MATCHES_NONE), excludes().orElse(MATCHES_NONE));
+    }
+    /**
+     * Creates a DocumentNameMatcher from an iterable of matcher sets.
+     * @return A DocumentNameMatcher that processes the matcher sets.
+     */
+    default DocumentNameMatcher createMatcher() {
+        return DocumentNameMatcher.matcherSet(includes().orElse(MATCHES_NONE), 
excludes().orElse(MATCHES_NONE));
+    }
+
+    static MatcherSet merge(List<MatcherSet> matcherSets) {
+        Builder builder = new Builder();
+        for (MatcherSet matcherSet : matcherSets) {
+            matcherSet.includes().ifPresent(builder::addIncluded);
+            matcherSet.excludes().ifPresent(builder::addExcluded);
+        }
+        return builder.build();
+    }
+
+    /**
+     * A MatcherSet that assumes the files contain the already formatted 
strings and just need to be
+     * localised for the fileName. When {@link #build()} is called the builder 
is reset to the initial state.
+     */
+    class Builder {
+
+        /**
+         * Adds to lists of qualified file patterns. Non-matching patterns 
start with a {@link ExclusionUtils#NEGATION_PREFIX}.
+         * @param matching the list to put matching file patterns into.
+         * @param notMatching the list to put non-matching file patterns into.
+         * @param patterns the patterns to match.
+         */
+        public static void segregateList(final Set<String> matching, final 
Set<String> notMatching,
+                                         final Iterable<String> patterns) {
+            patterns.forEach(s -> {
+                if (ExclusionUtils.MATCH_FILTER.test(s)) {
+                    matching.add(s);
+                } else {
+                    notMatching.add(s.substring(1));
+                }
+            });
+        }
+
+        /** The DocumentNameMatcher that specifies included files */
+        protected DocumentNameMatcher included;
+        /** The DocumentNameMatcher that specifies excluded files */
+        protected DocumentNameMatcher excluded;
+
+        public Builder() {
+        }
+
+        /**
+         * Converts a collection names into DocumentNameMatchers that use the 
{@code fromDocument} directory separator.
+         * @param dest the consumer to accept the DocumentNameMatcher.
+         * @param nameFormat the format for the matcher names. Requires '%s' 
for the {@code fromDocument} localised name.
+         * @param fromDocument the document that the patterns are associated 
with.
+         * @param names the list of patterns. If empty no action is taken.
+         */
+        private void processNames(final Consumer<DocumentNameMatcher> dest, 
final String nameFormat, final DocumentName fromDocument, final Set<String> 
names) {
+            if (!names.isEmpty()) {
+                String name = String.format(nameFormat, 
fromDocument.localized("/").substring(1));
+                dest.accept(new DocumentNameMatcher(name, 
MatchPatterns.from(fromDocument.getDirectorySeparator(), names), 
fromDocument.getBaseDocumentName()));
+            }
+        }
+
+        /**
+         * Adds included file names from the specified document. File names 
are resolved relative to the directory
+         * of the {@code fromDocument}.
+         * @param fromDocument the document the names were read from.
+         * @param names the names that were read from the document. Must use 
the separator specified by {@code fromDocument}.
+         * @return this
+         */
+        public Builder addIncluded(final DocumentName fromDocument, final 
Set<String> names) {
+            processNames(this::addIncluded, "'included %s'", fromDocument, 
names);
+            return this;
+        }
+
+        /**
+         * Adds excluded file names from the specified document. File names 
are resolved relative to the directory
+         * of the {@code fromDocument}.
+         * @param fromDocument the document the names were read from.
+         * @param names the names that were read from the document. Must use 
the separator specified by {@code fromDocument}.
+         * @return this
+         */
+        public Builder addExcluded(final DocumentName fromDocument, final 
Set<String> names) {
+            processNames(this::addExcluded, "'excluded %s'", fromDocument, 
names);
+            return this;
+        }
+
+        /**
+         * Adds specified DocumentNameMatcher to the included matchers.
+         * @param matcher A document name matcher to add to the included set.
+         * @return this
+         */
+        public Builder addIncluded(final DocumentNameMatcher matcher) {
+            this.included = this.included == null ? matcher : 
DocumentNameMatcher.or(this.included, matcher);
+            return this;
+        }
+
+        /**
+         * Adds specified DocumentNameMatcher to the excluded matchers.
+         * @param matcher A document name matcher to add to the excluded set.
+         * @return this
+         */
+        public Builder addExcluded(final DocumentNameMatcher matcher) {
+            this.excluded = this.excluded == null ? matcher : 
DocumentNameMatcher.or(this.excluded, matcher);
+            return this;
+        }
+
+        /**
+         * Builds a MatcherSet. When {@code build()} is called the builder is 
reset to the initial state.
+         * @return the MatcherSet based upon the included and excluded 
matchers.
+         */
+        public MatcherSet build() {
+            MatcherSet result = new MatcherSet() {
+                private final DocumentNameMatcher myIncluded = included;
+                private final DocumentNameMatcher myExcluded = excluded;
+
+                @Override
+                public Optional<DocumentNameMatcher> includes() {
+                    return Optional.ofNullable(myIncluded);
+                }
+
+                @Override
+                public Optional<DocumentNameMatcher> excludes() {
+                    return Optional.ofNullable(myExcluded);
+                }
+            };
+            included = null;
+            excluded = null;
+            return result;
+        }
+    }
+}
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 1bd805f4..d2c59707 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
@@ -96,24 +96,38 @@ public enum StandardCollection {
             new GitFileProcessor()
     ),
     /**
-     * The hidden directories. Directories with names that start with '.'
+     * The hidden directories. Directories with names that start with {@code .}
      */
     HIDDEN_DIR("The hidden directories. Directories with names that start with 
'.'",
             null,
-            new DocumentNameMatcher("HIDDEN_DIR", (Predicate<DocumentName>) 
documentName -> {
-                        File f = new File(documentName.getName());
-                        return f.isDirectory() && ExclusionUtils.isHidden(f);
-                }), null
+            new DocumentNameMatcher("HIDDEN_DIR", new 
Predicate<DocumentName>() {
+                @Override
+                public boolean test(final DocumentName documentName) {
+                    File file = documentName.asFile();
+                    return file.isDirectory() && 
ExclusionUtils.isHidden(documentName.getShortName());
+                }
+                @Override
+                public String toString() {
+                    return "HIDDEN_DIR";
+                }
+            }), null
     ),
     /**
-     * The hidden files. Directories with names that start with '.'
+     * The hidden files. Directories with names that start with {@code .}
      */
     HIDDEN_FILE("The hidden files. Directories with names that start with '.'",
             null,
-            new DocumentNameMatcher("HIDDEN_FILE", (Predicate<DocumentName>) 
documentName -> {
-                    File f = new File(documentName.getName());
-                    return f.isFile() && ExclusionUtils.isHidden(f);
-                }), null
+            new DocumentNameMatcher("HIDDEN_FILE", new 
Predicate<DocumentName>() {
+                @Override
+                public boolean test(final DocumentName documentName) {
+                    File file = documentName.asFile();
+                    return file.isFile() && 
ExclusionUtils.isHidden(documentName.getShortName());
+                }
+                @Override
+                public String toString() {
+                    return "HIDDEN_FILE";
+                }
+            }), null
     ),
     /**
      * The files and directories created by an IDEA IDE based tool.
@@ -121,9 +135,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 MAC computer.
+     * The .DS_Store files on Mac computers.
      */
-    MAC("The .DS_Store files MAC computer.",
+    MAC("The .DS_Store files Mac computers.",
             Collections.singletonList("**/.DS_Store"), null, null),
     /**
      * The files and directories created by Maven build system based project.

Reply via email to