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 #moduleName} is non-null, then the module name is appended.</li> 144 * <li>If {@link #isVersioned} is {@code true}, then the next elements in the paths are 145 * {@code "META-INF/versions/<n>"} where {@code <n>} is the release number.</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 private void completeIfVersioned(SourceVersion baseVersion) { 199 @SuppressWarnings("LocalVariableHidesMemberVariable") 200 SourceVersion release = this.release; 201 isVersioned = (release != baseVersion); 202 if (isVersioned) { 203 if (release == null) { 204 release = SourceVersion.latestSupported(); 205 // `this.release` intentionally left to null. 206 } 207 outputDirectory = outputDirectoryForReleases(outputDirectory, release); 208 } 209 } 210 211 /** 212 * Returns the directory where to write the compilation for a specific Java release. 213 * 214 * @param outputDirectory usually the value of {@link #outputDirectory} 215 * @param release the release, or {@code null} for the default release 216 */ 217 static Path outputDirectoryForReleases(Path outputDirectory, SourceVersion release) { 218 if (release == null) { 219 release = SourceVersion.latestSupported(); 220 } 221 String version = release.name(); // TODO: replace by runtimeVersion() in Java 18. 222 version = version.substring(version.lastIndexOf('_') + 1); 223 return outputDirectoryForReleases(outputDirectory).resolve(version); 224 } 225 226 /** 227 * Returns the directory where to write the compilation for a specific Java release. 228 * The caller shall add the version number to the returned path. 229 */ 230 static Path outputDirectoryForReleases(Path outputDirectory) { 231 // TODO: use Path.resolve(String, String...) with Java 22. 232 return outputDirectory.resolve("META-INF").resolve("versions"); 233 } 234 235 /** 236 * {@return the target version as an object from the Java tools API} 237 * 238 * @param root the source directory for which to get the target version 239 * @throws UnsupportedVersionException if the version string cannot be parsed 240 */ 241 static Optional<SourceVersion> targetVersion(final SourceRoot root) { 242 return root.targetVersion().map(Version::toString).map(SourceDirectory::parse); 243 } 244 245 /** 246 * Parses the given version string. 247 * This method parses the version with {@link Runtime.Version#parse(String)}. 248 * Therefore, for Java 8, the version shall be "8", not "1.8". 249 * 250 * @param version the version to parse, or null or empty if none 251 * @return the parsed version, or {@code null} if the given string was null or empty 252 * @throws UnsupportedVersionException if the version string cannot be parsed 253 */ 254 private static SourceVersion parse(final String version) { 255 if (version == null || version.isBlank()) { 256 return null; 257 } 258 try { 259 var parsed = Runtime.Version.parse(version); 260 return SourceVersion.valueOf("RELEASE_" + parsed.feature()); 261 // TODO: Replace by return SourceVersion.valueOf(v) after upgrade to Java 18. 262 } catch (IllegalArgumentException e) { 263 throw new UnsupportedVersionException("Illegal version number: \"" + version + '"', e); 264 } 265 } 266 267 /** 268 * Gets the list of source directories from the project manager. 269 * The returned list includes only the directories that exist. 270 * 271 * @param compileSourceRoots the root paths to source files 272 * @param defaultRelease the release to use if the {@code <source>} element provides none, or {@code null} 273 * @param outputDirectory the directory where to store the compilation results 274 * @return the given list of paths wrapped as source directory objects 275 */ 276 static List<SourceDirectory> fromProject( 277 Stream<SourceRoot> compileSourceRoots, String defaultRelease, Path outputDirectory) { 278 var release = parse(defaultRelease); // May be null. 279 var roots = new ArrayList<SourceDirectory>(); 280 compileSourceRoots.forEach((SourceRoot source) -> { 281 Path directory = source.directory(); 282 if (Files.exists(directory)) { 283 var fileKind = JavaFileObject.Kind.OTHER; 284 var outputFileKind = JavaFileObject.Kind.OTHER; 285 if (Language.JAVA_FAMILY.equals(source.language())) { 286 fileKind = JavaFileObject.Kind.SOURCE; 287 outputFileKind = JavaFileObject.Kind.CLASS; 288 } 289 roots.add(new SourceDirectory( 290 directory, 291 source.includes(), 292 source.excludes(), 293 fileKind, 294 source.module().orElse(null), 295 targetVersion(source).orElse(release), 296 outputDirectory, 297 outputFileKind)); 298 } 299 }); 300 roots.stream() 301 .map((dir) -> dir.release) 302 .filter(Objects::nonNull) 303 .min(SourceVersion::compareTo) 304 .ifPresent((baseVersion) -> roots.forEach((dir) -> dir.completeIfVersioned(baseVersion))); 305 return roots; 306 } 307 308 /** 309 * Converts the given list of paths to a list of source directories. 310 * The returned list includes only the directories that exist. 311 * Used only when the compiler plugin is configured with the {@code compileSourceRoots} option. 312 * 313 * @param compileSourceRoots the root paths to source files 314 * @param moduleName name of the module for which source directories are provided, or {@code null} if none 315 * @param defaultRelease the release to use, or {@code null} of unspecified 316 * @param outputDirectory the directory where to store the compilation results 317 * @return the given list of paths wrapped as source directory objects 318 */ 319 static List<SourceDirectory> fromPluginConfiguration( 320 List<String> compileSourceRoots, String moduleName, String defaultRelease, Path outputDirectory) { 321 var release = parse(defaultRelease); // May be null. 322 var roots = new ArrayList<SourceDirectory>(compileSourceRoots.size()); 323 for (String file : compileSourceRoots) { 324 Path directory = Path.of(file); 325 if (Files.exists(directory)) { 326 roots.add(new SourceDirectory( 327 directory, 328 List.of(), 329 List.of(), 330 JavaFileObject.Kind.SOURCE, 331 moduleName, 332 release, 333 outputDirectory, 334 JavaFileObject.Kind.CLASS)); 335 } 336 } 337 return roots; 338 } 339 340 /** 341 * Returns whether the given file is a {@code module-info.java} file. 342 * TODO: we could make this method non-static and verify that the given 343 * file is in the root of this directory. 344 */ 345 static boolean isModuleInfoSource(Path file) { 346 return (MODULE_INFO + JAVA_FILE_SUFFIX).equals(file.getFileName().toString()); 347 } 348 349 /** 350 * Invoked for each source files in this directory. 351 */ 352 void visit(Path sourceFile) { 353 if (isModuleInfoSource(sourceFile)) { 354 // Paranoiac check: only one file should exist, but if many, keep the one closest to the root. 355 if (moduleInfo == null || moduleInfo.getNameCount() >= sourceFile.getNameCount()) { 356 moduleInfo = sourceFile; 357 } 358 } 359 } 360 361 /** 362 * Path to the {@code module-info.java} source file, or empty if none. 363 * This information is accurate only after {@link PathFilter} finished 364 * to walk through all source files in a directory. 365 */ 366 public Optional<Path> getModuleInfo() { 367 return Optional.ofNullable(moduleInfo); 368 } 369 370 /** 371 * {@return the Java version of the sources in this directory if different than the base version} 372 * The value returned by this method is related to the {@code META-INF/versions/} subdirectory in 373 * the path returned by {@link #getOutputDirectory()}. If this method returns an empty value, then 374 * there is no such subdirectory (which doesn't mean that the user did not specified a Java version). 375 * If non-empty, the returned value is the value of <var>n</var> in {@code META-INF/versions/n}. 376 */ 377 public Optional<SourceVersion> getSpecificVersion() { 378 return Optional.ofNullable(isVersioned ? release : null); 379 } 380 381 /** 382 * {@return the directory where to store the compilation results} 383 * This is the <abbr>MOJO</abbr> output directory potentially completed with 384 * sub-directories for module name and {@code META-INF/versions} versioning. 385 */ 386 public Path getOutputDirectory() { 387 return outputDirectory; 388 } 389 390 /** 391 * Compares the given object with this source directory for equality. 392 * 393 * @param obj the object to compare 394 * @return whether the two objects have the same path, module name and release version 395 */ 396 @Override 397 public boolean equals(Object obj) { 398 if (obj instanceof SourceDirectory other) { 399 return root.equals(other.root) 400 && includes.equals(other.includes) 401 && excludes.equals(other.excludes) 402 && fileKind == other.fileKind 403 && Objects.equals(moduleName, other.moduleName) 404 && release == other.release 405 && outputDirectory.equals(other.outputDirectory) 406 && outputFileKind == other.outputFileKind; 407 } 408 return false; 409 } 410 411 /** 412 * {@return a hash code value for this root directory} 413 */ 414 @Override 415 public int hashCode() { 416 return Objects.hash(root, moduleName, release); 417 } 418 419 /** 420 * {@return a string representation of this root directory for debugging purposes} 421 */ 422 @Override 423 public String toString() { 424 var sb = new StringBuilder(100).append('"').append(root).append('"'); 425 if (moduleName != null) { 426 sb.append(" for module \"").append(moduleName).append('"'); 427 } 428 if (release != null) { 429 sb.append(" on Java release ").append(release); 430 } 431 return sb.toString(); 432 } 433 }