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 }