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>"**/project.pj"</code>,
274 * <code>"**/.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 }