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 javax.lang.model.SourceVersion;
22  import javax.tools.JavaFileObject;
23  
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.Objects;
29  import java.util.Optional;
30  import java.util.stream.Stream;
31  
32  import org.apache.maven.api.Language;
33  import org.apache.maven.api.SourceRoot;
34  import org.apache.maven.api.Version;
35  
36  /**
37   * A single root directory of source files, associated with module name and release version.
38   * The module names are used when compiling a Module Source Hierarchy.
39   * The release version is used for multi-versions JAR files.
40   *
41   * <p>This class contains also the output directory, because this information is needed
42   * for determining whether a source file need to be recompiled.</p>
43   *
44   * @author Martin Desruisseaux
45   */
46  final class SourceDirectory {
47      /**
48       * The module-info filename, without extension.
49       */
50      static final String MODULE_INFO = "module-info";
51  
52      /**
53       * File suffix of source code.
54       */
55      static final String JAVA_FILE_SUFFIX = ".java";
56  
57      /**
58       * File suffix of compiler classes.
59       */
60      static final String CLASS_FILE_SUFFIX = ".class";
61  
62      /**
63       * The root directory of all source files. Whether the path is relative or absolute depends on the paths given to
64       * the {@link #fromProject fromProject(…)} or {@link #fromPluginConfiguration fromPluginConfiguration(…)} methods.
65       * This class preserves the relative/absolute characteristic of the user-specified directories in order to behave
66       * as intended by users in operations such as {@linkplain Path#relativize relativization}, especially in regard of
67       * symbolic links. In practice, this path is often an absolute path.
68       */
69      final Path root;
70  
71      /**
72       * Filter for selecting files below the {@linkplain #root} directory, or an empty list for the default filter.
73       * For the Java language, the default filter is {@code "*.java"}. The filters are used by {@link PathFilter}.
74       *
75       * <p>This field differs from {@link PathFilter#includes} in that it is specified in the {@code <source>} element,
76       * while the latter is specified in the plugin configuration. The filter specified here can be different for each
77       * source directory, while the plugin configuration applies to all source directories.</p>
78       *
79       * @see PathFilter#includes
80       */
81      final List<String> includes;
82  
83      /**
84       * Filter for excluding files below the {@linkplain #root} directory, or an empty list for no exclusion.
85       * See {@link #includes} for the difference between this field and {@link PathFilter#excludes}.
86       *
87       * @see PathFilter#excludes
88       */
89      final List<String> excludes;
90  
91      /**
92       * Kind of source files in this directory. This is usually {@link JavaFileObject.Kind#SOURCE}.
93       * This information is used for building a default include filter such as {@code "glob:*.java}
94       * if the user didn't specified an explicit filter. The default include filter may change for
95       * each root directory.
96       */
97      final JavaFileObject.Kind fileKind;
98  
99      /**
100      * Name of the module for which source directories are provided, or {@code null} if none.
101      * This name is supplied to the constructor instead of parsed from {@code module-info.java}
102      * file because the latter may not exist in this directory. For example, in a multi-release
103      * project, the module-info may be declared in another directory for the base version.
104      *
105      * @see #getModuleInfo()
106      */
107     final String moduleName;
108 
109     /**
110      * Path to the {@code module-info} file, or {@code null} if none. This flag is set when
111      * walking through the directory content. This is related, but not strictly equivalent,
112      * to whether the {@link #moduleName} is non-null.
113      *
114      * @see #getModuleInfo()
115      */
116     private Path moduleInfo;
117 
118     /**
119      * The Java release for which source directories are provided, or {@code null} for the default release.
120      * This is used for multi-versions JAR files. Note that a non-null value does not mean that the classes
121      * will be put in a {@code META-INF/versions/} subdirectory, because this version may be the base version.
122      *
123      * @see #getSpecificVersion()
124      */
125     final SourceVersion release;
126 
127     /**
128      * Whether the {@linkplain #release} is a version other than the base version.
129      * This flag is initially unknown (conservatively assumed false) and is set after the base version is known.
130      * Note that a null {@linkplain #release} is considered more recent than all non-null releases (because null
131      * stands for the default, which is usually the runtime version), and therefore is considered versioned if
132      * some non-null releases exist.
133      *
134      * @see #completeIfVersioned(SourceVersion)
135      */
136     private boolean isVersioned;
137 
138     /**
139      * The directory where to store the compilation results.
140      * This is the MOJO output directory with sub-directories appended according the following rules, in that order:
141      *
142      * <ol>
143      *   <li>If {@link #isVersioned} is {@code true}, then the relative part of the path starts with
144      *       {@code "META-INF/versions/<n>"} where {@code <n>} is the release number.</li>
145      *   <li>If {@link #moduleName} is non-null, then the module name is appended.</li>
146      * </ol>
147      *
148      * @see #getOutputDirectory()
149      */
150     private Path outputDirectory;
151 
152     /**
153      * Kind of output files in the output directory.
154      * This is usually {@link JavaFileObject.Kind#CLASS}.
155      */
156     final JavaFileObject.Kind outputFileKind;
157 
158     /**
159      * Creates a new source directory.
160      *
161      * @param root the root directory of all source files
162      * @param includes patterns for selecting files below the root directory, or an empty list for the default filter
163      * @param excludes patterns for excluding files below the root directory, or an empty list for no exclusion
164      * @param fileKind kind of source files in this directory (usually {@code SOURCE})
165      * @param moduleName name of the module for which source directories are provided, or {@code null} if none
166      * @param release Java release for which source directories are provided, or {@code null} for the default release
167      * @param outputDirectory the directory where to store the compilation results
168      * @param outputFileKind Kind of output files in the output directory (usually {@ codeCLASS})
169      */
170     @SuppressWarnings("checkstyle:ParameterNumber")
171     private SourceDirectory(
172             Path root,
173             List<String> includes,
174             List<String> excludes,
175             JavaFileObject.Kind fileKind,
176             String moduleName,
177             SourceVersion release,
178             Path outputDirectory,
179             JavaFileObject.Kind outputFileKind) {
180         this.root = Objects.requireNonNull(root);
181         this.includes = Objects.requireNonNull(includes);
182         this.excludes = Objects.requireNonNull(excludes);
183         this.fileKind = Objects.requireNonNull(fileKind);
184         this.moduleName = moduleName;
185         this.release = release;
186         if (moduleName != null) {
187             outputDirectory = outputDirectory.resolve(moduleName);
188         }
189         this.outputDirectory = outputDirectory;
190         this.outputFileKind = outputFileKind;
191     }
192 
193     /**
194      * Potentially adds the {@code META-INF/versions/} part of the path to the output directory.
195      * This method can be invoked only after the base version has been determined, which happens
196      * after all other source directories have been built.
197      *
198      * @param baseVersion the Java release target by the non-versioned classes
199      */
200     private void completeIfVersioned(SourceVersion baseVersion) {
201         @SuppressWarnings("LocalVariableHidesMemberVariable")
202         SourceVersion release = this.release;
203         isVersioned = (release != baseVersion);
204         if (isVersioned) {
205             if (release == null) {
206                 release = SourceVersion.latestSupported();
207                 // `this.release` intentionally left to null.
208             }
209             var hierarchy = (moduleName != null) ? DirectoryHierarchy.MODULE_SOURCE : DirectoryHierarchy.PACKAGE;
210             outputDirectory = hierarchy.outputDirectoryForReleases(outputDirectory, release);
211         }
212     }
213 
214     /**
215      * {@return the target version as an object from the Java tools API}
216      *
217      * @param root the source directory for which to get the target version
218      * @throws UnsupportedVersionException if the version string cannot be parsed
219      */
220     static Optional<SourceVersion> targetVersion(final SourceRoot root) {
221         return root.targetVersion().map(Version::toString).map(SourceDirectory::parse);
222     }
223 
224     /**
225      * Parses the given version string.
226      * This method parses the version with {@link Runtime.Version#parse(String)}.
227      * Therefore, for Java 8, the version shall be "8", not "1.8".
228      *
229      * @param version the version to parse, or null or empty if none
230      * @return the parsed version, or {@code null} if the given string was null or empty
231      * @throws UnsupportedVersionException if the version string cannot be parsed
232      */
233     static SourceVersion parse(final String version) {
234         if (version == null || version.isBlank()) {
235             return null;
236         }
237         try {
238             var parsed = Runtime.Version.parse(version);
239             return SourceVersion.valueOf("RELEASE_" + parsed.feature());
240             // TODO: Replace by return SourceVersion.valueOf(v) after upgrade to Java 18.
241         } catch (IllegalArgumentException e) {
242             throw new UnsupportedVersionException("Illegal version number: \"" + version + '"', e);
243         }
244     }
245 
246     /**
247      * Gets the list of source directories from the project manager.
248      * The returned list includes only the directories that exist.
249      *
250      * @param compileSourceRoots the root paths to source files
251      * @param defaultRelease the release to use if the {@code <source>} element provides none, or {@code null}
252      * @param outputDirectory the directory where to store the compilation results
253      * @return the given list of paths wrapped as source directory objects
254      */
255     static List<SourceDirectory> fromProject(
256             Stream<SourceRoot> compileSourceRoots, String defaultRelease, Path outputDirectory) {
257         var release = parse(defaultRelease); // May be null.
258         var roots = new ArrayList<SourceDirectory>();
259         compileSourceRoots.forEach((SourceRoot source) -> {
260             Path directory = source.directory();
261             if (Files.exists(directory)) {
262                 var fileKind = JavaFileObject.Kind.OTHER;
263                 var outputFileKind = JavaFileObject.Kind.OTHER;
264                 if (Language.JAVA_FAMILY.equals(source.language())) {
265                     fileKind = JavaFileObject.Kind.SOURCE;
266                     outputFileKind = JavaFileObject.Kind.CLASS;
267                 }
268                 roots.add(new SourceDirectory(
269                         directory,
270                         source.includes(),
271                         source.excludes(),
272                         fileKind,
273                         source.module().orElse(null),
274                         targetVersion(source).orElse(release),
275                         outputDirectory,
276                         outputFileKind));
277             }
278         });
279         roots.stream()
280                 .map((dir) -> dir.release)
281                 .filter(Objects::nonNull)
282                 .min(SourceVersion::compareTo)
283                 .ifPresent((baseVersion) -> roots.forEach((dir) -> dir.completeIfVersioned(baseVersion)));
284         return roots;
285     }
286 
287     /**
288      * Converts the given list of paths to a list of source directories.
289      * The returned list includes only the directories that exist.
290      * Used only when the compiler plugin is configured with the {@code compileSourceRoots} option.
291      *
292      * @param compileSourceRoots the root paths to source files
293      * @param moduleName name of the module for which source directories are provided, or {@code null} if none
294      * @param defaultRelease the release to use, or {@code null} of unspecified
295      * @param outputDirectory the directory where to store the compilation results
296      * @return the given list of paths wrapped as source directory objects
297      */
298     static List<SourceDirectory> fromPluginConfiguration(
299             List<String> compileSourceRoots, String moduleName, String defaultRelease, Path outputDirectory) {
300         var release = parse(defaultRelease); // May be null.
301         var roots = new ArrayList<SourceDirectory>(compileSourceRoots.size());
302         for (String file : compileSourceRoots) {
303             Path directory = Path.of(file);
304             if (Files.exists(directory)) {
305                 roots.add(new SourceDirectory(
306                         directory,
307                         List.of(),
308                         List.of(),
309                         JavaFileObject.Kind.SOURCE,
310                         moduleName,
311                         release,
312                         outputDirectory,
313                         JavaFileObject.Kind.CLASS));
314             }
315         }
316         return roots;
317     }
318 
319     /**
320      * Returns whether the given file is a {@code module-info.java} file.
321      * TODO: we could make this method non-static and verify that the given
322      * file is in the root of this directory.
323      */
324     static boolean isModuleInfoSource(Path file) {
325         return (MODULE_INFO + JAVA_FILE_SUFFIX).equals(file.getFileName().toString());
326     }
327 
328     /**
329      * Invoked for each source files in this directory.
330      */
331     void visit(Path sourceFile) {
332         if (isModuleInfoSource(sourceFile)) {
333             // Paranoiac check: only one file should exist, but if many, keep the one closest to the root.
334             if (moduleInfo == null || moduleInfo.getNameCount() >= sourceFile.getNameCount()) {
335                 moduleInfo = sourceFile;
336             }
337         }
338     }
339 
340     /**
341      * Path to the {@code module-info.java} source file, or empty if none.
342      * This information is accurate only after {@link PathFilter} finished
343      * to walk through all source files in a directory.
344      */
345     public Optional<Path> getModuleInfo() {
346         return Optional.ofNullable(moduleInfo);
347     }
348 
349     /**
350      * {@return the Java version of the sources in this directory if different than the base version}
351      * The value returned by this method is related to the {@code META-INF/versions/} subdirectory in
352      * the path returned by {@link #getOutputDirectory()}. If this method returns an empty value, then
353      * there is no such subdirectory (which doesn't mean that the user did not specified a Java version).
354      * If non-empty, the returned value is the value of <var>n</var> in {@code META-INF/versions/n}.
355      */
356     public Optional<SourceVersion> getSpecificVersion() {
357         return Optional.ofNullable(isVersioned ? release : null);
358     }
359 
360     /**
361      * {@return the directory where to store the compilation results}
362      * This is the <abbr>MOJO</abbr> output directory potentially completed with
363      * sub-directories for module name and {@code META-INF/versions} versioning.
364      */
365     public Path getOutputDirectory() {
366         return outputDirectory;
367     }
368 
369     /**
370      * Compares the given object with this source directory for equality.
371      *
372      * @param obj the object to compare
373      * @return whether the two objects have the same path, module name and release version
374      */
375     @Override
376     public boolean equals(Object obj) {
377         if (obj instanceof SourceDirectory other) {
378             return root.equals(other.root)
379                     && includes.equals(other.includes)
380                     && excludes.equals(other.excludes)
381                     && fileKind == other.fileKind
382                     && Objects.equals(moduleName, other.moduleName)
383                     && release == other.release
384                     && outputDirectory.equals(other.outputDirectory)
385                     && outputFileKind == other.outputFileKind;
386         }
387         return false;
388     }
389 
390     /**
391      * {@return a hash code value for this root directory}
392      */
393     @Override
394     public int hashCode() {
395         return Objects.hash(root, moduleName, release);
396     }
397 
398     /**
399      * {@return a string representation of this root directory for debugging purposes}
400      */
401     @Override
402     public String toString() {
403         var sb = new StringBuilder(100).append('"').append(root).append('"');
404         if (moduleName != null) {
405             sb.append(" for module \"").append(moduleName).append('"');
406         }
407         if (release != null) {
408             sb.append(" on Java release ").append(release);
409         }
410         return sb.toString();
411     }
412 }