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

desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-compiler-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new 95bd529  Make the default include/exclude syntax compatible with the 
behavior of Maven 3 (#945)
95bd529 is described below

commit 95bd529e3d5d4b1dc05eabae07b2215858490a0d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Jul 12 12:16:54 2025 +0200

    Make the default include/exclude syntax compatible with the behavior of 
Maven 3 (#945)
    
    Copy the `PathSelector` class from Maven core with removal of unused 
features.
    It allows the include/exclude patterns to reproduce the behavior of Maven 3
    when no syntax is specified. Before this commit the default syntax was glob,
    which is not identical to Maven 3 behavior.
    
    A future version should use the `PathSelector` from maven-impl directly.
    But it may require to move that class in another module.
---
 .../plugin/compiler/AbstractCompilerMojo.java      |   6 +-
 .../apache/maven/plugin/compiler/PathFilter.java   | 219 +++-----
 .../apache/maven/plugin/compiler/PathSelector.java | 566 +++++++++++++++++++++
 .../maven/plugin/compiler/SourceDirectory.java     |  17 +-
 4 files changed, 643 insertions(+), 165 deletions(-)

diff --git 
a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java 
b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
index d2792ba..893f121 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
@@ -930,14 +930,16 @@ public abstract class AbstractCompilerMojo implements 
Mojo {
     /**
      * {@return the inclusion filters for the compiler, or an empty list for 
all Java source files}.
      * The filter patterns are described in {@link 
java.nio.file.FileSystem#getPathMatcher(String)}.
-     * If no syntax is specified, the default syntax is "glob".
+     * If no syntax is specified, the default syntax is a derivative of "glob" 
compatible with the
+     * behavior of Maven 3.
      */
     protected abstract Set<String> getIncludes();
 
     /**
      * {@return the exclusion filters for the compiler, or an empty list if 
none}.
      * The filter patterns are described in {@link 
java.nio.file.FileSystem#getPathMatcher(String)}.
-     * If no syntax is specified, the default syntax is "glob".
+     * If no syntax is specified, the default syntax is a derivative of "glob" 
compatible with the
+     * behavior of Maven 3.
      */
     protected abstract Set<String> getExcludes();
 
diff --git a/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java 
b/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java
index cb2b437..7f1995d 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java
@@ -28,18 +28,18 @@ import java.nio.file.Path;
 import java.nio.file.PathMatcher;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.DosFileAttributes;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
-import java.util.function.Predicate;
 
 /**
  * Applies inclusion and exclusion filters on paths, and builds a list of 
files in a directory tree.
  * The set of allowed syntax contains at least "glob" and "regex".
  * See {@link FileSystem#getPathMatcher(String)} Javadoc for a description of 
the "glob" syntax.
- * If no syntax is specified, then the default syntax is "glob".
+ * If no syntax is specified, then the default syntax is a derivative of the 
"glob" syntax which
+ * reproduces the behavior of Maven 3.
  *
  * <p>The list of files to process is built by applying the path matcher on 
each regular (non directory) files.
  * The walk in file trees has the following characteristics:</p>
@@ -54,18 +54,19 @@ import java.util.function.Predicate;
  *
  * @author Martin Desruisseaux
  */
-final class PathFilter extends SimpleFileVisitor<Path> implements 
Predicate<Path> {
+final class PathFilter extends SimpleFileVisitor<Path> {
     /**
      * Whether to use the default include pattern.
      * The pattern depends on the type of source file.
+     *
+     * @see javax.tools.JavaFileObject.Kind#extension
      */
     private final boolean useDefaultInclude;
 
     /**
      * Inclusion filters for the files in the directories to walk as specified 
in the plugin configuration.
-     * The array should contain at least one element, unless {@link 
SourceDirectory#includes} is non-empty.
-     * If {@link #useDefaultInclude} is {@code true}, then this array length 
shall be exactly 1 and the
-     * single element is overwritten for each directory to walk.
+     * The array should contain at least one element. If {@link 
#useDefaultInclude} is {@code true}, then
+     * this array length shall be exactly 1 and the single element is 
overwritten for each directory to walk.
      *
      * @see SourceDirectory#includes
      */
@@ -79,45 +80,22 @@ final class PathFilter extends SimpleFileVisitor<Path> 
implements Predicate<Path
     private final String[] excludes;
 
     /**
-     * All exclusion filters for incremental build calculation, or an empty 
array if none.
-     * Updated files, if excluded by this filter, will not cause the project 
to be rebuilt.
-     */
-    private final String[] incrementalExcludes;
-
-    /**
-     * The matchers for inclusion filters (never empty).
-     * The array length shall be equal or greater than the {@link #includes} 
array length.
-     * The array is initially null and created when first needed, then when 
the file system changes.
+     * Combination of include and exclude filters. This is an instance of 
{@link PathSelector},
+     * unless the includes/excludes can be simplified to a single standard 
matcher instance.
      */
-    private PathMatcher[] includeMatchers;
+    private PathMatcher matchers;
 
     /**
-     * The matchers for exclusion filters (potentially empty).
-     * The array length shall be equal or greater than the {@link #excludes} 
array length.
-     * The array is initially null and created when first needed, then when 
the file system changes.
+     * All exclusion filters for incremental build calculation, or an empty 
list if none.
+     * Updated files, if excluded by a pattern, will not cause the project to 
be rebuilt.
      */
-    private PathMatcher[] excludeMatchers;
+    private final Collection<String> incrementalExcludes;
 
     /**
      * The matchers for exclusion filters for incremental build calculation.
-     * The array length shall be equal to the {@link #incrementalExcludes} 
array length.
-     * The array is initially null and created when first needed, then when 
the file system changes.
+     * May be an instance of {@link PathSelector}, or {@code null} if none.
      */
-    private PathMatcher[] incrementalExcludeMatchers;
-
-    /**
-     * Whether paths must be relativized before to be given to a matcher. If 
{@code true} (the default),
-     * then every paths will be made relative to the source root directory for 
allowing patterns like
-     * {@code "foo/bar/*.java"} to work. As a slight optimization, we can skip 
this step if all patterns
-     * start with {@code "**"}.
-     */
-    private boolean needRelativize;
-
-    /**
-     * The file system of the path matchers, or {@code null} if not yet 
determined.
-     * This is used in case not all paths are on the same file system.
-     */
-    private FileSystem fs;
+    private PathMatcher incrementalExcludeMatchers;
 
     /**
      * The result of listing all files, or {@code null} if no walking is in 
progress.
@@ -146,114 +124,25 @@ final class PathFilter extends SimpleFileVisitor<Path> 
implements Predicate<Path
         }
         includes = specified.toArray(String[]::new);
         excludes = mojo.getExcludes().toArray(String[]::new);
-        incrementalExcludes = 
mojo.getIncrementalExcludes().toArray(String[]::new);
-    }
-
-    /**
-     * Returns {@code true} if at least one pattern does not start with {@code 
"**"}.
-     * This is a slight optimization for avoiding the need to relativize each 
path
-     * before to give it to a matcher.
-     */
-    private static boolean needRelativize(String[] patterns) {
-        for (String pattern : patterns) {
-            if (!pattern.startsWith("**", pattern.indexOf(':') + 1)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Creates the matchers for the given patterns.
-     * If a pattern does not specify a syntax, then the "glob" syntax is used 
by default.
-     * If the {@code forDirectory} list contains at least one element and 
{@code patterns}
-     * is the default pattern, then the latter is ignored in favor of the 
former.
-     *
-     * <p>This method should be invoked only once, unless different paths are 
on different file systems.</p>
-     *
-     * @param forDirectory the matchers declared in the {@code <source>} 
element for the current {@link #sourceRoot}
-     * @param patterns the matterns declared in the compiler plugin 
configuration
-     * @param hasDefault whether the first element of {@code patterns} is the 
default pattern
-     * @param fs the file system
-     * @return all matchers from the source, followed by matchers from the 
given patterns
-     */
-    private static PathMatcher[] createMatchers(
-            List<PathMatcher> forDirectory, String[] patterns, boolean 
hasDefault, FileSystem fs) {
-        final int base = forDirectory.size();
-        final int skip = (hasDefault && base != 0) ? 1 : 0;
-        final var target = forDirectory.toArray(new PathMatcher[base + 
patterns.length - skip]);
-        for (int i = skip; i < patterns.length; i++) {
-            String pattern = patterns[i];
-            if (pattern.indexOf(':') < 0) {
-                pattern = "glob:" + pattern;
-            }
-            target[base + i] = fs.getPathMatcher(pattern);
-        }
-        return target;
-    }
-
-    /**
-     * Tests whether the given path should be included according the 
include/exclude patterns.
-     * This method does not perform any I/O operation. For example, it does 
not check if the file is hidden.
-     *
-     * @param  path  the source file to test
-     * @return whether the given source file should be included
-     */
-    @Override
-    public boolean test(Path path) {
-        @SuppressWarnings("LocalVariableHidesMemberVariable")
-        final SourceDirectory sourceRoot = this.sourceRoot; // Protect from 
changes.
-        FileSystem pfs = path.getFileSystem();
-        if (pfs != fs) {
-            if (useDefaultInclude) {
-                includes[0] = "glob:**" + sourceRoot.fileKind.extension;
-            }
-            includeMatchers = createMatchers(sourceRoot.includes, includes, 
useDefaultInclude, pfs);
-            excludeMatchers = createMatchers(sourceRoot.excludes, excludes, 
false, pfs);
-            incrementalExcludeMatchers = createMatchers(List.of(), 
incrementalExcludes, false, pfs);
-            needRelativize = !(sourceRoot.includes.isEmpty() && 
sourceRoot.excludes.isEmpty())
-                    || needRelativize(includes)
-                    || needRelativize(excludes);
-            fs = pfs;
-        }
-        if (needRelativize) {
-            path = sourceRoot.root.relativize(path);
-        }
-        for (PathMatcher include : includeMatchers) {
-            if (include.matches(path)) {
-                for (PathMatcher exclude : excludeMatchers) {
-                    if (exclude.matches(path)) {
-                        return false;
-                    }
-                }
-                return true;
-            }
-        }
-        return false;
+        incrementalExcludes = mojo.getIncrementalExcludes();
     }
 
     /**
-     * {@return whether to ignore the given file for incremental build 
calculation}.
-     * This method shall be invoked only after {@link #test(Path)} for the 
same file,
-     * because it depends on matcher updates performed by the {@code test} 
method.
-     */
-    private boolean ignoreModification(Path path) {
-        for (PathMatcher exclude : incrementalExcludeMatchers) {
-            if (exclude.matches(path)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Invoked for a file in a directory. If the given file is not hidden and 
pass the include/exclude filters,
+     * Invoked for a file in a directory. If the given file passes the 
include/exclude filters,
      * then it is added to the list of source files.
+     *
+     * @param  file  the source file to test
+     * @param  attrs the file basic attributes
+     * @return {@link FileVisitResult#CONTINUE}
      */
     @Override
     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
throws IOException {
-        if (!isHidden(file, attrs) && test(file)) {
-            sourceFiles.add(new SourceFile(sourceRoot, file, attrs, 
ignoreModification(file)));
+        if (matchers.matches(file)) {
+            sourceFiles.add(new SourceFile(
+                    sourceRoot,
+                    file,
+                    attrs,
+                    (incrementalExcludeMatchers != null) && 
incrementalExcludeMatchers.matches(file)));
         }
         return FileVisitResult.CONTINUE;
     }
@@ -264,25 +153,13 @@ final class PathFilter extends SimpleFileVisitor<Path> 
implements Predicate<Path
      */
     @Override
     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes 
attrs) throws IOException {
-        return isHidden(dir, attrs) ? FileVisitResult.SKIP_SUBTREE : 
FileVisitResult.CONTINUE;
-    }
-
-    /**
-     * {@return whether the given file is hidden}. This method is used instead 
of {@link Files#isHidden(Path)}
-     * because it opportunistically uses the available attributes instead of 
making another access to the file system.
-     */
-    private static boolean isHidden(Path file, BasicFileAttributes attrs) {
-        if (attrs instanceof DosFileAttributes dos) {
-            return dos.isHidden();
-        } else {
-            return file.getFileName().toString().startsWith(".");
-        }
+        return Files.isHidden(dir) ? FileVisitResult.SKIP_SUBTREE : 
FileVisitResult.CONTINUE;
     }
 
     /**
      * {@return all source files found in the given root directories}.
      * The include and exclude filters specified at construction time are 
applied.
-     * Hidden files and directories are ignored, and symbolic links are 
followed.
+     * Hidden directories are ignored, and symbolic links are followed.
      *
      * @param rootDirectories the root directories to scan
      * @throws IOException if a root directory cannot be walked
@@ -292,16 +169,50 @@ final class PathFilter extends SimpleFileVisitor<Path> 
implements Predicate<Path
         try {
             sourceFiles = result;
             for (SourceDirectory directory : rootDirectories) {
+                if (!incrementalExcludes.isEmpty()) {
+                    incrementalExcludeMatchers = new 
PathSelector(directory.root, incrementalExcludes, null).simplify();
+                }
+                String[] includesOrDefault = includes;
+                if (useDefaultInclude) {
+                    if (directory.includes.isEmpty()) {
+                        includesOrDefault[0] = "glob:**" + 
directory.fileKind.extension;
+                    } else {
+                        includesOrDefault = null;
+                    }
+                }
                 sourceRoot = directory;
+                matchers = new PathSelector(
+                                directory.root,
+                                concat(directory.includes, includesOrDefault),
+                                concat(directory.excludes, excludes))
+                        .simplify();
                 Files.walkFileTree(directory.root, 
EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, this);
-                fs = null; // Will force a recalculation of matchers in next 
iteration.
             }
         } catch (UncheckedIOException e) {
             throw e.getCause();
         } finally {
             sourceRoot = null;
             sourceFiles = null;
+            matchers = null;
         }
         return result;
     }
+
+    /**
+     * Returns the concatenation of patterns specified in the source with the 
patterns specified in the plugin.
+     * As a side-effect, this method set the {@link #needRelativize} flag to 
{@code true} if at least one pattern
+     * does not start with {@code "**"}. The latter is a slight optimization 
for avoiding the need to relativize
+     * each path before to give it to a matcher when this relativization is 
not necessary.
+     *
+     * @param source  the patterns specified in the {@code <source>} element
+     * @param plugin  the patterns specified in the {@code <plugin>} element, 
or null if none
+     */
+    private static List<String> concat(List<String> source, String[] plugin) {
+        if (plugin == null || plugin.length == 0) {
+            return source;
+        }
+        var patterns = new ArrayList<String>(source);
+        patterns.addAll(Arrays.asList(plugin));
+        return patterns;
+    }
 }
diff --git a/src/main/java/org/apache/maven/plugin/compiler/PathSelector.java 
b/src/main/java/org/apache/maven/plugin/compiler/PathSelector.java
new file mode 100644
index 0000000..509bfde
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugin/compiler/PathSelector.java
@@ -0,0 +1,566 @@
+/*
+ * 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.maven.plugin.compiler;
+
+import java.io.File;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Determines whether a path is selected according to include/exclude patterns.
+ * The pathnames used for method parameters will be relative to some base 
directory
+ * and use {@code '/'} as separator, regardless of the hosting operating 
system.
+ *
+ * <h2>Syntax</h2>
+ * If a pattern contains the {@code ':'} character and the prefix before is 
longer than 1 character,
+ * then that pattern is given verbatim to {@link 
FileSystem#getPathMatcher(String)}, which interprets
+ * the part before {@code ':'} as the syntax (usually {@code "glob"} or {@code 
"regex"}).
+ * If a pattern does not contain the {@code ':'} character, or if the prefix 
is one character long
+ * (interpreted as a Windows drive), then the syntax defaults to a 
reproduction of the Maven 3 behavior.
+ * This is implemented as the {@code "glob"} syntax with the following 
modifications:
+ *
+ * <ul>
+ *   <li>The platform-specific separator ({@code '\\'} on Windows) is replaced 
by {@code '/'}.
+ *       Note that it means that the backslash cannot be used for escaping 
characters.</li>
+ *   <li>Trailing {@code "/"} is completed as {@code "/**"}.</li>
+ *   <li>The {@code "**"} wildcard means "0 or more directories" instead of "1 
or more directories".
+ *       This is implemented by adding variants of the pattern without the 
{@code "**"} wildcard.</li>
+ *   <li>Bracket characters [ ] and { } are escaped.</li>
+ *   <li>On Unix only, the escape character {@code '\\'} is itself 
escaped.</li>
+ * </ul>
+ *
+ * If above changes are not desired, put an explicit {@code "glob:"} prefix 
before the pattern.
+ * Note that putting such a prefix is recommended anyway for better 
performances.
+ *
+ * @author Benjamin Bentmann
+ * @author Martin Desruisseaux
+ *
+ * @see java.nio.file.FileSystem#getPathMatcher(String)
+ */
+final class PathSelector implements PathMatcher {
+    /**
+     * Maximum number of characters of the prefix before {@code ':'} for 
handling as a Maven syntax.
+     */
+    private static final int MAVEN_SYNTAX_THRESHOLD = 1;
+
+    /**
+     * The default syntax to use if none was specified. Note that when this 
default syntax is applied,
+     * the user-provided pattern get some changes as documented in class 
Javadoc.
+     */
+    private static final String DEFAULT_SYNTAX = "glob:";
+
+    /**
+     * Characters having a special meaning in the glob syntax.
+     *
+     * @see FileSystem#getPathMatcher(String)
+     */
+    private static final String SPECIAL_CHARACTERS = "*?[]{}\\";
+
+    /**
+     * A path matcher which accepts all files.
+     *
+     * @see #simplify()
+     */
+    private static final PathMatcher INCLUDES_ALL = (path) -> true;
+
+    /**
+     * String representations of the normalized include filters.
+     * Each pattern shall be prefixed by its syntax, which is {@value 
#DEFAULT_SYNTAX} by default.
+     * An empty array means to include all files.
+     *
+     * @see #toString()
+     */
+    private final String[] includePatterns;
+
+    /**
+     * String representations of the normalized exclude filters.
+     * Each pattern shall be prefixed by its syntax. If no syntax is specified,
+     * the default is a Maven 3 syntax similar, but not identical, to {@value 
#DEFAULT_SYNTAX}.
+     * This array may be longer or shorter than the user-supplied excludes, 
depending on whether
+     * default excludes have been added and whether some unnecessary excludes 
have been omitted.
+     *
+     * @see #toString()
+     */
+    private final String[] excludePatterns;
+
+    /**
+     * The matcher for includes. The length of this array is equal to {@link 
#includePatterns} array length.
+     * An empty array means to include all files.
+     */
+    private final PathMatcher[] includes;
+
+    /**
+     * The matcher for excludes. The length of this array is equal to {@link 
#excludePatterns} array length.
+     */
+    private final PathMatcher[] excludes;
+
+    /**
+     * The matcher for all directories to include. This array includes the 
parents of all those directories,
+     * because they need to be accepted before we can walk to the 
sub-directories.
+     * This is an optimization for skipping whole directories when possible.
+     * An empty array means to include all directories.
+     */
+    private final PathMatcher[] dirIncludes;
+
+    /**
+     * The matcher for directories to exclude. This array does <em>not</em> 
include the parent directories,
+     * because they may contain other sub-trees that need to be included.
+     * This is an optimization for skipping whole directories when possible.
+     */
+    private final PathMatcher[] dirExcludes;
+
+    /**
+     * The base directory. All files will be relativized to that directory 
before to be matched.
+     */
+    private final Path baseDirectory;
+
+    /**
+     * Whether paths must be relativized before to be given to a matcher. If 
{@code true}, then every paths
+     * will be made relative to {@link #baseDirectory} for allowing patterns 
like {@code "foo/bar/*.java"}
+     * to work. As a slight optimization, we can skip this step if all 
patterns start with {@code "**"}.
+     */
+    private final boolean needRelativize;
+
+    /**
+     * Creates a new selector from the given includes and excludes.
+     *
+     * @param directory the base directory of the files to filter
+     * @param includes the patterns of the files to include, or null or empty 
for including all files
+     * @param excludes the patterns of the files to exclude, or null or empty 
for no exclusion
+     */
+    PathSelector(Path directory, Collection<String> includes, 
Collection<String> excludes) {
+        includePatterns = normalizePatterns(includes, false);
+        excludePatterns = normalizePatterns(effectiveExcludes(excludes, 
includePatterns), true);
+        baseDirectory = directory;
+        FileSystem fs = directory.getFileSystem();
+        this.includes = matchers(fs, includePatterns);
+        this.excludes = matchers(fs, excludePatterns);
+        dirIncludes = matchers(fs, directoryPatterns(includePatterns, false));
+        dirExcludes = matchers(fs, directoryPatterns(excludePatterns, true));
+        needRelativize = needRelativize(includePatterns) || 
needRelativize(excludePatterns);
+    }
+
+    /**
+     * Returns the given array of excludes, optionally expanded with a default 
set of excludes,
+     * then with unnecessary excludes omitted. An unnecessary exclude is an 
exclude which will never
+     * match a file because there is no include which would accept a file that 
could match the exclude.
+     * For example, if the only include is {@code "*.java"}, then the 
<code>"**&sol;project.pj"</code>,
+     * <code>"**&sol;.DS_Store"</code> and other excludes will never match a 
file and can be omitted.
+     * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} 
contains many elements,
+     * removing unnecessary excludes can reduce a lot the number of matches 
tested on each source file.
+     *
+     * <h4>Implementation note</h4>
+     * The removal of unnecessary excludes is done on a best effort basis. The 
current implementation
+     * compares only the prefixes and suffixes of each pattern, keeping the 
pattern in case of doubt.
+     * This is not bad, but it does not remove all unnecessary patterns. It 
would be possible to do
+     * better in the future if benchmarking suggests that it would be worth 
the effort.
+     *
+     * @param excludes the user-specified excludes, potentially not yet 
converted to glob syntax
+     * @param includes the include patterns converted to glob syntax
+     * @param useDefaultExcludes whether to expand user exclude with the set 
of default excludes
+     * @return the potentially expanded or reduced set of excludes to use
+     */
+    private static Collection<String> effectiveExcludes(Collection<String> 
excludes, final String[] includes) {
+        if (excludes == null || excludes.isEmpty()) {
+            return List.of();
+        } else {
+            excludes = new ArrayList<>(excludes);
+            excludes.removeIf(Objects::isNull);
+        }
+        if (includes.length == 0) {
+            return excludes;
+        }
+        /*
+         * Get the prefixes and suffixes of all includes, stopping at the 
first special character.
+         * Redundant prefixes and suffixes are omitted.
+         */
+        var prefixes = new String[includes.length];
+        var suffixes = new String[includes.length];
+        for (int i = 0; i < includes.length; i++) {
+            String include = includes[i];
+            if (!include.startsWith(DEFAULT_SYNTAX)) {
+                return excludes; // Do not filter if at least one pattern is 
too complicated.
+            }
+            include = include.substring(DEFAULT_SYNTAX.length());
+            prefixes[i] = prefixOrSuffix(include, false);
+            suffixes[i] = prefixOrSuffix(include, true);
+        }
+        prefixes = sortByLength(prefixes, false);
+        suffixes = sortByLength(suffixes, true);
+        /*
+         * Keep only the exclude which start with one of the prefixes and end 
with one of the suffixes.
+         * Note that a prefix or suffix may be the empty string, which match 
everything.
+         */
+        final Iterator<String> it = excludes.iterator();
+        nextExclude:
+        while (it.hasNext()) {
+            final String exclude = it.next();
+            final int s = exclude.indexOf(':');
+            if (s <= MAVEN_SYNTAX_THRESHOLD || 
exclude.startsWith(DEFAULT_SYNTAX)) {
+                if (cannotMatch(exclude, prefixes, false) || 
cannotMatch(exclude, suffixes, true)) {
+                    it.remove();
+                }
+            }
+        }
+        return excludes;
+    }
+
+    /**
+     * Returns the maximal amount of ordinary characters at the beginning or 
end of the given pattern.
+     * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS 
special character}.
+     *
+     * @param include the pattern for which to get a prefix or suffix without 
special character
+     * @param suffix {@code false} if a prefix is desired, or {@code true} if 
a suffix is desired
+     */
+    private static String prefixOrSuffix(final String include, boolean suffix) 
{
+        int s = suffix ? -1 : include.length();
+        for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) {
+            char c = SPECIAL_CHARACTERS.charAt(i);
+            if (suffix) {
+                s = Math.max(s, include.lastIndexOf(c));
+            } else {
+                int p = include.indexOf(c);
+                if (p >= 0 && p < s) {
+                    s = p;
+                }
+            }
+        }
+        return suffix ? include.substring(s + 1) : include.substring(0, s);
+    }
+
+    /**
+     * Returns {@code true} if the given exclude cannot match any include 
patterns.
+     * In case of doubt, returns {@code false}.
+     *
+     * @param exclude the exclude pattern to test
+     * @param fragments the prefixes or suffixes (fragments without special 
characters) of the includes
+     * @param suffix {@code false} if the specified fragments are prefixes, 
{@code true} if they are suffixes
+     * @return {@code true} if it is certain that the exclude pattern cannot 
match, or {@code false} in case of doubt
+     */
+    private static boolean cannotMatch(String exclude, final String[] 
fragments, final boolean suffix) {
+        exclude = prefixOrSuffix(exclude, suffix);
+        for (String fragment : fragments) {
+            int fg = fragment.length();
+            int ex = exclude.length();
+            int length = Math.min(fg, ex);
+            if (suffix) {
+                fg -= length;
+                ex -= length;
+            } else {
+                fg = 0;
+                ex = 0;
+            }
+            if (exclude.regionMatches(ex, fragment, fg, length)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Sorts the given patterns by their length. The main intent is to have 
the empty string first,
+     * while will cause the loops testing for prefixes and suffixes to stop 
almost immediately.
+     * Short prefixes or suffixes are also more likely to be matched.
+     *
+     * @param fragments the fragments to sort in-place
+     * @param suffix {@code false} if the specified fragments are prefixes, 
{@code true} if they are suffixes
+     * @return the given array, or a smaller array if some fragments were 
discarded because redundant
+     */
+    private static String[] sortByLength(final String[] fragments, final 
boolean suffix) {
+        Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length());
+        int count = 0;
+        /*
+         * Simplify the array of prefixes or suffixes by removing all 
redundant elements.
+         * An element is redundant if there is a shorter prefix or suffix with 
the same characters.
+         */
+        nextBase:
+        for (String fragment : fragments) {
+            for (int i = count; --i >= 0; ) {
+                String base = fragments[i];
+                if (suffix ? fragment.endsWith(base) : 
fragment.startsWith(base)) {
+                    continue nextBase; // Skip this fragment
+                }
+            }
+            fragments[count++] = fragment;
+        }
+        return (fragments.length == count) ? fragments : 
Arrays.copyOf(fragments, count);
+    }
+
+    /**
+     * Returns the given array of patterns with path separator normalized to 
{@code '/'}.
+     * Null or empty patterns are ignored, and duplications are removed.
+     *
+     * @param patterns the patterns to normalize
+     * @param excludes whether the patterns are exclude patterns
+     * @return normalized patterns without null, empty or duplicated patterns
+     */
+    private static String[] normalizePatterns(final Collection<String> 
patterns, final boolean excludes) {
+        if (patterns == null || patterns.isEmpty()) {
+            return new String[0];
+        }
+        // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
+        final var normalized = new LinkedHashSet<String>(patterns.size());
+        for (String pattern : patterns) {
+            if (pattern != null && !pattern.isEmpty()) {
+                if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) {
+                    pattern = pattern.replace(File.separatorChar, '/');
+                    if (pattern.endsWith("/")) {
+                        pattern += "**";
+                    }
+                    // Following are okay only when "**" means "0 or more 
directories".
+                    while (pattern.endsWith("/**/**")) {
+                        pattern = pattern.substring(0, pattern.length() - 3);
+                    }
+                    while (pattern.startsWith("**/**/")) {
+                        pattern = pattern.substring(3);
+                    }
+                    pattern = pattern.replace("/**/**/", "/**/");
+                    pattern = pattern.replace("\\", "\\\\")
+                            .replace("[", "\\[")
+                            .replace("]", "\\]")
+                            .replace("{", "\\{")
+                            .replace("}", "\\}");
+                    normalized.add(DEFAULT_SYNTAX + pattern);
+                    /*
+                     * If the pattern starts or ends with "**", Java GLOB 
expects a directory level at
+                     * that location while Maven seems to consider that "**" 
can mean "no directory".
+                     * Add another pattern for reproducing this effect.
+                     */
+                    addPatternsWithOneDirRemoved(normalized, pattern, 0);
+                } else {
+                    normalized.add(pattern);
+                }
+            }
+        }
+        return simplify(normalized, excludes);
+    }
+
+    /**
+     * Adds all variants of the given pattern with {@code **} removed.
+     * This is used for simulating the Maven behavior where {@code "**} may 
match zero directory.
+     * Tests suggest that we need an explicit GLOB pattern with no {@code 
"**"} for matching an absence of directory.
+     *
+     * @param patterns where to add the derived patterns
+     * @param pattern  the pattern for which to add derived forms, without the 
"glob:" syntax prefix
+     * @param end      should be 0 (reserved for recursive invocations of this 
method)
+     */
+    private static void addPatternsWithOneDirRemoved(final Set<String> 
patterns, final String pattern, int end) {
+        final int length = pattern.length();
+        int start;
+        while ((start = pattern.indexOf("**", end)) >= 0) {
+            end = start + 2; // 2 is the length of "**".
+            if (end < length) {
+                if (pattern.charAt(end) != '/') {
+                    continue;
+                }
+                if (start == 0) {
+                    end++; // Ommit the leading slash if there is nothing 
before it.
+                }
+            }
+            if (start > 0 && pattern.charAt(--start) != '/') {
+                continue;
+            }
+            String reduced = pattern.substring(0, start) + 
pattern.substring(end);
+            patterns.add(DEFAULT_SYNTAX + reduced);
+            addPatternsWithOneDirRemoved(patterns, reduced, start);
+        }
+    }
+
+    /**
+     * Applies some heuristic rules for simplifying the set of patterns,
+     * then returns the patterns as an array.
+     *
+     * @param patterns the patterns to simplify and return as an array
+     * @param excludes whether the patterns are exclude patterns
+     * @return the set content as an array, after simplification
+     */
+    private static String[] simplify(Set<String> patterns, boolean excludes) {
+        /*
+         * If the "**" pattern is present, it makes all other patterns useless.
+         * In the case of include patterns, an empty set means to include 
everything.
+         */
+        if (patterns.remove("**")) {
+            patterns.clear();
+            if (excludes) {
+                patterns.add("**");
+            }
+        }
+        return patterns.toArray(String[]::new);
+    }
+
+    /**
+     * Eventually adds the parent directory of the given patterns, without 
duplicated values.
+     * The patterns given to this method should have been normalized.
+     *
+     * @param patterns the normalized include or exclude patterns
+     * @param excludes whether the patterns are exclude patterns
+     * @return pattens of directories to include or exclude
+     */
+    private static String[] directoryPatterns(final String[] patterns, final 
boolean excludes) {
+        // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
+        final var directories = new LinkedHashSet<String>(patterns.length);
+        for (String pattern : patterns) {
+            if (pattern.startsWith(DEFAULT_SYNTAX)) {
+                if (excludes) {
+                    if (pattern.endsWith("/**")) {
+                        directories.add(pattern.substring(0, pattern.length() 
- 3));
+                    }
+                } else {
+                    int s = pattern.indexOf(':');
+                    if (pattern.regionMatches(++s, "**/", 0, 3)) {
+                        s = pattern.indexOf('/', s + 3);
+                        if (s < 0) {
+                            return new String[0]; // Pattern is "**", so we 
need to accept everything.
+                        }
+                        directories.add(pattern.substring(0, s));
+                    }
+                }
+            }
+        }
+        return simplify(directories, excludes);
+    }
+
+    /**
+     * Returns {@code true} if at least one pattern requires path to be 
relativized before to be matched.
+     *
+     * @param patterns include or exclude patterns
+     * @return whether at least one pattern require relativization
+     */
+    private static boolean needRelativize(String[] patterns) {
+        for (String pattern : patterns) {
+            if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Creates the path matchers for the given patterns.
+     * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for 
each pattern.
+     */
+    private static PathMatcher[] matchers(final FileSystem fs, final String[] 
patterns) {
+        final var matchers = new PathMatcher[patterns.length];
+        for (int i = 0; i < patterns.length; i++) {
+            matchers[i] = fs.getPathMatcher(patterns[i]);
+        }
+        return matchers;
+    }
+
+    /**
+     * {@return a potentially simpler matcher equivalent to this matcher}.
+     */
+    @SuppressWarnings("checkstyle:MissingSwitchDefault")
+    public PathMatcher simplify() {
+        if (!needRelativize && excludes.length == 0) {
+            switch (includes.length) {
+                case 0:
+                    return INCLUDES_ALL;
+                case 1:
+                    return includes[0];
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Determines whether a path is selected.
+     * This is true if the given file matches an include pattern and no 
exclude pattern.
+     *
+     * @param path the pathname to test, must not be {@code null}
+     * @return {@code true} if the given path is selected, {@code false} 
otherwise
+     */
+    @Override
+    public boolean matches(Path path) {
+        if (needRelativize) {
+            path = baseDirectory.relativize(path);
+        }
+        return (includes.length == 0 || isMatched(path, includes))
+                && (excludes.length == 0 || !isMatched(path, excludes));
+    }
+
+    /**
+     * {@return whether the given file matches according to one of the given 
matchers}.
+     */
+    private static boolean isMatched(Path path, PathMatcher[] matchers) {
+        for (PathMatcher matcher : matchers) {
+            if (matcher.matches(path)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determines whether a directory could contain selected paths.
+     *
+     * @param directory the directory pathname to test, must not be {@code 
null}
+     * @return {@code true} if the given directory might contain selected 
paths, {@code false} if the
+     *         directory will definitively not contain selected paths
+     */
+    public boolean couldHoldSelected(Path directory) {
+        if (baseDirectory.equals(directory)) {
+            return true;
+        }
+        directory = baseDirectory.relativize(directory);
+        return (dirIncludes.length == 0 || isMatched(directory, dirIncludes))
+                && (dirExcludes.length == 0 || !isMatched(directory, 
dirExcludes));
+    }
+
+    /**
+     * Appends the elements of the given array in the given buffer.
+     * This is a helper method for {@link #toString()} implementations.
+     *
+     * @param buffer the buffer to add the elements to
+     * @param label label identifying the array of elements to add
+     * @param patterns the elements to append, or {@code null} if none
+     */
+    private static void append(StringBuilder buffer, String label, String[] 
patterns) {
+        buffer.append(label).append(": [");
+        if (patterns != null) {
+            for (int i = 0; i < patterns.length; i++) {
+                if (i != 0) {
+                    buffer.append(", ");
+                }
+                buffer.append(patterns[i]);
+            }
+        }
+        buffer.append(']');
+    }
+
+    /**
+     * {@return a string representation for logging purposes}.
+     */
+    @Override
+    public String toString() {
+        var buffer = new StringBuilder();
+        append(buffer, "includes", includePatterns);
+        append(buffer.append(", "), "excludes", excludePatterns);
+        return buffer.toString();
+    }
+}
diff --git 
a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java 
b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
index 4429a26..920981b 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
@@ -21,10 +21,8 @@ package org.apache.maven.plugin.compiler;
 import javax.lang.model.SourceVersion;
 import javax.tools.JavaFileObject;
 
-import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.PathMatcher;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -80,7 +78,7 @@ final class SourceDirectory {
      *
      * @see PathFilter#includes
      */
-    final List<PathMatcher> includes;
+    final List<String> includes;
 
     /**
      * Filter for excluding files below the {@linkplain #root} directory, or 
an empty list for no exclusion.
@@ -88,7 +86,7 @@ final class SourceDirectory {
      *
      * @see PathFilter#excludes
      */
-    final List<PathMatcher> excludes;
+    final List<String> excludes;
 
     /**
      * Kind of source files in this directory. This is usually {@link 
JavaFileObject.Kind#SOURCE}.
@@ -161,6 +159,8 @@ final class SourceDirectory {
      * Creates a new source directory.
      *
      * @param root the root directory of all source files
+     * @param includes patterns for selecting files below the root directory, 
or an empty list for the default filter
+     * @param excludes patterns for excluding files below the root directory, 
or an empty list for no exclusion
      * @param fileKind kind of source files in this directory (usually {@code 
SOURCE})
      * @param moduleName name of the module for which source directories are 
provided, or {@code null} if none
      * @param release Java release for which source directories are provided, 
or {@code null} for the default release
@@ -170,8 +170,8 @@ final class SourceDirectory {
     @SuppressWarnings("checkstyle:ParameterNumber")
     private SourceDirectory(
             Path root,
-            List<PathMatcher> includes,
-            List<PathMatcher> excludes,
+            List<String> includes,
+            List<String> excludes,
             JavaFileObject.Kind fileKind,
             String moduleName,
             SourceVersion release,
@@ -286,11 +286,10 @@ final class SourceDirectory {
                     fileKind = JavaFileObject.Kind.SOURCE;
                     outputFileKind = JavaFileObject.Kind.CLASS;
                 }
-                FileSystem fs = directory.getFileSystem();
                 roots.add(new SourceDirectory(
                         directory,
-                        
source.includes().stream().map(fs::getPathMatcher).toList(),
-                        
source.excludes().stream().map(fs::getPathMatcher).toList(),
+                        source.includes(),
+                        source.excludes(),
                         fileKind,
                         source.module().orElse(null),
                         targetVersion(source).orElse(release),


Reply via email to