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.FileSystem;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.Objects;
30  import java.util.Optional;
31  
32  /**
33   * A single root directory of source files, associated with module name and release version.
34   * The module names are used when compiling a Module Source Hierarchy.
35   * The release version is used for multi-versions JAR files.
36   *
37   * <p>This class contains also the output directory, because this information is needed
38   * for determining whether a source file need to be recompiled.</p>
39   *
40   * @author Martin Desruisseaux
41   */
42  final class SourceDirectory {
43      /**
44       * The module-info filename, without extension.
45       */
46      static final String MODULE_INFO = "module-info";
47  
48      /**
49       * File suffix of source code.
50       */
51      static final String JAVA_FILE_SUFFIX = ".java";
52  
53      /**
54       * File suffix of compiler classes.
55       */
56      static final String CLASS_FILE_SUFFIX = ".class";
57  
58      /**
59       * The root directory of all source files.
60       */
61      final Path root;
62  
63      /**
64       * Kind of source files in this directory. This is usually {@link JavaFileObject.Kind#SOURCE}.
65       * This information is used for building a default include filter such as {@code "glob:*.java}
66       * if the user didn't specified an explicit filter. The default include filter may change for
67       * each root directory.
68       */
69      final JavaFileObject.Kind fileKind;
70  
71      /**
72       * Name of the module for which source directories are provided, or {@code null} if none.
73       * This name is supplied to the constructor instead of parsed from {@code module-info.java}
74       * file because the latter may not exist in this directory. For example, in a multi-release
75       * project the module-info may be declared in another directory for the base version.
76       */
77      final String moduleName;
78  
79      /**
80       * Path to the {@code module-info} file, or {@code null} if none. This flag is set when
81       * walking through the directory content. This is related, but not strictly equivalent,
82       * to whether the {@link #moduleName} is non-null.
83       */
84      private Path moduleInfo;
85  
86      /**
87       * The Java release for which source directories are provided, or {@code null} for the default release.
88       * This is used for multi-versions JAR files.
89       */
90      final SourceVersion release;
91  
92      /**
93       * The directory where to store the compilation results.
94       * This is the MOJO output directory with sub-directories appended according the following rules, in that order:
95       *
96       * <ol>
97       *   <li>If {@link #moduleName} is non-null, then the module name is appended.</li>
98       *   <li>If {@link #release} is non-null, then the next elements in the paths are
99       *       {@code "META-INF/versions/<n>"} where {@code <n>} is the release number.</li>
100      * </ol>
101      */
102     final Path outputDirectory;
103 
104     /**
105      * Kind of output files in the output directory.
106      * This is usually {@link JavaFileObject.Kind#CLASS}.
107      */
108     final JavaFileObject.Kind outputFileKind;
109 
110     /**
111      * Creates a new source directory.
112      *
113      * @param root the root directory of all source files
114      * @param fileKind kind of source files in this directory (usually {@code SOURCE})
115      * @param moduleName name of the module for which source directories are provided, or {@code null} if none
116      * @param release Java release for which source directories are provided, or {@code null} for the default release
117      * @param outputDirectory the directory where to store the compilation results
118      * @param outputFileKind Kind of output files in the output directory (usually {@ codeCLASS})
119      */
120     private SourceDirectory(
121             Path root,
122             JavaFileObject.Kind fileKind,
123             String moduleName,
124             SourceVersion release,
125             Path outputDirectory,
126             JavaFileObject.Kind outputFileKind) {
127         this.root = Objects.requireNonNull(root);
128         this.fileKind = Objects.requireNonNull(fileKind);
129         this.moduleName = moduleName;
130         this.release = release;
131         if (release != null) {
132             String version = release.name();
133             version = version.substring(version.lastIndexOf('_') + 1);
134             FileSystem fs = outputDirectory.getFileSystem();
135             Path subdir;
136             if (moduleName != null) {
137                 subdir = fs.getPath(moduleName, "META-INF", "versions", version);
138             } else {
139                 subdir = fs.getPath("META-INF", "versions", version);
140             }
141             outputDirectory = outputDirectory.resolve(subdir);
142         } else if (moduleName != null) {
143             outputDirectory = outputDirectory.resolve(moduleName);
144         }
145         this.outputDirectory = outputDirectory;
146         this.outputFileKind = outputFileKind;
147     }
148 
149     /**
150      * Converts the given list of paths to a list of source directories.
151      * The returned list includes only the directories that exist.
152      *
153      * @param compileSourceRoots the root paths to source files
154      * @param outputDirectory the directory where to store the compilation results
155      * @return the given list of paths wrapped as source directory objects
156      */
157     static List<SourceDirectory> fromPaths(List<Path> compileSourceRoots, Path outputDirectory) {
158         var roots = new ArrayList<SourceDirectory>(compileSourceRoots.size());
159         for (Path p : compileSourceRoots) {
160             if (Files.exists(p)) {
161                 // TODO: specify file kind, module name and release version.
162                 roots.add(new SourceDirectory(
163                         p, JavaFileObject.Kind.SOURCE, null, null, outputDirectory, JavaFileObject.Kind.CLASS));
164             }
165         }
166         return roots;
167     }
168 
169     /**
170      * Returns whether the given file is a {@code module-info.java} file.
171      * TODO: we could make this method non-static and verify that the given
172      * file is in the root of this directory.
173      */
174     static boolean isModuleInfoSource(Path file) {
175         return (MODULE_INFO + JAVA_FILE_SUFFIX).equals(file.getFileName().toString());
176     }
177 
178     /**
179      * Invoked for each source files in this directory.
180      */
181     void visit(Path sourceFile) {
182         if (isModuleInfoSource(sourceFile)) {
183             // Paranoiac check: only one file should exist, but if many, keep the one closest to the root.
184             if (moduleInfo == null || moduleInfo.getNameCount() >= sourceFile.getNameCount()) {
185                 moduleInfo = sourceFile;
186             }
187         }
188     }
189 
190     /**
191      * Path to the {@code module-info.java} source file, or empty if none.
192      * This information is accurate only after {@link PathFilter} finished
193      * to walk through all source files in a directory.
194      */
195     public Optional<Path> getModuleInfo() {
196         return Optional.ofNullable(moduleInfo);
197     }
198 
199     /**
200      * Compares the given object with this source directory for equality.
201      *
202      * @param obj the object to compare
203      * @return whether the two objects have the same path, module name and release version
204      */
205     @Override
206     public boolean equals(Object obj) {
207         if (obj instanceof SourceDirectory other) {
208             return release == other.release
209                     && Objects.equals(moduleName, other.moduleName)
210                     && root.equals(other.root)
211                     && outputDirectory.equals(other.outputDirectory);
212         }
213         return false;
214     }
215 
216     /**
217      * {@return a hash code value for this root directory}.
218      */
219     @Override
220     public int hashCode() {
221         return root.hashCode() + 7 * Objects.hash(moduleName, release);
222     }
223 
224     /**
225      * {@return a string representation of this root directory for debugging purposes}.
226      */
227     @Override
228     public String toString() {
229         var sb = new StringBuilder(100).append('"').append(root).append('"');
230         if (moduleName != null) {
231             sb.append(" for module \"").append(moduleName).append('"');
232         }
233         if (release != null) {
234             sb.append(" on Java release ").append(release);
235         }
236         return sb.toString();
237     }
238 }