View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.impl;
20  
21  import java.io.File;
22  import java.nio.file.FileSystem;
23  import java.nio.file.Path;
24  import java.nio.file.PathMatcher;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Iterator;
29  import java.util.LinkedHashSet;
30  import java.util.List;
31  import java.util.Objects;
32  import java.util.Set;
33  
34  import org.apache.maven.api.annotations.Nonnull;
35  
36  /**
37   * Determines whether a path is selected according to include/exclude patterns.
38   * The pathnames used for method parameters will be relative to some base directory
39   * and use {@code '/'} as separator, regardless of the hosting operating system.
40   *
41   * <h2>Syntax</h2>
42   * If a pattern contains the {@code ':'} character and the prefix before is longer than 1 character,
43   * then that pattern is given verbatim to {@link FileSystem#getPathMatcher(String)}, which interprets
44   * the part before {@code ':'} as the syntax (usually {@code "glob"} or {@code "regex"}).
45   * If a pattern does not contain the {@code ':'} character, or if the prefix is one character long
46   * (interpreted as a Windows drive), then the syntax defaults to a reproduction of the Maven 3 behavior.
47   * This is implemented as the {@code "glob"} syntax with the following modifications:
48   *
49   * <ul>
50   *   <li>The platform-specific separator ({@code '\\'} on Windows) is replaced by {@code '/'}.
51   *       Note that it means that the backslash cannot be used for escaping characters.</li>
52   *   <li>Trailing {@code "/"} is completed as {@code "/**"}.</li>
53   *   <li>The {@code "**"} wildcard means "0 or more directories" instead of "1 or more directories".
54   *       This is implemented by adding variants of the pattern without the {@code "**"} wildcard.</li>
55   *   <li>Bracket characters [ ] and { } are escaped.</li>
56   *   <li>On Unix only, the escape character {@code '\\'} is itself escaped.</li>
57   * </ul>
58   *
59   * If above changes are not desired, put an explicit {@code "glob:"} prefix before the pattern.
60   * Note that putting such a prefix is recommended anyway for better performances.
61   *
62   * @see java.nio.file.FileSystem#getPathMatcher(String)
63   */
64  final class PathSelector implements PathMatcher {
65      /**
66       * Patterns which should be excluded by default, like <abbr>SCM</abbr> files.
67       *
68       * <p><b>Source:</b> this list is copied from {@code plexus-utils-4.0.2} (released in
69       * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.</p>
70       */
71      private static final List<String> DEFAULT_EXCLUDES = List.of(
72              // Miscellaneous typical temporary files
73              "**/*~",
74              "**/#*#",
75              "**/.#*",
76              "**/%*%",
77              "**/._*",
78  
79              // CVS
80              "**/CVS",
81              "**/CVS/**",
82              "**/.cvsignore",
83  
84              // RCS
85              "**/RCS",
86              "**/RCS/**",
87  
88              // SCCS
89              "**/SCCS",
90              "**/SCCS/**",
91  
92              // Visual SourceSafe
93              "**/vssver.scc",
94  
95              // MKS
96              "**/project.pj",
97  
98              // Subversion
99              "**/.svn",
100             "**/.svn/**",
101 
102             // Arch
103             "**/.arch-ids",
104             "**/.arch-ids/**",
105 
106             // Bazaar
107             "**/.bzr",
108             "**/.bzr/**",
109 
110             // SurroundSCM
111             "**/.MySCMServerInfo",
112 
113             // Mac
114             "**/.DS_Store",
115 
116             // Serena Dimensions Version 10
117             "**/.metadata",
118             "**/.metadata/**",
119 
120             // Mercurial
121             "**/.hg",
122             "**/.hg/**",
123 
124             // git
125             "**/.git",
126             "**/.git/**",
127             "**/.gitignore",
128 
129             // BitKeeper
130             "**/BitKeeper",
131             "**/BitKeeper/**",
132             "**/ChangeSet",
133             "**/ChangeSet/**",
134 
135             // darcs
136             "**/_darcs",
137             "**/_darcs/**",
138             "**/.darcsrepo",
139             "**/.darcsrepo/**",
140             "**/-darcs-backup*",
141             "**/.darcs-temp-mail");
142 
143     /**
144      * Maximum number of characters of the prefix before {@code ':'} for handling as a Maven syntax.
145      */
146     private static final int MAVEN_SYNTAX_THRESHOLD = 1;
147 
148     /**
149      * The default syntax to use if none was specified. Note that when this default syntax is applied,
150      * the user-provided pattern get some changes as documented in class Javadoc.
151      */
152     private static final String DEFAULT_SYNTAX = "glob:";
153 
154     /**
155      * Characters having a special meaning in the glob syntax.
156      *
157      * @see FileSystem#getPathMatcher(String)
158      */
159     private static final String SPECIAL_CHARACTERS = "*?[]{}\\";
160 
161     /**
162      * A path matcher which accepts all files.
163      *
164      * @see #simplify()
165      */
166     static final PathMatcher INCLUDES_ALL = (path) -> true;
167 
168     /**
169      * String representations of the normalized include filters.
170      * Each pattern shall be prefixed by its syntax, which is {@value #DEFAULT_SYNTAX} by default.
171      * An empty array means to include all files.
172      *
173      * @see #toString()
174      */
175     private final String[] includePatterns;
176 
177     /**
178      * String representations of the normalized exclude filters.
179      * Each pattern shall be prefixed by its syntax. If no syntax is specified,
180      * the default is a Maven 3 syntax similar, but not identical, to {@value #DEFAULT_SYNTAX}.
181      * This array may be longer or shorter than the user-supplied excludes, depending on whether
182      * default excludes have been added and whether some unnecessary excludes have been omitted.
183      *
184      * @see #toString()
185      */
186     private final String[] excludePatterns;
187 
188     /**
189      * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length.
190      * An empty array means to include all files.
191      */
192     private final PathMatcher[] includes;
193 
194     /**
195      * The matcher for excludes. The length of this array is equal to {@link #excludePatterns} array length.
196      */
197     private final PathMatcher[] excludes;
198 
199     /**
200      * The matcher for all directories to include. This array includes the parents of all those directories,
201      * because they need to be accepted before we can walk to the sub-directories.
202      * This is an optimization for skipping whole directories when possible.
203      * An empty array means to include all directories.
204      */
205     private final PathMatcher[] dirIncludes;
206 
207     /**
208      * The matcher for directories to exclude. This array does <em>not</em> include the parent directories,
209      * because they may contain other sub-trees that need to be included.
210      * This is an optimization for skipping whole directories when possible.
211      */
212     private final PathMatcher[] dirExcludes;
213 
214     /**
215      * The base directory. All files will be relativized to that directory before to be matched.
216      */
217     private final Path baseDirectory;
218 
219     /**
220      * Whether paths must be relativized before being given to a matcher. If {@code true}, then every paths
221      * will be made relative to {@link #baseDirectory} for allowing patterns like {@code "foo/bar/*.java"}
222      * to work. As a slight optimization, we can skip this step if all patterns start with {@code "**"}.
223      */
224     private final boolean needRelativize;
225 
226     /**
227      * Creates a new selector from the given includes and excludes.
228      *
229      * @param directory the base directory of the files to filter
230      * @param includes the patterns of the files to include, or null or empty for including all files
231      * @param excludes the patterns of the files to exclude, or null or empty for no exclusion
232      * @param useDefaultExcludes whether to augment the excludes with a default set of <abbr>SCM</abbr> patterns
233      * @throws NullPointerException if directory is null
234      */
235     private PathSelector(
236             @Nonnull Path directory,
237             Collection<String> includes,
238             Collection<String> excludes,
239             boolean useDefaultExcludes) {
240         baseDirectory = Objects.requireNonNull(directory, "directory cannot be null");
241         includePatterns = normalizePatterns(includes, false);
242         excludePatterns = normalizePatterns(effectiveExcludes(excludes, includePatterns, useDefaultExcludes), true);
243         FileSystem fileSystem = baseDirectory.getFileSystem();
244         this.includes = matchers(fileSystem, includePatterns);
245         this.excludes = matchers(fileSystem, excludePatterns);
246         dirIncludes = matchers(fileSystem, directoryPatterns(includePatterns, false));
247         dirExcludes = matchers(fileSystem, directoryPatterns(excludePatterns, true));
248         needRelativize = needRelativize(includePatterns) || needRelativize(excludePatterns);
249     }
250 
251     /**
252      * Creates a new matcher from the given includes and excludes.
253      *
254      * @param directory the base directory of the files to filter
255      * @param includes the patterns of the files to include, or null or empty for including all files
256      * @param excludes the patterns of the files to exclude, or null or empty for no exclusion
257      * @param useDefaultExcludes whether to augment the excludes with a default set of <abbr>SCM</abbr> patterns
258      * @throws NullPointerException if directory is null
259      * @return a path matcher for the given includes and excludes
260      */
261     public static PathMatcher of(
262             @Nonnull Path directory,
263             Collection<String> includes,
264             Collection<String> excludes,
265             boolean useDefaultExcludes) {
266         return new PathSelector(directory, includes, excludes, useDefaultExcludes).simplify();
267     }
268 
269     /**
270      * Returns the given array of excludes, optionally expanded with a default set of excludes,
271      * then with unnecessary excludes omitted. An unnecessary exclude is an exclude which will never
272      * match a file because there are no includes which would accept a file that could match the exclude.
273      * For example, if the only include is {@code "*.java"}, then the <code>"**&sol;project.pj"</code>,
274      * <code>"**&sol;.DS_Store"</code> and other excludes will never match a file and can be omitted.
275      * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} contains many elements,
276      * removing unnecessary excludes can reduce a lot the number of matches tested on each source file.
277      *
278      * <h4>Implementation note</h4>
279      * The removal of unnecessary excludes is done on a best effort basis. The current implementation
280      * compares only the prefixes and suffixes of each pattern, keeping the pattern in case of doubt.
281      * This is not bad, but it does not remove all unnecessary patterns. It would be possible to do
282      * better in the future if benchmarking suggests that it would be worth the effort.
283      *
284      * @param excludes the user-specified excludes, potentially not yet converted to glob syntax
285      * @param includes the include patterns converted to glob syntax
286      * @param useDefaultExcludes whether to expand user exclude with the set of default excludes
287      * @return the potentially expanded or reduced set of excludes to use
288      */
289     private static Collection<String> effectiveExcludes(
290             Collection<String> excludes, final String[] includes, final boolean useDefaultExcludes) {
291         if (excludes == null || excludes.isEmpty()) {
292             if (useDefaultExcludes) {
293                 excludes = new ArrayList<>(DEFAULT_EXCLUDES);
294             } else {
295                 return List.of();
296             }
297         } else {
298             excludes = new ArrayList<>(excludes);
299             excludes.removeIf(Objects::isNull);
300             if (useDefaultExcludes) {
301                 excludes.addAll(DEFAULT_EXCLUDES);
302             }
303         }
304         if (includes.length == 0) {
305             return excludes;
306         }
307         /*
308          * Get the prefixes and suffixes of all includes, stopping at the first special character.
309          * Redundant prefixes and suffixes are omitted.
310          */
311         var prefixes = new String[includes.length];
312         var suffixes = new String[includes.length];
313         for (int i = 0; i < includes.length; i++) {
314             String include = includes[i];
315             if (!include.startsWith(DEFAULT_SYNTAX)) {
316                 return excludes; // Do not filter if at least one pattern is too complicated.
317             }
318             include = include.substring(DEFAULT_SYNTAX.length());
319             prefixes[i] = prefixOrSuffix(include, false);
320             suffixes[i] = prefixOrSuffix(include, true);
321         }
322         prefixes = sortByLength(prefixes, false);
323         suffixes = sortByLength(suffixes, true);
324         /*
325          * Keep only the exclude which start with one of the prefixes and end with one of the suffixes.
326          * Note that a prefix or suffix may be the empty string, which match everything.
327          */
328         final Iterator<String> it = excludes.iterator();
329         nextExclude:
330         while (it.hasNext()) {
331             final String exclude = it.next();
332             final int s = exclude.indexOf(':');
333             if (s <= MAVEN_SYNTAX_THRESHOLD || exclude.startsWith(DEFAULT_SYNTAX)) {
334                 if (cannotMatch(exclude, prefixes, false) || cannotMatch(exclude, suffixes, true)) {
335                     it.remove();
336                 }
337             }
338         }
339         return excludes;
340     }
341 
342     /**
343      * Returns the maximal amount of ordinary characters at the beginning or end of the given pattern.
344      * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS special character}.
345      *
346      * @param include the pattern for which to get a prefix or suffix without special character
347      * @param suffix {@code false} if a prefix is desired, or {@code true} if a suffix is desired
348      */
349     private static String prefixOrSuffix(final String include, boolean suffix) {
350         int s = suffix ? -1 : include.length();
351         for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) {
352             char c = SPECIAL_CHARACTERS.charAt(i);
353             if (suffix) {
354                 s = Math.max(s, include.lastIndexOf(c));
355             } else {
356                 int p = include.indexOf(c);
357                 if (p >= 0 && p < s) {
358                     s = p;
359                 }
360             }
361         }
362         return suffix ? include.substring(s + 1) : include.substring(0, s);
363     }
364 
365     /**
366      * Returns {@code true} if the given exclude cannot match any include patterns.
367      * In case of doubt, returns {@code false}.
368      *
369      * @param exclude the exclude pattern to test
370      * @param fragments the prefixes or suffixes (fragments without special characters) of the includes
371      * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes
372      * @return {@code true} if it is certain that the exclude pattern cannot match, or {@code false} in case of doubt
373      */
374     private static boolean cannotMatch(String exclude, final String[] fragments, final boolean suffix) {
375         exclude = prefixOrSuffix(exclude, suffix);
376         for (String fragment : fragments) {
377             int fg = fragment.length();
378             int ex = exclude.length();
379             int length = Math.min(fg, ex);
380             if (suffix) {
381                 fg -= length;
382                 ex -= length;
383             } else {
384                 fg = 0;
385                 ex = 0;
386             }
387             if (exclude.regionMatches(ex, fragment, fg, length)) {
388                 return false;
389             }
390         }
391         return true;
392     }
393 
394     /**
395      * Sorts the given patterns by their length. The main intent is to have the empty string first,
396      * while will cause the loops testing for prefixes and suffixes to stop almost immediately.
397      * Short prefixes or suffixes are also more likely to be matched.
398      *
399      * @param fragments the fragments to sort in-place
400      * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes
401      * @return the given array, or a smaller array if some fragments were discarded because redundant
402      */
403     private static String[] sortByLength(final String[] fragments, final boolean suffix) {
404         Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length());
405         int count = 0;
406         /*
407          * Simplify the array of prefixes or suffixes by removing all redundant elements.
408          * An element is redundant if there is a shorter prefix or suffix with the same characters.
409          */
410         nextBase:
411         for (String fragment : fragments) {
412             for (int i = count; --i >= 0; ) {
413                 String base = fragments[i];
414                 if (suffix ? fragment.endsWith(base) : fragment.startsWith(base)) {
415                     continue nextBase; // Skip this fragment
416                 }
417             }
418             fragments[count++] = fragment;
419         }
420         return (fragments.length == count) ? fragments : Arrays.copyOf(fragments, count);
421     }
422 
423     /**
424      * Returns the given array of patterns with path separator normalized to {@code '/'}.
425      * Null or empty patterns are ignored, and duplications are removed.
426      *
427      * @param patterns the patterns to normalize
428      * @param excludes whether the patterns are exclude patterns
429      * @return normalized patterns without null, empty or duplicated patterns
430      */
431     private static String[] normalizePatterns(final Collection<String> patterns, final boolean excludes) {
432         if (patterns == null || patterns.isEmpty()) {
433             return new String[0];
434         }
435         // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
436         final var normalized = new LinkedHashSet<String>(patterns.size());
437         for (String pattern : patterns) {
438             if (pattern != null && !pattern.isEmpty()) {
439                 if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) {
440                     pattern = pattern.replace(File.separatorChar, '/');
441                     if (pattern.endsWith("/")) {
442                         pattern += "**";
443                     }
444                     // Following are okay only when "**" means "0 or more directories".
445                     while (pattern.endsWith("/**/**")) {
446                         pattern = pattern.substring(0, pattern.length() - 3);
447                     }
448                     while (pattern.startsWith("**/**/")) {
449                         pattern = pattern.substring(3);
450                     }
451                     pattern = pattern.replace("/**/**/", "/**/");
452                     pattern = pattern.replace("\\", "\\\\")
453                             .replace("[", "\\[")
454                             .replace("]", "\\]")
455                             .replace("{", "\\{")
456                             .replace("}", "\\}");
457                     normalized.add(DEFAULT_SYNTAX + pattern);
458                     /*
459                      * If the pattern starts or ends with "**", Java GLOB expects a directory level at
460                      * that location while Maven seems to consider that "**" can mean "no directory".
461                      * Add another pattern for reproducing this effect.
462                      */
463                     addPatternsWithOneDirRemoved(normalized, pattern, 0);
464                 } else {
465                     normalized.add(pattern);
466                 }
467             }
468         }
469         return simplify(normalized, excludes);
470     }
471 
472     /**
473      * Adds all variants of the given pattern with {@code **} removed.
474      * This is used for simulating the Maven behavior where {@code "**} may match zero directory.
475      * Tests suggest that we need an explicit GLOB pattern with no {@code "**"} for matching an absence of directory.
476      *
477      * @param patterns where to add the derived patterns
478      * @param pattern  the pattern for which to add derived forms, without the "glob:" syntax prefix
479      * @param end      should be 0 (reserved for recursive invocations of this method)
480      */
481     private static void addPatternsWithOneDirRemoved(final Set<String> patterns, final String pattern, int end) {
482         final int length = pattern.length();
483         int start;
484         while ((start = pattern.indexOf("**", end)) >= 0) {
485             end = start + 2; // 2 is the length of "**".
486             if (end < length) {
487                 if (pattern.charAt(end) != '/') {
488                     continue;
489                 }
490                 if (start == 0) {
491                     end++; // Ommit the leading slash if there is nothing before it.
492                 }
493             }
494             if (start > 0 && pattern.charAt(--start) != '/') {
495                 continue;
496             }
497             String reduced = pattern.substring(0, start) + pattern.substring(end);
498             patterns.add(DEFAULT_SYNTAX + reduced);
499             addPatternsWithOneDirRemoved(patterns, reduced, start);
500         }
501     }
502 
503     /**
504      * Applies some heuristic rules for simplifying the set of patterns,
505      * then returns the patterns as an array.
506      *
507      * @param patterns the patterns to simplify and return as an array
508      * @param excludes whether the patterns are exclude patterns
509      * @return the set content as an array, after simplification
510      */
511     private static String[] simplify(Set<String> patterns, boolean excludes) {
512         /*
513          * If the "**" pattern is present, it makes all other patterns useless.
514          * In the case of include patterns, an empty set means to include everything.
515          */
516         if (patterns.remove("**")) {
517             patterns.clear();
518             if (excludes) {
519                 patterns.add("**");
520             }
521         }
522         return patterns.toArray(String[]::new);
523     }
524 
525     /**
526      * Eventually adds the parent directory of the given patterns, without duplicated values.
527      * The patterns given to this method should have been normalized.
528      *
529      * @param patterns the normalized include or exclude patterns
530      * @param excludes whether the patterns are exclude patterns
531      * @return patterns of directories to include or exclude
532      */
533     private static String[] directoryPatterns(final String[] patterns, final boolean excludes) {
534         // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
535         final var directories = new LinkedHashSet<String>(patterns.length);
536         for (String pattern : patterns) {
537             if (pattern.startsWith(DEFAULT_SYNTAX)) {
538                 if (excludes) {
539                     if (pattern.endsWith("/**")) {
540                         directories.add(pattern.substring(0, pattern.length() - 3));
541                     }
542                 } else {
543                     int s = pattern.indexOf(':');
544                     if (pattern.regionMatches(++s, "**/", 0, 3)) {
545                         s = pattern.indexOf('/', s + 3);
546                         if (s < 0) {
547                             return new String[0]; // Pattern is "**", so we need to accept everything.
548                         }
549                         directories.add(pattern.substring(0, s));
550                     }
551                 }
552             }
553         }
554         return simplify(directories, excludes);
555     }
556 
557     /**
558      * Returns {@code true} if at least one pattern requires path being relativized before to be matched.
559      *
560      * @param patterns include or exclude patterns
561      * @return whether at least one pattern require relativization
562      */
563     private static boolean needRelativize(String[] patterns) {
564         for (String pattern : patterns) {
565             if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) {
566                 return true;
567             }
568         }
569         return false;
570     }
571 
572     /**
573      * Creates the path matchers for the given patterns.
574      * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for each pattern.
575      */
576     private static PathMatcher[] matchers(final FileSystem fs, final String[] patterns) {
577         final var matchers = new PathMatcher[patterns.length];
578         for (int i = 0; i < patterns.length; i++) {
579             matchers[i] = fs.getPathMatcher(patterns[i]);
580         }
581         return matchers;
582     }
583 
584     /**
585      * {@return a potentially simpler matcher equivalent to this matcher}.
586      */
587     @SuppressWarnings("checkstyle:MissingSwitchDefault")
588     private PathMatcher simplify() {
589         if (!needRelativize && excludes.length == 0) {
590             switch (includes.length) {
591                 case 0:
592                     return INCLUDES_ALL;
593                 case 1:
594                     return includes[0];
595             }
596         }
597         return this;
598     }
599 
600     /**
601      * Determines whether a path is selected.
602      * This is true if the given file matches an include pattern and no exclude pattern.
603      *
604      * @param path the pathname to test, must not be {@code null}
605      * @return {@code true} if the given path is selected, {@code false} otherwise
606      */
607     @Override
608     public boolean matches(Path path) {
609         if (needRelativize) {
610             path = baseDirectory.relativize(path);
611         }
612         return (includes.length == 0 || isMatched(path, includes))
613                 && (excludes.length == 0 || !isMatched(path, excludes));
614     }
615 
616     /**
617      * {@return whether the given file matches according to one of the given matchers}.
618      */
619     private static boolean isMatched(Path path, PathMatcher[] matchers) {
620         for (PathMatcher matcher : matchers) {
621             if (matcher.matches(path)) {
622                 return true;
623             }
624         }
625         return false;
626     }
627 
628     /**
629      * Returns whether {@link #couldHoldSelected(Path)} may return {@code false} for some directories.
630      * This method can be used to determine if directory filtering optimization is possible.
631      *
632      * @return {@code true} if directory filtering is possible, {@code false} if all directories
633      *         will be considered as potentially containing selected files
634      */
635     boolean canFilterDirectories() {
636         return dirIncludes.length != 0 || dirExcludes.length != 0;
637     }
638 
639     /**
640      * Determines whether a directory could contain selected paths.
641      *
642      * @param directory the directory pathname to test, must not be {@code null}
643      * @return {@code true} if the given directory might contain selected paths, {@code false} if the
644      *         directory will definitively not contain selected paths
645      */
646     public boolean couldHoldSelected(Path directory) {
647         if (baseDirectory.equals(directory)) {
648             return true;
649         }
650         directory = baseDirectory.relativize(directory);
651         return (dirIncludes.length == 0 || isMatched(directory, dirIncludes))
652                 && (dirExcludes.length == 0 || !isMatched(directory, dirExcludes));
653     }
654 
655     /**
656      * Appends the elements of the given array in the given buffer.
657      * This is a helper method for {@link #toString()} implementations.
658      *
659      * @param buffer the buffer to add the elements to
660      * @param label label identifying the array of elements to add
661      * @param patterns the elements to append, or {@code null} if none
662      */
663     private static void append(StringBuilder buffer, String label, String[] patterns) {
664         buffer.append(label).append(": [");
665         if (patterns != null) {
666             for (int i = 0; i < patterns.length; i++) {
667                 if (i != 0) {
668                     buffer.append(", ");
669                 }
670                 buffer.append(patterns[i]);
671             }
672         }
673         buffer.append(']');
674     }
675 
676     /**
677      * {@return a string representation for logging purposes}.
678      */
679     @Override
680     public String toString() {
681         var buffer = new StringBuilder();
682         append(buffer, "includes", includePatterns);
683         append(buffer.append(", "), "excludes", excludePatterns);
684         return buffer.toString();
685     }
686 }