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.plugin.compiler;
20  
21  import java.io.IOException;
22  import java.io.UncheckedIOException;
23  import java.nio.file.FileSystem;
24  import java.nio.file.FileVisitOption;
25  import java.nio.file.FileVisitResult;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.PathMatcher;
29  import java.nio.file.SimpleFileVisitor;
30  import java.nio.file.attribute.BasicFileAttributes;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.EnumSet;
35  import java.util.List;
36  
37  /**
38   * Applies inclusion and exclusion filters on paths, and builds a list of files in a directory tree.
39   * The set of allowed syntax contains at least "glob" and "regex".
40   * See {@link FileSystem#getPathMatcher(String)} Javadoc for a description of the "glob" syntax.
41   * If no syntax is specified, then the default syntax is a derivative of the "glob" syntax which
42   * reproduces the behavior of Maven 3.
43   *
44   * <p>The list of files to process is built by applying the path matcher on each regular (non directory) files.
45   * The walk in file trees has the following characteristics:</p>
46   *
47   * <ul>
48   *   <li>Symbolic links are followed.</li>
49   *   <li>Hidden files and hidden directories are ignored.</li>
50   * </ul>
51   *
52   * Instances of this class can be reused for filtering many directories, but is not thread safe.
53   * Each instance shall be used by a single thread only.
54   *
55   * @author Martin Desruisseaux
56   */
57  final class PathFilter extends SimpleFileVisitor<Path> {
58      /**
59       * Whether to use the default include pattern.
60       * The pattern depends on the type of source file.
61       *
62       * @see javax.tools.JavaFileObject.Kind#extension
63       */
64      private final boolean useDefaultInclude;
65  
66      /**
67       * Inclusion filters for the files in the directories to walk as specified in the plugin configuration.
68       * The array should contain at least one element. If {@link #useDefaultInclude} is {@code true}, then
69       * this array length shall be exactly 1 and the single element is overwritten for each directory to walk.
70       *
71       * @see SourceDirectory#includes
72       */
73      private final String[] includes;
74  
75      /**
76       * Exclusion filters for the files in the directories to walk as specified in the plugin configuration.
77       *
78       * @see SourceDirectory#excludes
79       */
80      private final String[] excludes;
81  
82      /**
83       * Combination of include and exclude filters. This is an instance of {@link PathSelector},
84       * unless the includes/excludes can be simplified to a single standard matcher instance.
85       */
86      private PathMatcher matchers;
87  
88      /**
89       * All exclusion filters for incremental build calculation, or an empty list if none.
90       * Updated files, if excluded by a pattern, will not cause the project to be rebuilt.
91       */
92      private final Collection<String> incrementalExcludes;
93  
94      /**
95       * The matchers for exclusion filters for incremental build calculation.
96       * May be an instance of {@link PathSelector}, or {@code null} if none.
97       */
98      private PathMatcher incrementalExcludeMatchers;
99  
100     /**
101      * The result of listing all files, or {@code null} if no walking is in progress.
102      * This field is temporarily assigned a value when walking in a tree of directories,
103      * then reset to {@code null} after the walk finished.
104      */
105     private List<SourceFile> sourceFiles;
106 
107     /**
108      * The root directory of files being scanned.
109      * This field is temporarily assigned a value when walking in a tree of directories,
110      * then reset to {@code null} after the walk finished.
111      */
112     private SourceDirectory sourceRoot;
113 
114     /**
115      * Creates a new filter.
116      *
117      * @param mojo the <abbr>MOJO</abbr> from which to take the includes/excludes configuration
118      */
119     PathFilter(AbstractCompilerMojo mojo) {
120         Collection<String> specified = mojo.getIncludes();
121         useDefaultInclude = specified.isEmpty();
122         if (useDefaultInclude) {
123             specified = List.of("**"); // Place-holder replaced by "**/*.java" in `test(…)`.
124         }
125         includes = specified.toArray(String[]::new);
126         excludes = mojo.getExcludes().toArray(String[]::new);
127         incrementalExcludes = mojo.getIncrementalExcludes();
128     }
129 
130     /**
131      * Invoked for a file in a directory. If the given file passes the include/exclude filters,
132      * then it is added to the list of source files.
133      *
134      * @param  file  the source file to test
135      * @param  attrs the file basic attributes
136      * @return {@link FileVisitResult#CONTINUE}
137      */
138     @Override
139     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
140         if (matchers.matches(file)) {
141             sourceFiles.add(new SourceFile(
142                     sourceRoot,
143                     file,
144                     attrs,
145                     (incrementalExcludeMatchers != null) && incrementalExcludeMatchers.matches(file)));
146         }
147         return FileVisitResult.CONTINUE;
148     }
149 
150     /**
151      * Invoked for a directory before entries in the directory are visited.
152      * If the directory is hidden, then it is skipped.
153      */
154     @Override
155     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
156         return Files.isHidden(dir) ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
157     }
158 
159     /**
160      * {@return all source files found in the given root directories}
161      * The include and exclude filters specified at construction time are applied.
162      * Hidden directories are ignored, and symbolic links are followed.
163      *
164      * @param rootDirectories the root directories to scan
165      * @throws IOException if a root directory cannot be walked
166      */
167     public List<SourceFile> walkSourceFiles(Iterable<SourceDirectory> rootDirectories) throws IOException {
168         final var result = new ArrayList<SourceFile>();
169         try {
170             sourceFiles = result;
171             for (SourceDirectory directory : rootDirectories) {
172                 if (!incrementalExcludes.isEmpty()) {
173                     incrementalExcludeMatchers = new PathSelector(directory.root, incrementalExcludes, null).simplify();
174                 }
175                 String[] includesOrDefault = includes;
176                 if (useDefaultInclude) {
177                     if (directory.includes.isEmpty()) {
178                         includesOrDefault[0] = "glob:**" + directory.fileKind.extension;
179                     } else {
180                         includesOrDefault = null;
181                     }
182                 }
183                 sourceRoot = directory;
184                 matchers = new PathSelector(
185                                 directory.root,
186                                 concat(directory.includes, includesOrDefault),
187                                 concat(directory.excludes, excludes))
188                         .simplify();
189                 Files.walkFileTree(directory.root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, this);
190             }
191         } catch (UncheckedIOException e) {
192             throw e.getCause();
193         } finally {
194             sourceRoot = null;
195             sourceFiles = null;
196             matchers = null;
197         }
198         return result;
199     }
200 
201     /**
202      * Returns the concatenation of patterns specified in the source with the patterns specified in the plugin.
203      * As a side-effect, this method set the {@link #needRelativize} flag to {@code true} if at least one pattern
204      * does not start with {@code "**"}. The latter is a slight optimization for avoiding the need to relativize
205      * each path before to give it to a matcher when this relativization is not necessary.
206      *
207      * @param source  the patterns specified in the {@code <source>} element
208      * @param plugin  the patterns specified in the {@code <plugin>} element, or null if none
209      */
210     private static List<String> concat(List<String> source, String[] plugin) {
211         if (plugin == null || plugin.length == 0) {
212             return source;
213         }
214         var patterns = new ArrayList<String>(source);
215         patterns.addAll(Arrays.asList(plugin));
216         return patterns;
217     }
218 }