This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-compiler-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new 7c300a3 Refactoring of the handling of `--patch-module` (#1002)
7c300a3 is described below
commit 7c300a3509efdd9796e236d94bb9192dda567ebd
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Dec 25 19:20:36 2025 +0100
Refactoring of the handling of `--patch-module` (#1002)
Bug fixes in the handling of `--patch-module` option for tests and for
multi-release projects:
* Fix a compilation errors when a module exists only for a Java version
higher than the base version.
* Add the base version in the module path for avoiding recompilation of
classes of the base version.
* Determine the modules to patch by scanning the output directory instead
of the <source> elements.
* Change dependency collections from List to Deque for efficient
prepending/removal operations.
* Enable a test which was previously disabled because of toolchain.
* Remove the hack that consisted in temporarily delete `module-info.class`
when overwriting that file.
* Refactor `compile()` method for better readability, with sub-tasks moved
to helper methods.
* Add `DirectoryHierarchy` enumeration in replacement of boolean flags.
* Separate the handling of classpath project and modular project cases in
two inner classes.
* Refactor `WorkaroundForPatchModule` for making the workaround less
intrusive in main code.
---------
Co-authored-by: Gerd Aschemann <[email protected]>
Co-authored-by: Claude Opus 4.5 <[email protected]>
---
.../invoker.properties | 18 -
src/it/MCOMPILER-275_separate-moduleinfo/pom.xml | 7 +-
.../verify.groovy | 14 +-
src/it/multirelease-with-modules/verify.groovy | 6 +
.../plugin/compiler/AbstractCompilerMojo.java | 7 +-
.../plugin/compiler/CompilationTaskSources.java | 57 --
.../apache/maven/plugin/compiler/CompilerMojo.java | 5 +-
.../maven/plugin/compiler/DiagnosticLogger.java | 12 +-
.../maven/plugin/compiler/DirectoryHierarchy.java | 125 ++++
.../maven/plugin/compiler/ForkedToolSources.java | 23 +-
.../maven/plugin/compiler/IncrementalBuild.java | 7 +-
.../plugin/compiler/ModuleDirectoryRemover.java | 3 +
.../maven/plugin/compiler/SourceDirectory.java | 42 +-
.../maven/plugin/compiler/SourcesForRelease.java | 34 +-
.../maven/plugin/compiler/TestCompilerMojo.java | 51 +-
.../apache/maven/plugin/compiler/ToolExecutor.java | 652 +++++++++++++++------
.../maven/plugin/compiler/ToolExecutorForTest.java | 338 ++++++-----
.../plugin/compiler/WorkaroundForPatchModule.java | 442 +++++++++-----
18 files changed, 1159 insertions(+), 684 deletions(-)
diff --git a/src/it/MCOMPILER-275_separate-moduleinfo/invoker.properties
b/src/it/MCOMPILER-275_separate-moduleinfo/invoker.properties
deleted file mode 100644
index ddb00e6..0000000
--- a/src/it/MCOMPILER-275_separate-moduleinfo/invoker.properties
+++ /dev/null
@@ -1,18 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-invoker.toolchain.jdk.version=1.9
diff --git a/src/it/MCOMPILER-275_separate-moduleinfo/pom.xml
b/src/it/MCOMPILER-275_separate-moduleinfo/pom.xml
index 27e28c6..955b57e 100644
--- a/src/it/MCOMPILER-275_separate-moduleinfo/pom.xml
+++ b/src/it/MCOMPILER-275_separate-moduleinfo/pom.xml
@@ -49,9 +49,6 @@
<id>default-compile</id>
<!-- compile everything to ensure module-info contains right
entries -->
<configuration>
- <jdkToolchain>
- <version>1.9</version>
- </jdkToolchain>
<release>17</release>
</configuration>
</execution>
@@ -64,9 +61,7 @@
<excludes>
<exclude>module-info.java</exclude>
</excludes>
- <!-- ideally this would be 1.5, but with CI's it is hard to have
a proper toolchains.xml in place -->
- <source>8</source>
- <target>8</target>
+ <release>16</release>
</configuration>
</execution>
</executions>
diff --git a/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy
b/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy
index 8bab980..1e7e2bf 100644
--- a/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy
+++ b/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy
@@ -18,13 +18,7 @@
*/
def log = new File( basedir, 'build.log').text
-assert log.count( "[INFO] Toolchain in maven-compiler-plugin: JDK" ) == 1
-
-assert log.count( "[INFO] Recompiling the module because of changed source
code." ) == 1
-assert log.count( "[INFO] Recompiling the module because of added or removed
source files." ) == 1
-assert log.count( "[INFO] Recompiling the module because of changed
dependency." ) == 1
-
-// major_version: 52 = java8 -> execution id "base-compile"
-assert new File( basedir, 'target/classes/com/foo/MyClass.class' ).bytes[7] ==
52
-// major_version: 53 = java9 -> execution id "default-compile"
-assert new File( basedir, 'target/classes/module-info.class' ).bytes[7] == 53
+// major_version: 60 = java 16 -> execution id "base-compile"
+assert new File( basedir, 'target/classes/com/foo/MyClass.class' ).bytes[7] ==
60
+// major_version: 61 = java 17 -> execution id "default-compile"
+assert new File( basedir, 'target/classes/module-info.class' ).bytes[7] == 61
diff --git a/src/it/multirelease-with-modules/verify.groovy
b/src/it/multirelease-with-modules/verify.groovy
index 146fa96..ee31cf7 100644
--- a/src/it/multirelease-with-modules/verify.groovy
+++ b/src/it/multirelease-with-modules/verify.groovy
@@ -30,6 +30,12 @@ assert baseVersion == getMajor(new File( basedir,
"target/classes/foo.bar.more/m
assert nextVersion == getMajor(new File( basedir,
"target/classes/META-INF/versions-modular/16/foo.bar/foo/OtherFile.class"))
assert nextVersion == getMajor(new File( basedir,
"target/classes/META-INF/versions-modular/16/foo.bar.more/more/OtherFile.class"))
+// Verify that the classes inherited from the base version were not recompiled
a second time.
+assert new File( basedir,
"target/classes/META-INF/versions-modular/16/foo.bar/foo/MainFile.class").exists()
== false
+assert new File( basedir,
"target/classes/META-INF/versions-modular/16/foo.bar/foo/YetAnotherFile.class").exists()
== false
+assert new File( basedir,
"target/classes/META-INF/versions-modular/16/foo.bar.more/more/MainFile.class").exists()
== false
+
+
int getMajor(File file)
{
assert file.exists()
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
index d52aff1..c00d35a 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
@@ -1523,7 +1523,10 @@ public abstract class AbstractCompilerMojo implements
Mojo {
*
* @param source the source file to parse (may be null or not exist)
* @return the module name, or {@code null} if not found
+ *
+ * @deprecated This is invoked only by other deprecated methods.
*/
+ @Deprecated(since = "4.0.0")
final String parseModuleInfoName(Path source) throws IOException {
if (source != null && Files.exists(source)) {
Charset charset = charset();
@@ -1619,7 +1622,7 @@ public abstract class AbstractCompilerMojo implements
Mojo {
*/
@Deprecated(since = "4.0.0")
@SuppressWarnings("UseSpecificCatch")
- final void resolveProcessorPathEntries(Map<PathType, List<Path>> addTo)
throws MojoException {
+ final void resolveProcessorPathEntries(Map<PathType, Collection<Path>>
addTo) throws MojoException {
List<DependencyCoordinate> dependencies = annotationProcessorPaths;
if (!isAbsent(dependencies)) {
try {
@@ -1826,7 +1829,7 @@ public abstract class AbstractCompilerMojo implements
Mojo {
try (BufferedWriter out = Files.newBufferedWriter(pathForRelease))
{
configuration.setRelease(sources.getReleaseString());
configuration.format((i == indexToShow) ? commandLine : null,
out);
- for (Map.Entry<PathType, List<Path>> entry :
sources.dependencySnapshot.entrySet()) {
+ for (Map.Entry<PathType, Collection<Path>> entry :
sources.dependencySnapshot.entrySet()) {
writeOption(out, entry.getKey(), entry.getValue());
}
for (Map.Entry<String, Set<Path>> root :
sources.roots.entrySet()) {
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/CompilationTaskSources.java
b/src/main/java/org/apache/maven/plugin/compiler/CompilationTaskSources.java
deleted file mode 100644
index 69146a7..0000000
--- a/src/main/java/org/apache/maven/plugin/compiler/CompilationTaskSources.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.plugin.compiler;
-
-import javax.tools.JavaCompiler;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.List;
-
-/**
- * Source files to compile in a single compilation task.
- * This is simply a list of files, together with an optional task to execute
before and after compilation.
- */
-class CompilationTaskSources {
- /**
- * All source files to compile;
- */
- final List<Path> files;
-
- /**
- * Creates a new compilation task.
- *
- * @param files the files to compile.
- */
- CompilationTaskSources(List<Path> files) {
- this.files = files;
- }
-
- /**
- * Executes the compilation task. Subclasses can override this method is
they need to perform
- * pre-compilation or post-compilation tasks.
- *
- * @param task the compilation task
- * @return whether the compilation was successful.
- * @throws IOException if an initialization or cleaner task was required
and failed.
- */
- boolean compile(JavaCompiler.CompilationTask task) throws IOException {
- return task.call();
- }
-}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
index 373f073..425f16d 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
@@ -267,7 +267,8 @@ public class CompilerMojo extends AbstractCompilerMojo {
@Override
protected Path getOutputDirectory() {
if (SUPPORT_LEGACY && multiReleaseOutput && release != null) {
- return SourceDirectory.outputDirectoryForReleases(false,
outputDirectory)
+ return DirectoryHierarchy.PACKAGE
+ .outputDirectoryForReleases(outputDirectory)
.resolve(release);
}
return outputDirectory;
@@ -341,7 +342,7 @@ public class CompilerMojo extends AbstractCompilerMojo {
*/
@Deprecated(since = "4.0.0")
private TreeMap<SourceVersion, Path> getOutputDirectoryPerVersion() throws
IOException {
- final Path root = SourceDirectory.outputDirectoryForReleases(false,
outputDirectory);
+ final Path root =
DirectoryHierarchy.PACKAGE.outputDirectoryForReleases(outputDirectory);
if (Files.notExists(root)) {
return null;
}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java
b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java
index 6c1b92f..9bce7a5 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java
@@ -97,12 +97,14 @@ final class DiagnosticLogger implements
DiagnosticListener<JavaFileObject> {
* @return the given path, potentially relative to the base directory
*/
private String relativize(String file) {
- try {
- return directory.relativize(Path.of(file)).toString();
- } catch (IllegalArgumentException e) {
- // Ignore, keep the absolute path.
- return file;
+ if (directory != null) {
+ try {
+ return directory.relativize(Path.of(file)).toString();
+ } catch (IllegalArgumentException e) {
+ // Ignore, keep the absolute path.
+ }
}
+ return file;
}
/**
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/DirectoryHierarchy.java
b/src/main/java/org/apache/maven/plugin/compiler/DirectoryHierarchy.java
new file mode 100644
index 0000000..4175714
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugin/compiler/DirectoryHierarchy.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugin.compiler;
+
+import javax.lang.model.SourceVersion;
+
+import java.nio.file.Path;
+import java.util.Locale;
+
+/**
+ * The way that source files are organized in a file system directory
hierarchy.
+ * <a
href="https://docs.oracle.com/en/java/javase/25/docs/specs/man/javac.html#directory-hierarchies">Directory
+ * hierarchies</a> are <i>package hierarchy</i>, <i>module hierarchy</i> and
<i>module source hierarchy</i>, but
+ * for the purpose of the Maven Compiler Plugin we do not distinguish between
the two latter.
+ *
+ * @see <a
href="https://docs.oracle.com/en/java/javase/25/docs/specs/man/javac.html#directory-hierarchies">Directory
hierarchies</a>
+ */
+public enum DirectoryHierarchy {
+ /**
+ * Project using package hierarchy. This is the hierarchy used by all Java
projects before Java 9.
+ * Note that it does not necessarily implies a class-path project. A
modular project can still use
+ * the package hierarchy if the project contains only one module.
+ */
+ PACKAGE("versions"),
+
+ /**
+ * Project using package hierarchy, but in which a {@code module-info}
file has been detected.
+ * This is used for compilation of tests. For the main code, we pretend
that the hierarchy is
+ * {@link #MODULE_SOURCE} and move the directory output after compilation.
Therefore, this
+ * enumeration value can be understood as "pseudo module source hierarchy".
+ *
+ * @see ModuleDirectoryRemover
+ *
+ * @deprecated Used only for compatibility with Maven 3.
+ */
+ @Deprecated
+ PACKAGE_WITH_MODULE("versions"),
+
+ /**
+ * A multi-module project using module source hierarchy. It could also be
a module hierarchy,
+ * as the Maven Compiler Plugin does not need to distinguish <i>module
hierarchy</i> and
+ * <i>module source hierarchy</i>.
+ */
+ MODULE_SOURCE("versions-modular");
+
+ /**
+ * The {@value} directory.
+ */
+ static final String META_INF = "META-INF";
+
+ /**
+ * Name of the {@code META-INF/} sub-directory where multi-release outputs
are stored.
+ */
+ private final String versionDirectory;
+
+ /**
+ * Creates a new enumeration value.
+ *
+ * @param versionDirectory name of the {@code META-INF/} sub-directory
where multi-release outputs are stored
+ */
+ DirectoryHierarchy(String versionDirectory) {
+ this.versionDirectory = versionDirectory;
+ }
+
+ /**
+ * Returns the directory where to write the compiled class files for all
Java releases.
+ * The standard path for {@link #PACKAGE} hierarchy is {@code
META-INF/versions}.
+ * The caller shall add the version number to the returned path.
+ *
+ * @param outputDirectory usually the value of {@link
SourceDirectory#outputDirectory}
+ * @return the directory for all versions
+ */
+ public Path outputDirectoryForReleases(Path outputDirectory) {
+ // TODO: use Path.resolve(String, String...) with Java 22.
+ return outputDirectory.resolve(META_INF).resolve(versionDirectory);
+ }
+
+ /**
+ * Returns the directory where to write the compiled class files for a
specific Java release.
+ * The standard path is {@code META-INF/versions/${release}} where {@code
${release}} is the
+ * numerical value of the {@code release} argument. However for {@link
#MODULE_SOURCE} case,
+ * the returned path is rather {@code
META-INF/versions-modular/${release}}.
+ * The latter is non-standard because there is no standard multi-module
<abbr>JAR</abbr> formats as of 2025.
+ * The use of {@code "versions-modular"} is for allowing other plugins
such as Maven <abbr>JAR</abbr> plugin
+ * to avoid confusion with the standard case.
+ *
+ * @param outputDirectory usually the value of {@link
SourceDirectory#outputDirectory}
+ * @param release the release, or {@code null} for the default release
+ * @return the directory for the classes of the specified version
+ */
+ public Path outputDirectoryForReleases(Path outputDirectory, SourceVersion
release) {
+ if (release == null) {
+ release = SourceVersion.latestSupported();
+ }
+ String version = release.name(); // TODO: replace by runtimeVersion()
in Java 18.
+ version = version.substring(version.lastIndexOf('_') + 1);
+ return outputDirectoryForReleases(outputDirectory).resolve(version);
+ }
+
+ /**
+ * Returns a string representation for use in error message.
+ *
+ * @return human-readable string representation
+ */
+ @Override
+ public String toString() {
+ return name().replace('_', ' ').toLowerCase(Locale.US);
+ }
+}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java
b/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java
index fac6b5d..b274252 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java
@@ -64,6 +64,16 @@ final class ForkedToolSources implements
StandardJavaFileManager {
* {@link JavaPathType} because they are not about dependencies.
*/
private record OtherPathType(String name, String optionString, String
moduleName) implements PathType {
+ /**
+ * An option for the directory of source files of a module.
+ *
+ * @param moduleName name of the module
+ * @return option for the directory of source files of the specified
module
+ */
+ static OtherPathType moduleSources(String moduleName) {
+ return new OtherPathType("MODULE_SOURCE_PATH",
"--module-source-path", moduleName);
+ }
+
/**
* The option for the directory of source files.
*/
@@ -417,7 +427,7 @@ final class ForkedToolSources implements
StandardJavaFileManager {
throw new IllegalArgumentException("Unsupported location: " +
location);
}
}
- if (paths == null || paths.isEmpty()) {
+ if (isAbsent(paths)) {
locations.remove(type);
} else {
locations.put(type, paths);
@@ -435,17 +445,24 @@ final class ForkedToolSources implements
StandardJavaFileManager {
if (location == StandardLocation.PATCH_MODULE_PATH) {
type = JavaPathType.patchModule(moduleName);
} else if (location == StandardLocation.MODULE_SOURCE_PATH) {
- type = new OtherPathType("MODULE_SOURCE_PATH",
"--module-source-path", moduleName);
+ type = OtherPathType.moduleSources(moduleName);
} else {
throw new IllegalArgumentException("Unsupported location: " +
location);
}
- if (paths == null || paths.isEmpty()) {
+ if (isAbsent(paths)) {
locations.remove(type);
} else {
locations.put(type, paths);
}
}
+ /**
+ * Returns whether the given collection is null or empty.
+ */
+ private static boolean isAbsent(Collection<?> c) {
+ return (c == null) || c.isEmpty();
+ }
+
/**
* Returns the search path associated with the given location, or {@code
null} if none.
*/
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java
b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java
index a1f60a4..f2e1624 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java
@@ -566,7 +566,7 @@ final class IncrementalBuild {
}
boolean rebuild = false;
boolean allChanged = true;
- List<Path> added = new ArrayList<>();
+ final var added = new ArrayList<Path>();
for (SourceFile source : sourceFiles) {
SourceInfo previous = previousBuild.remove(source.file);
if (previous != null) {
@@ -646,13 +646,14 @@ final class IncrementalBuild {
*
* @see Aspect#DEPENDENCIES
*/
- String dependencyChanges(Iterable<List<Path>> dependencies,
Collection<String> fileExtensions) throws IOException {
+ String dependencyChanges(Iterable<Collection<Path>> dependencies,
Collection<String> fileExtensions)
+ throws IOException {
if (!cacheLoaded) {
loadCache();
}
final FileTime changeTime = FileTime.fromMillis(previousBuildTime);
final var updated = new ArrayList<Path>();
- for (List<Path> roots : dependencies) {
+ for (Collection<Path> roots : dependencies) {
for (Path root : roots) {
try (Stream<Path> files = Files.walk(root)) {
files.filter((f) -> {
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java
b/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java
index 878b603..e653cce 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java
@@ -31,6 +31,9 @@ import java.nio.file.Path;
*
* <p>The code in this class is useful only when {@link
AbstractCompilerMojo#SUPPORT_LEGACY} is true.
* This class can be fully deleted if a future version permanently set
above-cited flag to false.</p>
+ *
+ * @see CompilerMojo#directoryLevelToRemove
+ * @see ToolExecutorForTest#directoryLevelToRemove
*/
final class ModuleDirectoryRemover implements Closeable {
/**
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
index bb4ee92..676c935 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
@@ -206,47 +206,11 @@ final class SourceDirectory {
release = SourceVersion.latestSupported();
// `this.release` intentionally left to null.
}
- outputDirectory = outputDirectoryForReleases(moduleName != null,
outputDirectory, release);
+ var hierarchy = (moduleName != null) ?
DirectoryHierarchy.MODULE_SOURCE : DirectoryHierarchy.PACKAGE;
+ outputDirectory =
hierarchy.outputDirectoryForReleases(outputDirectory, release);
}
}
- /**
- * Returns the directory where to write the compiled class files for a
specific Java release.
- * The standard path is {@code META-INF/versions/${release}} where {@code
${release}} is the
- * numerical value of the {@code release} argument. However if {@code
modular} is {@code true},
- * then the returned path is rather {@code
META-INF/versions-modular/${release}}. The latter is
- * non-standard because there is no standard multi-module <abbr>JAR</abbr>
formats as of 2025.
- * The use of {@code "versions-modular"} is for allowing other plugins
such as Maven JAR plugin
- * to avoid confusion with the standard case.
- *
- * @param modular whether each version directory contains module names
- * @param outputDirectory usually the value of {@link #outputDirectory}
- * @param release the release, or {@code null} for the default release
- * @return the directory for the classes of the specified version
- */
- static Path outputDirectoryForReleases(boolean modular, Path
outputDirectory, SourceVersion release) {
- if (release == null) {
- release = SourceVersion.latestSupported();
- }
- String version = release.name(); // TODO: replace by runtimeVersion()
in Java 18.
- version = version.substring(version.lastIndexOf('_') + 1);
- return outputDirectoryForReleases(modular,
outputDirectory).resolve(version);
- }
-
- /**
- * Returns the directory where to write the compiled class files for all
Java releases.
- * The standard path (when {@code modular} is {@code false}) is {@code
META-INF/versions}.
- * The caller shall add the version number to the returned path.
- *
- * @param modular whether each version directory contains module names
- * @param outputDirectory usually the value of {@link #outputDirectory}
- * @return the directory for all versions
- */
- static Path outputDirectoryForReleases(boolean modular, Path
outputDirectory) {
- // TODO: use Path.resolve(String, String...) with Java 22.
- return outputDirectory.resolve("META-INF").resolve(modular ?
"versions-modular" : "versions");
- }
-
/**
* {@return the target version as an object from the Java tools API}
*
@@ -266,7 +230,7 @@ final class SourceDirectory {
* @return the parsed version, or {@code null} if the given string was
null or empty
* @throws UnsupportedVersionException if the version string cannot be
parsed
*/
- private static SourceVersion parse(final String version) {
+ static SourceVersion parse(final String version) {
if (version == null || version.isBlank()) {
return null;
}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java
b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java
index 8c11390..5180f7f 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java
@@ -22,9 +22,9 @@ import javax.lang.model.SourceVersion;
import java.io.Closeable;
import java.io.IOException;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -80,7 +80,7 @@ final class SourcesForRelease implements Closeable {
* Snapshot of {@link ToolExecutor#dependencies}.
* This information is saved in case a {@code target/javac.args} debug
file needs to be written.
*/
- Map<PathType, List<Path>> dependencySnapshot;
+ Map<PathType, Collection<Path>> dependencySnapshot;
/**
* The output directory for the release. This is either the base output
directory or a sub-directory
@@ -134,36 +134,6 @@ final class SourcesForRelease implements Closeable {
files.add(source.file);
}
- /**
- * If there is any {@code module-info.class} in the main classes that are
overwritten by this set of sources,
- * temporarily replace the main files by the test files. The {@link
#close()} method must be invoked after
- * this method for resetting the original state.
- *
- * <p>This method is invoked when the test files overwrite the {@code
module-info.class} from the main files.
- * This method should not be invoked during the compilation of main
classes, as its behavior may be not well
- * defined.</p>
- */
- void substituteModuleInfos(final Path mainOutputDirectory, final Path
testOutputDirectory) throws IOException {
- for (Map.Entry<SourceDirectory, ModuleInfoOverwrite> entry :
moduleInfos.entrySet()) {
- Path main = mainOutputDirectory;
- Path test = testOutputDirectory;
- SourceDirectory directory = entry.getKey();
- String moduleName = directory.moduleName;
- if (moduleName != null) {
- main = main.resolve(moduleName);
- if (!Files.isDirectory(main)) {
- main = mainOutputDirectory;
- }
- test = test.resolve(moduleName);
- if (!Files.isDirectory(test)) {
- test = testOutputDirectory;
- }
- }
- Path source = directory.getModuleInfo().orElseThrow(); // Should
never be absent for entries in the map.
- entry.setValue(ModuleInfoOverwrite.create(source, main, test));
- }
- }
-
/**
* Restores the hidden {@code module-info.class} files to their original
names.
*/
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
index 1fa3649..1cdc336 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java
@@ -212,13 +212,6 @@ public class TestCompilerMojo extends AbstractCompilerMojo
{
*/
private transient boolean hasMainModuleInfo;
- /**
- * Path to the {@code module-info.class} file of the main code, or {@code
null} if that file does not exist.
- * This field exists only for transferring this information to {@link
ToolExecutorForTest#mainModulePath},
- * and should be {@code null} the rest of the time.
- */
- transient Path mainModulePath;
-
/**
* The file where to dump the command-line when debug is activated or when
the compilation failed.
* For example, if the value is {@code "javac-test"}, then the Java
compiler can be launched
@@ -368,19 +361,22 @@ public class TestCompilerMojo extends
AbstractCompilerMojo {
}
/**
- * {@return the module name declared in the test sources}
+ * {@return the module name found in the package hierarchy of given
sources}
* We have to parse the source instead of the {@code module-info.class}
file
- * because the classes may not have been compiled yet.
- * This is not very reliable, but putting a {@code module-info.java} file
in the tests is deprecated anyway.
+ * because the classes may not have been compiled yet. This is not
reliable,
+ * but the use of package hierarchy for modular project should be avoided
in
+ * Maven 4.
+ *
+ * @deprecated Declare modules in {@code <source>} elements instead.
*/
- final String getTestModuleName(List<SourceDirectory> compileSourceRoots)
throws IOException {
+ @Deprecated(since = "4.0.0")
+ final String moduleNameFromPackageHierarchy(List<SourceDirectory>
compileSourceRoots) throws IOException {
for (SourceDirectory directory : compileSourceRoots) {
- if (directory.moduleName != null) {
- return directory.moduleName;
- }
- String name =
parseModuleInfoName(directory.getModuleInfo().orElse(null));
- if (name != null) {
- return name;
+ if (directory.moduleName == null) {
+ String name =
parseModuleInfoName(directory.getModuleInfo().orElse(null));
+ if (name != null) {
+ return name;
+ }
}
}
return null;
@@ -388,8 +384,11 @@ public class TestCompilerMojo extends AbstractCompilerMojo
{
/**
* {@return whether the project has at least one {@code module-info.class}
file}
+ * The {@code module-info.class} should be located in the main source code.
+ * However, this method checks also in the test source code for
compatibility with Maven 3,
+ * but this practice is deprecated.
*
- * @param roots root directories of the sources to compile
+ * @param roots root directories of the source files of the test classes
to compile
* @throws IOException if this method needed to read a module descriptor
and failed
*/
@Override
@@ -426,18 +425,12 @@ public class TestCompilerMojo extends
AbstractCompilerMojo {
*/
@Override
public ToolExecutor createExecutor(DiagnosticListener<? super
JavaFileObject> listener) throws IOException {
- try {
- Path file = mainOutputDirectory.resolve(MODULE_INFO +
CLASS_FILE_SUFFIX);
- if (Files.isRegularFile(file)) {
- mainModulePath = file;
- hasMainModuleInfo = true;
- }
- return new ToolExecutorForTest(this, listener);
- } finally {
- // Reset the fields that were used only for transfering
information to `ToolExecutorForTest`.
- hasTestModuleInfo = false;
- hasMainModuleInfo = false;
+ Path mainModulePath = mainOutputDirectory.resolve(MODULE_INFO +
CLASS_FILE_SUFFIX);
+ if (Files.isRegularFile(mainModulePath)) {
+ hasMainModuleInfo = true;
+ } else {
mainModulePath = null;
}
+ return new ToolExecutorForTest(this, listener, mainModulePath);
}
}
diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
index 1a9b1f2..907d323 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
@@ -33,10 +33,13 @@ import java.nio.charset.Charset;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Deque;
import java.util.EnumMap;
import java.util.EnumSet;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -116,6 +119,16 @@ public class ToolExecutor {
*/
protected final boolean hasModuleDeclaration;
+ /**
+ * How the source code of the project is organized, or {@code null} if not
yet determined.
+ * <a
href="https://docs.oracle.com/en/java/javase/25/docs/specs/man/javac.html#directory-hierarchies">Directory
+ * hierarchies</a> are <i>package hierarchy</i>, <i>module hierarchy</i>
and <i>module source hierarchy</i>, but
+ * for the purpose of the compiler plugin we do not distinguish between
the two latter.
+ *
+ * @see #determineDirectoryHierarchy(Collection)
+ */
+ private DirectoryHierarchy directoryHierarchy;
+
/**
* The result of resolving the dependencies, or {@code null} if not
available or not needed.
* For example, this field may be null if the constructor found no file to
compile,
@@ -137,7 +150,7 @@ public class ToolExecutor {
* @see #dependencies(PathType)
* @see #prependDependency(PathType, Path)
*/
- protected final Map<PathType, List<Path>> dependencies;
+ private final Map<PathType, Collection<Path>> dependencies;
/**
* The destination directory (or class output directory) for class files.
@@ -263,11 +276,11 @@ public class ToolExecutor {
dependencyResolution = mojo.resolveDependencies(hasModuleDeclaration);
if (dependencyResolution != null) {
dependencies.putAll(dependencyResolution.getDispatchedPaths());
- copyDependencyValues();
}
mojo.resolveProcessorPathEntries(dependencies);
mojo.amendincrementalCompilation(incrementalBuildConfig,
dependencies.keySet());
generatedSourceDirectories =
mojo.addGeneratedSourceDirectory(dependencies.keySet());
+ copyDependencyValues();
}
/**
@@ -278,6 +291,50 @@ public class ToolExecutor {
dependencies.entrySet().forEach((entry) ->
entry.setValue(List.copyOf(entry.getValue())));
}
+ /**
+ * Returns the output directory of the main classes if they were compiled
in a previous Maven phase.
+ * This method shall always return {@code null} when compiling to main
code. The return value can be
+ * non-null only when compiling the test classes, in which case the
returned path is the directory to
+ * prepend to the class-path or module-path before to compile the classes
managed by this executor.
+ *
+ * @return the directory to prepend to the class-path or module-path, or
{@code null} if none
+ */
+ Path getOutputDirectoryOfPreviousPhase() {
+ return null;
+ }
+
+ /**
+ * Returns the directory of the classes compiled for the specified module.
+ * If the project is multi-release, this method returns the directory for
the base version.
+ *
+ * <p>This is normally a sub-directory of the same name as the module name.
+ * However, when building tests for a project which is both multi-release
and multi-module,
+ * the directory may exist only for a target Java version higher than the
base version.</p>
+ *
+ * @param outputDirectory the output directory which is the root of modules
+ * @param moduleName the name of the module for which the class directory
is desired
+ * @return directories of classes for the given module
+ */
+ Path resolveModuleOutputDirectory(Path outputDirectory, String moduleName)
{
+ return outputDirectory.resolve(moduleName);
+ }
+
+ /**
+ * Name of the module when using package hierarchy, or {@code null} if not
applicable.
+ * This is used for setting {@code --patch-module} option during
compilation of tests.
+ * This field is null in a class-path project or in a multi-module project.
+ *
+ * <p>This information is used for compatibility with the Maven 3 way to
build a modular project.
+ * It is recommended to use the {@code <sources>} element instead. We may
remove this method in a
+ * future version if we abandon compatibility with the Maven 3 way to
build modular projects.</p>
+ *
+ * @deprecated Declare modules in {@code <source>} elements instead.
+ */
+ @Deprecated(since = "4.0.0")
+ String moduleNameFromPackageHierarchy() {
+ return null;
+ }
+
/**
* {@return whether a release version is specified for all sources}
*/
@@ -361,20 +418,33 @@ public class ToolExecutor {
}
/**
- * {@return a modifiable list of paths to all dependencies of the given
type}
- * The returned list is intentionally live: elements can be added or
removed
- * from the list for changing the state of this executor.
+ * Writes the incremental build cache into the {@code
target/maven-status/maven-compiler-plugin/} directory.
+ * This method should be invoked only once. Next invocations after the
first one have no effect.
+ *
+ * @throws IOException if an error occurred while writing the cache
+ */
+ private void saveIncrementalBuild() throws IOException {
+ if (incrementalBuild != null) {
+ incrementalBuild.writeCache();
+ incrementalBuild = null;
+ }
+ }
+
+ /**
+ * {@return a modifiable collection of paths to all dependencies of the
given type}
+ * The returned collection is intentionally live: elements can be added or
removed
+ * from the collection for changing the state of this executor.
*
* @param pathType type of path for which to get the dependencies
*/
- protected List<Path> dependencies(PathType pathType) {
- return dependencies.compute(pathType, (key, paths) -> {
+ protected Deque<Path> dependencies(PathType pathType) {
+ return (Deque<Path>) dependencies.compute(pathType, (key, paths) -> {
if (paths == null) {
- return new ArrayList<>();
- } else if (paths instanceof ArrayList<?>) {
- return paths;
+ return new ArrayDeque<>();
+ } else if (paths instanceof ArrayDeque<Path> deque) {
+ return deque;
} else {
- var copy = new ArrayList<Path>(paths.size() + 4); //
Anticipate the addition of new elements.
+ var copy = new ArrayDeque<Path>(paths.size() + 4); //
Anticipate the addition of new elements.
copy.addAll(paths);
return copy;
}
@@ -389,8 +459,8 @@ public class ToolExecutor {
*/
private void setDependencyPaths(final StandardJavaFileManager fileManager)
throws IOException {
final var unresolvedPaths = new ArrayList<Path>();
- for (Map.Entry<PathType, List<Path>> entry : dependencies.entrySet()) {
- List<Path> paths = entry.getValue();
+ for (Map.Entry<PathType, Collection<Path>> entry :
dependencies.entrySet()) {
+ Collection<Path> paths = entry.getValue();
PathType key = entry.getKey();
if (key instanceof JavaPathType type) {
/*
@@ -411,7 +481,7 @@ public class ToolExecutor {
* specify the class output directory as one of
the locations on the user class path,
* using the --class-path option or one of its
alternate forms."
*/
- paths = new ArrayList<>(paths);
+ paths = new ArrayDeque<>(paths);
paths.add(outputDirectory);
entry.setValue(paths);
}
@@ -453,9 +523,9 @@ public class ToolExecutor {
* @param first the path to put first
* @return the new paths for the given type, as a modifiable list
*/
- protected List<Path> prependDependency(final PathType pathType, final Path
first) {
- List<Path> paths = dependencies(pathType);
- paths.add(0, first);
+ protected Deque<Path> prependDependency(final PathType pathType, final
Path first) {
+ Deque<Path> paths = dependencies(pathType);
+ paths.addFirst(first);
return paths;
}
@@ -466,19 +536,6 @@ public class ToolExecutor {
return (release != null) ? release : SourceVersion.latest();
}
- /**
- * If the given module name is empty, tries to infer a default module
name. A module name is inferred
- * (tentatively) when the <abbr>POM</abbr> file does not contain an
explicit {@code <module>} element.
- * This method exists only for compatibility with the Maven 3 way to do a
modular project.
- *
- * @param moduleName the module name, or an empty string if not explicitly
specified
- * @return the specified module name, or an inferred module name if
available, or an empty string
- * @throws IOException if the module descriptor cannot be read.
- */
- String inferModuleNameIfMissing(String moduleName) throws IOException {
- return moduleName;
- }
-
/**
* Groups all sources files first by Java release versions, then by module
names.
* The elements are sorted in the order of {@link SourceVersion}
enumeration values,
@@ -511,44 +568,18 @@ public class ToolExecutor {
}
/**
- * Creates the file manager which will be used by the compiler.
- * This method does not configure the locations (sources, dependencies,
<i>etc.</i>).
- * Locations will be set by {@link #compile(JavaCompiler, Options,
Writer)} on the
- * file manager returned by this method.
+ * Checks if there are no sources to compile and handles that case.
+ * When there are no sources, this method cleans up the output directory
and logs a message.
*
- * @param compiler the compiler
- * @param workaround whether to apply {@link WorkaroundForPatchModule}
- * @return the file manager to use
+ * @return {@code true} if there are no sources to compile, {@code false}
if there are sources
+ * @throws IOException if an error occurred while deleting the empty
output directory
*/
- private StandardJavaFileManager createFileManager(JavaCompiler compiler,
boolean workaround) {
- StandardJavaFileManager fileManager =
compiler.getStandardFileManager(listener, LOCALE, encoding);
- if (WorkaroundForPatchModule.ENABLED && workaround && !(compiler
instanceof ForkedTool)) {
- fileManager = new WorkaroundForPatchModule(fileManager);
- }
- return fileManager;
- }
-
- /**
- * Runs the compilation task.
- *
- * @param compiler the compiler
- * @param configuration the options to give to the Java compiler
- * @param otherOutput where to write additional output from the compiler
- * @return whether the compilation succeeded
- * @throws IOException if an error occurred while reading or writing a file
- * @throws MojoException if the compilation failed for a reason identified
by this method
- * @throws RuntimeException if any other kind of error occurred
- */
- @SuppressWarnings("checkstyle:MethodLength")
- public boolean compile(final JavaCompiler compiler, final Options
configuration, final Writer otherOutput)
- throws IOException {
- /*
- * Announce what the compiler is about to do.
- */
+ private boolean noSourcesToCompile() throws IOException {
sourcesForDebugFile.clear();
if (sourceFiles.isEmpty()) {
String message = "No sources to compile.";
try {
+ // The directory must exist since it was created in the
constructor.
Files.delete(outputDirectory);
} catch (DirectoryNotEmptyException e) {
message += " However, the output directory is not empty.";
@@ -565,154 +596,393 @@ public class ToolExecutor {
}
logger.debug(sb);
}
- /*
- * Create a `JavaFileManager`, configure all paths (dependencies and
sources), then run the compiler.
- * The Java file manager has a cache, so it needs to be disposed after
the compilation is completed.
- * The same `JavaFileManager` may be reused for many compilation units
(e.g. multi-release) before
- * disposal in order to reuse its cache.
- */
- boolean success = true;
- try (StandardJavaFileManager fileManager = createFileManager(compiler,
hasModuleDeclaration)) {
- setDependencyPaths(fileManager);
- if (!generatedSourceDirectories.isEmpty()) {
-
fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT,
generatedSourceDirectories);
- }
- boolean isVersioned = false;
- Path latestOutputDirectory = null;
- /*
- * More than one compilation unit may exist in the case of a
multi-release project.
- * Units are compiled in the order of the release version, with
base compiled first.
- * At the beginning of each new iteration, `latestOutputDirectory`
is the path to
- * the compiled classes of the previous version.
- */
- compile:
- for (final SourcesForRelease unit : groupByReleaseAndModule()) {
- Path outputForRelease = null;
- boolean isClasspathProject = false;
- boolean isModularProject = false;
- String defaultModuleName = null;
- configuration.setRelease(unit.getReleaseString());
- for (final Map.Entry<String, Set<Path>> root :
unit.roots.entrySet()) {
- final String declaredModuleName = root.getKey();
- final String moduleName =
inferModuleNameIfMissing(declaredModuleName);
- if (moduleName.isEmpty()) {
- isClasspathProject = true;
+ return false;
+ }
+
+ /**
+ * Determines the directory hierarchy by scanning all compilation units.
+ * Also validates that there are no conflicting directory hierarchies
+ * and performs the necessary remapping for Maven 3 compatibility.
+ * This should be called once before processing any units.
+ *
+ * @param units all compilation units to scan
+ * @throws CompilationFailureException if both explicit and detected
module names are present
+ */
+ private void determineDirectoryHierarchy(final
Collection<SourcesForRelease> units) {
+ final String moduleNameFromPackageHierarchy =
moduleNameFromPackageHierarchy();
+ for (SourcesForRelease unit : units) {
+ for (String moduleName : unit.roots.keySet()) {
+ DirectoryHierarchy detected;
+ if (moduleName.isEmpty()) {
+ if (moduleNameFromPackageHierarchy == null) {
+ detected = DirectoryHierarchy.PACKAGE;
} else {
- isModularProject = true;
- if (declaredModuleName.isEmpty()) { // Modular project
using package source hierarchy.
- defaultModuleName = moduleName;
- }
- }
- if (isClasspathProject & isModularProject) {
- throw new CompilationFailureException("Mix of modular
and non-modular sources.");
+ detected = DirectoryHierarchy.PACKAGE_WITH_MODULE;
}
- final Set<Path> sourcePaths = root.getValue();
- if (isClasspathProject) {
-
fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourcePaths);
+ } else {
+ if (moduleNameFromPackageHierarchy == null) {
+ detected = DirectoryHierarchy.MODULE_SOURCE;
} else {
-
fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH,
moduleName, sourcePaths);
- }
- outputForRelease = outputDirectory; // Modified below if
compiling a non-base release.
- if (isVersioned) {
- outputForRelease =
Files.createDirectories(SourceDirectory.outputDirectoryForReleases(
- isModularProject, outputForRelease,
unit.release));
- if (isClasspathProject) {
- /*
- * For a non-modular project, this block is
executed at most once par compilation unit.
- * Add the paths to the classes compiled for
previous versions.
- */
- List<Path> classpath =
prependDependency(JavaPathType.CLASSES, latestOutputDirectory);
-
fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
- } else {
- /*
- * For a modular project, this block can be
executed an arbitrary number of times
- * (once per module).
- */
- Path latestOutputForModule =
latestOutputDirectory.resolve(moduleName);
- JavaPathType.Modular pathType =
JavaPathType.patchModule(moduleName);
- List<Path> paths = prependDependency(pathType,
latestOutputForModule);
-
fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH,
moduleName, paths);
- }
+ // Mix of package hierarchy and module source
hierarchy.
+ throw new CompilationFailureException(
+ "The \"%s\" module must be declared in a
<module> element of <sources>."
+
.formatted(moduleNameFromPackageHierarchy));
}
}
- /*
- * At this point, we finished to set the source paths. We have
also modified the class-path or
- * patched the modules with the output directories of codes
compiled for lower Java releases.
- * The `defaultModuleName` is an adjustment done when the
project is a Java module, but still
- * organized in a package source hierarchy instead of a module
source hierarchy. Updating the
- * `unit.roots` map is not needed for this class, but done in
case a `target/javac.args` file
- * will be written after the compilation.
- */
- if (defaultModuleName != null) {
- Set<Path> paths = unit.roots.remove("");
- if (paths != null) {
- unit.roots.put(defaultModuleName, paths);
- }
+ if (directoryHierarchy == null) {
+ directoryHierarchy = detected;
+ } else if (directoryHierarchy != detected) {
+ throw new CompilationFailureException(
+ "Mix of %s and %s
hierarchies.".formatted(directoryHierarchy, detected));
}
- copyDependencyValues();
- unit.dependencySnapshot = new LinkedHashMap<>(dependencies);
-
fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT,
Set.of(outputForRelease));
- latestOutputDirectory = outputForRelease;
- unit.outputForRelease = outputForRelease;
- sourcesForDebugFile.add(unit);
+ }
+ }
+ /*
+ * The following adjustment is for the case when the project is a Java
module, but nevertheless organized
+ * in a package hierarchy instead of a module source hierarchy. Update
the `unit.roots` map for compiling
+ * the module as if module source hiearchy was used. It will require
moving the output directory after
+ * compilation, which is done by `ModuleDirectoryRemover`.
+ */
+ if (moduleNameFromPackageHierarchy != null) {
+ for (SourcesForRelease unit : units) {
+ Set<Path> paths = unit.roots.remove("");
+ if (paths != null) {
+ unit.roots.put(moduleNameFromPackageHierarchy, paths);
+ }
+ }
+ }
+ }
+
+ /**
+ * Manager of class-path or module-paths specified to a {@link
StandardJavaFileManager}.
+ * This base class assumes {@link DirectoryHierarchy#PACKAGE}, and a
subclass is defined
+ * for the {@link DirectoryHierarchy#MODULE_SOURCE} case.
+ */
+ private class PathManager {
+ /**
+ * The file manager to configure for class-path or module-paths.
+ */
+ protected final StandardJavaFileManager fileManager;
+
+ /**
+ * The output directory of the previous compilation phase or version.
+ * For test compilation, this is the main output directory.
+ * For multi-release, this is the output of the previous Java version.
+ */
+ protected Path latestOutputDirectory;
+
+ /**
+ * Whether we are compiling a version after the base version.
+ *
+ * @see #markVersioned()
+ */
+ private boolean isVersioned;
+
+ /**
+ * Creates a new path manager for the given file manager.
+ *
+ * @param fileManager the file manager to configure for class-path or
module-paths
+ */
+ protected PathManager(StandardJavaFileManager fileManager) {
+ this.fileManager = fileManager;
+ latestOutputDirectory = getOutputDirectoryOfPreviousPhase();
+ }
+
+ /**
+ * Merges all the given sets into a single set. We use our own loop
instead of streams
+ * because the given collection should always contain exactly one
{@code Set<Path>},
+ * so we can return that set directly without copying its content in a
new set.
+ * The merge is a paranoiac safety as we could also throw an exception
instead.
+ */
+ private static Set<Path> merge(final Collection<Set<Path>>
directories) {
+ Set<Path> allSources = Set.of();
+ for (Set<Path> more : directories) {
+ if (allSources.isEmpty()) {
+ allSources = more;
+ } else {
+ // Should never happen, but merge anyway by safety.
+ allSources = new LinkedHashSet<>(allSources);
+ allSources.addAll(more);
+ }
+ }
+ return allSources;
+ }
+
+ /**
+ * Configures source directories for all roots in a compilation unit.
+ * Also configures the class-path or module-paths with the output
directories
+ * of previous compilation units (if any).
+ *
+ * <h4>Default implementation</h4>
+ * The default implementation configures source directories and
class-path for package hierarchy
+ * without {@code module-info}. Sub-classes need to override this
method if the project is modular.
+ *
+ * @param roots map of module names to source paths
+ * @throws IOException if an error occurred while setting locations
+ */
+ protected void configureSourcePaths(final Map<String, Set<Path>>
roots) throws IOException {
+ fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH,
merge(roots.values()));
+
+ // For multi-release builds, add previous version's output to
class-path.
+ if (latestOutputDirectory != null) {
+ Deque<Path> paths = prependDependency(JavaPathType.CLASSES,
latestOutputDirectory);
+ fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH,
paths);
+ }
+ }
+
+ /**
+ * Sets up the output directory for a compilation unit.
+ *
+ * @param unit the compilation unit
+ * @throws IOException if an error occurred while creating directories
or setting locations
+ */
+ final void setupOutputDirectory(final SourcesForRelease unit) throws
IOException {
+ Path outputForRelease = outputDirectory;
+ if (isVersioned) {
+ outputForRelease = Files.createDirectories(
+
directoryHierarchy.outputDirectoryForReleases(outputForRelease, unit.release));
+ }
+ fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT,
Set.of(outputForRelease));
+ // Records that a compilation unit completed, updating the
baseline for the next phase.
+ latestOutputDirectory = outputForRelease;
+ unit.outputForRelease = outputForRelease;
+ sourcesForDebugFile.add(unit);
+ }
+
+ /**
+ * Marks that subsequent iterations are for versions after the base
version.
+ */
+ final void markVersioned() {
+ isVersioned = true;
+ }
+ }
+
+ /**
+ * Manager of module-paths specified to a {@link StandardJavaFileManager}.
+ * This subclass handles the {@link DirectoryHierarchy#MODULE_SOURCE} case.
+ *
+ * <h2>Implementation details</h2>
+ * The fields in this class are used for patching, i.e. when compiling
test classes or a non-base version
+ * of a multi-release project. The output directory of the previous Java
version needs to be added to the
+ * class-path or module-path. However, in the case of a modular project,
we can add to the module path only
+ * once and all other additions must be done as patches.
+ */
+ private final class ModulePathManager extends PathManager {
+ /**
+ * Whether we can add output directories to the module-path.
+ * For modular projects, we can only add to module-path once.
+ * Subsequent additions must use {@code --patch-module}.
+ */
+ private boolean canAddOutputToModulePath;
+
+ /**
+ * Tracks modules from previous versions that may not be present in
the current version.
+ * Keys are module names, values indicate whether cleanup is needed.
+ */
+ private final Map<String, Boolean> modulesNotPresentInNewVersion;
+
+ /**
+ * Tracks how many source directories were added as patches per module.
+ * Keys are module names, values are the count of source directories.
+ * Used to remove these source entries and replace them with compiled
output.
+ *
+ * <h4>Purpose</h4>
+ * When patching a module, the source directories of the compilation
unit are declared as a patch applied
+ * over the output directories of previous compilation units. But
after the compilation, if there are more
+ * units to compile, we will need to replace the sources in {@code
--patch-module} by the compilation output
+ * before to declare the source directories of the next compilation
unit.
+ */
+ private final Map<String, Integer> modulesWithSourcesAsPatches;
+
+ /**
+ * Creates a new path manager for the given file manager.
+ *
+ * @param fileManager the file manager to configure for class-path
or module-paths
+ */
+ ModulePathManager(StandardJavaFileManager fileManager) {
+ super(fileManager);
+ canAddOutputToModulePath = true;
+ modulesNotPresentInNewVersion = new LinkedHashMap<>();
+ modulesWithSourcesAsPatches = new HashMap<>();
+ }
+
+ /**
+ * Configures module source paths for all roots in a compilation unit.
+ * If the project uses package hierarchy with a {@code module-info}
file,
+ * the module names in the keys of the {@code roots} map must have
been resolved by
+ * {@link #determineDirectoryHierarchy(Collection)} before to invoke
this method.</p>
+ *
+ * <p>Configures also the {@code --patch-module} options for a module
being compiled for
+ * a newer Java version. The patch consists of (in order, highest
priority first):</p>
+ * <ol>
+ * <li>Current source paths (so the compiler sees the new version's
sources).</li>
+ * <li>Output from previous Java version (compiled classes to
inherit).</li>
+ * <li>Existing patch-module dependencies.</li>
+ * </ol>
+ *
+ * @param roots map of module names to source paths
+ * @throws IOException if an error occurred while setting locations
+ */
+ @Override
+ protected void configureSourcePaths(final Map<String, Set<Path>>
roots) throws IOException {
+ for (var entry : roots.entrySet()) {
+ final String moduleName = entry.getKey();
+ final Set<Path> sourcePaths = entry.getValue();
+
fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH,
moduleName, sourcePaths);
+ modulesNotPresentInNewVersion.put(moduleName, Boolean.FALSE);
/*
- * Compile the source files now. The following loop should be
executed exactly once.
- * It may be executed twice when compiling test classes
overwriting the `module-info`,
- * in which case the `module-info` needs to be compiled
separately from other classes.
- * However, this is a deprecated practice.
+ * When compiling for the base Java version, the configuration
for current module is finished.
+ * The remaining of this loop is executed only for target Java
versions after the base version.
+ * In those cases, we need to add the paths to the classes
compiled for the previous version.
+ * A non-modular project would always add the paths to the
class-path. For a modular project,
+ * add the paths to the module-path only the first time.
After, we need to use patch-module.
*/
- JavaCompiler.CompilationTask task;
- for (CompilationTaskSources c : toCompilationTasks(unit)) {
- Iterable<? extends JavaFileObject> sources =
fileManager.getJavaFileObjectsFromPaths(c.files);
- StandardJavaFileManager workaround = fileManager;
- boolean workaroundNeedsClose = false;
- // Check flag separately to clearly indicate this entire
block is a workaround hack.
- if (WorkaroundForPatchModule.ENABLED) {
- if (workaround instanceof WorkaroundForPatchModule wp)
{
- workaround = wp.getFileManagerIfUsable();
- if (workaround == null) {
- workaround = createFileManager(compiler,
false);
- wp.copyTo(workaround);
- workaroundNeedsClose = true;
- }
- }
- }
- task = compiler.getTask(otherOutput, workaround, listener,
configuration.options, null, sources);
- success = c.compile(task);
- if (workaroundNeedsClose) {
- workaround.close();
+ if (latestOutputDirectory != null) {
+ if (canAddOutputToModulePath) {
+ canAddOutputToModulePath = false;
+ Deque<Path> paths =
prependDependency(JavaPathType.MODULES, latestOutputDirectory);
+
fileManager.setLocationFromPaths(StandardLocation.MODULE_PATH, paths);
}
- if (!success) {
- break compile;
+ /*
+ * For a modular project, following block can be executed
an arbitrary number of times
+ * We need to declare that the sources that we are
compiling are for patching a module.
+ * But we also need to remember that these sources will
need to be removed in the next
+ * iteration, because they will be replaced by the
compiled classes (the above block).
+ */
+ final Deque<Path> paths =
dependencies(JavaPathType.patchModule(moduleName));
+ removeFirsts(paths,
modulesWithSourcesAsPatches.put(moduleName, sourcePaths.size()));
+ Path latestOutput =
resolveModuleOutputDirectory(latestOutputDirectory, moduleName);
+ if (Files.exists(latestOutput)) {
+ paths.addFirst(latestOutput);
}
+ sourcePaths.forEach(paths::addFirst);
+
fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH,
moduleName, paths);
}
- isVersioned = true; // Any further iteration is for a version
after the base version.
}
- /*
- * Post-compilation.
- */
- if (listener instanceof DiagnosticLogger diagnostic) {
- diagnostic.logSummary();
+ omitSourcelessModulesInNewVersion();
+ }
+
+ /**
+ * Removes from compilation the modules that were present in previous
version but not in the current version.
+ * This clears the source paths and updates patch-module for leftover
modules.
+ * This method has no effect when compiling for the base Java version.
+ *
+ * @throws IOException if an error occurred while setting locations
+ */
+ private void omitSourcelessModulesInNewVersion() throws IOException {
+ for (var iterator =
modulesNotPresentInNewVersion.entrySet().iterator(); iterator.hasNext(); ) {
+ Map.Entry<String, Boolean> entry = iterator.next();
+ if (entry.getValue()) {
+ String moduleName = entry.getKey();
+ Deque<Path> paths =
dependencies(JavaPathType.patchModule(moduleName));
+ if (removeFirsts(paths,
modulesWithSourcesAsPatches.remove(moduleName))) {
+
paths.addFirst(latestOutputDirectory.resolve(moduleName));
+ } else if (paths.isEmpty()) {
+ // Not sure why the following is needed, but it has
been observed in real projects.
+ paths.add(outputDirectory.resolve(moduleName));
+ }
+
fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH,
moduleName, paths);
+
fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH,
moduleName, Set.of());
+ iterator.remove();
+ } else {
+ entry.setValue(Boolean.TRUE); // For compilation of next
target version (if any).
+ }
}
- } catch (UncheckedIOException e) {
- throw e.getCause();
}
- if (success && incrementalBuild != null) {
- incrementalBuild.writeCache();
- incrementalBuild = null;
+
+ /**
+ * Removes the first <var>n</var> elements of the given collection.
+ * This is used for removing {@code --patch-module} items that were
added as source directories.
+ * The callers should replace the removed items by the output
directory of these source files.
+ *
+ * @param paths the paths from which to remove the first elements
+ * @param count number of elements to remove, or {@code null} if none
+ * @return whether at least one item has been removed
+ */
+ private static boolean removeFirsts(Deque<Path> paths, Integer count) {
+ boolean changed = false;
+ if (count != null) {
+ for (int i = count; --i >= 0; ) {
+ changed |= (paths.removeFirst() != null);
+ }
+ }
+ return changed;
}
- return success;
}
/**
- * Subdivides a compilation unit into one or more compilation tasks.
- * This is a workaround for deprecated practices such as overwriting the
main {@code module-info} in the tests.
- * In the latter case, we need to compile the test {@code module-info}
separately, before the other test classes.
+ * Runs the compilation task.
+ *
+ * @param compiler the compiler
+ * @param configuration the options to give to the Java compiler
+ * @param otherOutput where to write additional output from the compiler
+ * @return whether the compilation succeeded
+ * @throws IOException if an error occurred while reading or writing a file
+ * @throws MojoException if the compilation failed for a reason identified
by this method
+ * @throws RuntimeException if any other kind of error occurred
*/
- CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) {
- if (unit.files.isEmpty()) {
- return new CompilationTaskSources[0];
+ public boolean compile(JavaCompiler compiler, final Options configuration,
final Writer otherOutput)
+ throws IOException {
+
+ if (noSourcesToCompile()) {
+ return true;
}
- return new CompilationTaskSources[] {new
CompilationTaskSources(unit.files)};
+
+ // Determine project type once from all units before processing.
+ final Collection<SourcesForRelease> units = groupByReleaseAndModule();
+ determineDirectoryHierarchy(units);
+
+ // Workaround for a `javax.tools` method which seems not yet supported
on all compilers.
+ if (WorkaroundForPatchModule.ENABLED && hasModuleDeclaration &&
!(compiler instanceof ForkedTool)) {
+ compiler = new WorkaroundForPatchModule(compiler);
+ }
+ boolean success = true;
+ try (StandardJavaFileManager fileManager =
compiler.getStandardFileManager(listener, LOCALE, encoding)) {
+ setDependencyPaths(fileManager);
+ if (!generatedSourceDirectories.isEmpty()) {
+
fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT,
generatedSourceDirectories);
+ }
+ final PathManager pathManager =
+ switch (directoryHierarchy) {
+ case PACKAGE -> new PathManager(fileManager);
+ case PACKAGE_WITH_MODULE, MODULE_SOURCE -> new
ModulePathManager(fileManager);
+ };
+
+ // Compile each release version in order (base version first for
multi-release projects).
+ for (final SourcesForRelease unit : units) {
+ configuration.setRelease(unit.getReleaseString());
+ pathManager.configureSourcePaths(unit.roots);
+
+ // Snapshot dependencies for debug file
+ copyDependencyValues();
+ unit.dependencySnapshot = new LinkedHashMap<>(dependencies);
+
+ // Set up output directory and compile (only if there are
files).
+ pathManager.setupOutputDirectory(unit);
+
+ // Compile the source files now.
+ if (!unit.files.isEmpty()) {
+ Iterable<? extends JavaFileObject> sources =
fileManager.getJavaFileObjectsFromPaths(unit.files);
+ JavaCompiler.CompilationTask task;
+ task = compiler.getTask(otherOutput, fileManager,
listener, configuration.options, null, sources);
+ success = task.call();
+ if (!success) {
+ break;
+ }
+ }
+ pathManager.markVersioned();
+ }
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
+ }
+
+ // Performs post-compilation tasks such as logging and writing
incremental build cache.
+ if (listener instanceof DiagnosticLogger diagnostic) {
+ diagnostic.logSummary();
+ }
+ if (success) {
+ saveIncrementalBuild();
+ }
+ return success;
}
}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
index 0eba178..5665882 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.maven.plugin.compiler;
+import javax.lang.model.SourceVersion;
import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
@@ -26,21 +27,23 @@ import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UncheckedIOException;
import java.io.Writer;
import java.lang.module.ModuleDescriptor;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.HashMap;
import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-
-import org.apache.maven.api.JavaPathType;
-import org.apache.maven.api.PathType;
-import org.apache.maven.api.ProjectScope;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.stream.Stream;
import static
org.apache.maven.plugin.compiler.AbstractCompilerMojo.SUPPORT_LEGACY;
+import static org.apache.maven.plugin.compiler.DirectoryHierarchy.META_INF;
+import static
org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
+import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
/**
* A task which configures and executes the Java compiler for the test classes.
@@ -55,12 +58,13 @@ class ToolExecutorForTest extends ToolExecutor {
*
* @see TestCompilerMojo#mainOutputDirectory
*/
- protected final Path mainOutputDirectory;
+ private final Path mainOutputDirectory;
/**
- * Path to the {@code module-info.class} file of the main code, or {@code
null} if that file does not exist.
+ * The main output directory of each module. This is usually {@code
mainOutputDirectory/<module>},
+ * except if some modules are defined only for some Java versions higher
than the base version.
*/
- private final Path mainModulePath;
+ private final Map<String, Path> mainOutputDirectoryForModules;
/**
* Whether to place the main classes on the module path when {@code
module-info} is present.
@@ -86,27 +90,18 @@ class ToolExecutorForTest extends ToolExecutor {
private final boolean hasTestModuleInfo;
/**
- * Whether the tests are declared in their own module. If {@code true},
- * then the {@code module-info.java} file of the test declares a name
- * different than the {@code module-info.java} file of the main code.
- */
- private boolean testInItsOwnModule;
-
- /**
- * Whether the {@code module-info} of the tests overwrites the main {@code
module-info}.
- * This is a deprecated practice, but is accepted if {@link
#SUPPORT_LEGACY} is true.
- */
- private boolean overwriteMainModuleInfo;
-
- /**
- * Name of the main module to compile, or {@code null} if not yet
determined.
- * If the project is not modular, then this field contains an empty string.
+ * Name of the module when using package hierarchy, or {@code null} if not
applicable.
+ * This is used for setting {@code --patch-module} option during
compilation of tests.
+ * This field is null in a class-path project or in a multi-module project.
*
- * TODO: use "*" as a sentinel value for modular source hierarchy.
+ * <p>This field exists mostly for compatibility with the Maven 3 way to
build a modular project.
+ * It is recommended to use the {@code <sources>} element instead. We may
remove this field in a
+ * future version if we abandon compatibility with the Maven 3 way to
build modular projects.</p>
*
- * @see #getMainModuleName()
+ * @deprecated Declare modules in {@code <source>} elements instead.
*/
- private String moduleName;
+ @Deprecated(since = "4.0.0")
+ private String moduleNameFromPackageHierarchy;
/**
* Whether {@link #addModuleOptions(Options)} has already been invoked.
@@ -133,107 +128,163 @@ class ToolExecutorForTest extends ToolExecutor {
*
* @param mojo the <abbr>MOJO</abbr> from which to take a snapshot
* @param listener where to send compilation warnings, or {@code null} for
the Maven logger
+ * @param mainModulePath path to the {@code module-info.class} file of the
main code, or {@code null} if none
* @throws MojoException if this constructor identifies an invalid
parameter in the <abbr>MOJO</abbr>
* @throws IOException if an error occurred while creating the output
directory or scanning the source directories
*/
@SuppressWarnings("deprecation")
- ToolExecutorForTest(TestCompilerMojo mojo, DiagnosticListener<? super
JavaFileObject> listener) throws IOException {
+ ToolExecutorForTest(
+ final TestCompilerMojo mojo,
+ final DiagnosticListener<? super JavaFileObject> listener,
+ final Path mainModulePath)
+ throws IOException {
super(mojo, listener);
- mainOutputDirectory = mojo.mainOutputDirectory;
- mainModulePath = mojo.mainModulePath;
+ /*
+ * Notable work done by the parent constructor (examples with default
paths):
+ *
+ * - Set `outputDirectory` to a single "target/test-classes".
+ * - Set `sourceDirectories` to many "src/<module>/test/java".
+ * - Set `sourceFiles` to the content of `sourceDirectories`.
+ * - Set `dependencies` with class-path and module-path, but not
including main output directory.
+ *
+ * We will need to add the main output directory to the class-path or
module-path, but not here.
+ * It will be done by `ToolExecutor.compile(…)` if
`getOutputDirectoryOfPreviousPhase()` returns
+ * a non-null value.
+ */
useModulePath = mojo.useModulePath;
hasTestModuleInfo = mojo.hasTestModuleInfo;
+ mainOutputDirectory = mojo.mainOutputDirectory;
+ mainOutputDirectoryForModules = new HashMap<>();
+ if (Files.notExists(mainOutputDirectory)) {
+ return;
+ }
+ if (mainModulePath != null) {
+ try (InputStream in = Files.newInputStream(mainModulePath)) {
+ moduleNameFromPackageHierarchy =
ModuleDescriptor.read(in).name();
+ }
+ }
+ // Following is non-null only for modular project using package
hierarchy.
+ final String testModuleName =
mojo.moduleNameFromPackageHierarchy(sourceDirectories);
+ if (testModuleName != null) {
+ moduleNameFromPackageHierarchy = testModuleName;
+ }
/*
- * If we are compiling the test classes of a modular project, add the
`--patch-modules` options.
+ * If compiling the test classes of a modular project, we will need
`--patch-modules` options.
* In this case, the option values are directories of main class files
of the patched module.
+ * This block only prepares an empty map for each module. Maps are
filled in the next block.
*/
- final var patchedModules = new LinkedHashMap<String, Set<Path>>();
+ final var patchedModules = new LinkedHashMap<String,
NavigableMap<SourceVersion, Path>>();
for (SourceDirectory dir : sourceDirectories) {
String moduleToPatch = dir.moduleName;
if (moduleToPatch == null) {
- moduleToPatch = getMainModuleName();
- if (moduleToPatch.isEmpty()) {
+ moduleToPatch = moduleNameFromPackageHierarchy;
+ if (moduleToPatch == null) {
continue; // No module-info found.
}
- if (SUPPORT_LEGACY) {
- String testModuleName =
mojo.getTestModuleName(sourceDirectories);
- if (testModuleName != null) {
- overwriteMainModuleInfo =
testModuleName.equals(getMainModuleName());
- if (!overwriteMainModuleInfo) {
- testInItsOwnModule = true;
- continue; // The test classes are in their own
module.
- }
- }
- }
+ /*
+ * Modular project using package hierarchy (Maven 3 way).
+ * We will need to move directories after compilation for
reproducing the Maven 3 output.
+ */
directoryLevelToRemove = moduleToPatch;
}
- patchedModules.put(moduleToPatch, new LinkedHashSet<>()); //
Signal that this module exists in the test.
+ if (testModuleName != null &&
!moduleToPatch.equals(testModuleName)) {
+ // Mix of package hierarchy and module source hierarchy.
+ throw new CompilationFailureException(
+ "The \"" + testModuleName + "\" module must be
declared in a <module> element of <sources>.");
+ }
+ patchedModules.put(moduleToPatch, new TreeMap<>()); // Signal that
this module exists in the test.
+ }
+ // Shortcut for class-path projects.
+ if (patchedModules.isEmpty()) {
+ return;
}
/*
- * The values of `patchedModules` are empty lists. Now, add the real
paths to
- * main class for each module that exists in both the main code and
the test.
+ * The values of `patchedModules` are empty maps. Now, add the real
paths to the
+ * main classes for each module that exists in both the main code and
the tests.
+ * Note that a module may exist only in the
`META-INF/versions-modular/` directory.
*/
- mojo.getSourceRoots(ProjectScope.MAIN).forEach((root) -> {
- root.module().ifPresent((moduleToPatch) -> {
- Set<Path> paths = patchedModules.get(moduleToPatch);
- if (paths != null) {
- Path path = root.targetPath().orElseGet(() ->
Path.of(moduleToPatch));
- path = mainOutputDirectory.resolve(path);
- paths.add(path);
+ addDirectoryIfModule(
+ mainOutputDirectory, moduleNameFromPackageHierarchy,
SourceVersion.RELEASE_0, patchedModules);
+ addModuleDirectories(mainOutputDirectory, SourceVersion.RELEASE_0,
patchedModules);
+ Path versionsDirectory =
DirectoryHierarchy.MODULE_SOURCE.outputDirectoryForReleases(mainOutputDirectory);
+ if (Files.exists(versionsDirectory)) {
+ List<Path> asList;
+ try (Stream<Path> paths = Files.list(versionsDirectory)) {
+ asList = paths.toList();
+ }
+ for (Path path : asList) {
+ SourceVersion version;
+ try {
+ version =
SourceDirectory.parse(path.getFileName().toString());
+ } catch (UnsupportedVersionException e) {
+ logger.debug(e);
+ continue;
}
- });
- });
- patchedModules.values().removeIf(Set::isEmpty);
- patchedModules.forEach((moduleToPatch, paths) -> {
-
dependencies(JavaPathType.patchModule(moduleToPatch)).addAll(paths);
- });
+ addModuleDirectories(path, version, patchedModules);
+ }
+ }
/*
- * If there is no module to patch, we probably have a non-modular
project.
- * In such case, we need to put the main output directory on the
classpath.
- * It may also be a modular project not declared in the `<source>`
element.
+ * At this point, we finished to scan the main output directory for
modules.
+ * Remembers the directories of each module. They are usually
sub-directories
+ * of the main directory, but could also be in
`META-INF/versions-modular/`.
*/
- if (patchedModules.isEmpty() && Files.exists(mainOutputDirectory)) {
- PathType pathType = JavaPathType.CLASSES;
- if (hasModuleDeclaration) {
- pathType = JavaPathType.MODULES;
- if (!testInItsOwnModule) {
- String moduleToPatch = getMainModuleName();
- if (!moduleToPatch.isEmpty()) {
- pathType = JavaPathType.patchModule(moduleToPatch);
- directoryLevelToRemove = moduleToPatch;
- }
- }
+ patchedModules.forEach((moduleToPatch, directories) -> {
+ Map.Entry<SourceVersion, Path> base = directories.firstEntry();
+ if (base != null) {
+ mainOutputDirectoryForModules.putIfAbsent(moduleToPatch,
base.getValue());
}
- prependDependency(pathType, mainOutputDirectory);
- }
+ });
}
/**
- * {@return the module name of the main code, or an empty string if none}
- * This method reads the module descriptor when first needed and caches
the result.
- * This used if the user did not specified an explicit {@code <module>}
element in the sources.
+ * Performs a shallow scan of the given directory for modules.
+ * This method searches for {@code module-info.class} files.
+ *
+ * <p>The keys of the {@code addTo} map are module names. Values are paths
for all versions where
+ * {@code module-info.class} has been found. Note that this is not an
exhaustive list of paths for
+ * all versions, because most {@code versions} directories do not have a
{@code module-info.class} file.
+ * Therefore, the {@code SortedMap} will usually contain only the base
directory. But we check versions
+ * anyway because sometime, a module does not exist in the base directory
and is first defined only for
+ * a higher version.</p>
+ *
+ * <p>This method adds paths to existing entries only, and ignores modules
that are not already in the map.
+ * This is done that way for collecting modules that are both in the main
code and in the tests.</p>
*
- * @throws IOException if the module descriptor cannot be read.
+ * @param directory the directory to scan
+ * @param version target Java version of the directory to add
+ * @param addTo where to add the module paths
+ * @throws IOException if an error occurred while scanning the directories
*/
- private String getMainModuleName() throws IOException {
- if (moduleName == null) {
- if (mainModulePath != null) {
- try (InputStream in = Files.newInputStream(mainModulePath)) {
- moduleName = ModuleDescriptor.read(in).name();
- }
- } else {
- moduleName = "";
- }
+ private void addModuleDirectories(
+ Path directory, SourceVersion version, Map<String,
NavigableMap<SourceVersion, Path>> addTo)
+ throws IOException {
+
+ try (Stream<Path> paths = Files.list(directory)) {
+ paths.forEach(
+ (path) -> addDirectoryIfModule(path,
path.getFileName().toString(), version, addTo));
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
}
- return moduleName;
}
/**
- * If the given module name is empty, tries to infer a default module name.
+ * Adds the given directory in {@code addTo} if the directory contains a
{@code module-info.class} file.
+ *
+ * @param directory the directory to scan
+ * @param moduleName name of the module to add
+ * @param version target Java version of the directory to add
+ * @param addTo where to add the module paths
*/
- @Override
- final String inferModuleNameIfMissing(String moduleName) throws
IOException {
- return (!testInItsOwnModule && moduleName.isEmpty()) ?
getMainModuleName() : moduleName;
+ private static void addDirectoryIfModule(
+ Path directory,
+ String moduleName,
+ SourceVersion version,
+ Map<String, NavigableMap<SourceVersion, Path>> addTo) {
+
+ NavigableMap<SourceVersion, Path> versions = addTo.get(moduleName);
+ if (versions != null &&
Files.isRegularFile(directory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX))) {
+ versions.putIfAbsent(version, directory);
+ }
}
/**
@@ -258,7 +309,7 @@ class ToolExecutorForTest extends ToolExecutor {
final var patches = new LinkedHashMap<String, ModuleInfoPatch>();
for (SourceDirectory source : sourceDirectories) {
Path file = source.root.resolve(ModuleInfoPatch.FILENAME);
- String module;
+ String moduleName;
if (Files.notExists(file)) {
if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo &&
hasModuleDeclaration) {
/*
@@ -273,29 +324,30 @@ class ToolExecutorForTest extends ToolExecutor {
* We generate that patch only for the first module. If there
is more modules
* without `patch-module-info`, we will copy the `defaultInfo`
instance.
*/
- module = source.moduleName;
- if (module == null) {
- module = getMainModuleName();
- if (module.isEmpty()) {
+ moduleName = source.moduleName;
+ if (moduleName == null) {
+ moduleName = moduleNameFromPackageHierarchy;
+ if (moduleName == null) {
continue;
}
}
if (defaultInfo != null) {
- patches.putIfAbsent(module, null); // Remember that we
will need to compute a value later.
+ patches.putIfAbsent(moduleName, null); // Remember that we
will need to compute a value later.
continue;
}
- defaultInfo = new ModuleInfoPatch(module, info);
+ defaultInfo = new ModuleInfoPatch(moduleName, info);
defaultInfo.setToDefaults();
info = defaultInfo;
} else {
- info = new ModuleInfoPatch(getMainModuleName(), info);
+ info = new ModuleInfoPatch(moduleNameFromPackageHierarchy,
info);
try (BufferedReader reader = Files.newBufferedReader(file)) {
info.load(reader);
}
- module = info.getModuleName();
+ moduleName = info.getModuleName();
}
- if (patches.put(module, info) != null) {
- throw new ModuleInfoPatchException("\"module-info-patch " +
module + "\" is defined more than once.");
+ if (patches.put(moduleName, info) != null) {
+ throw new ModuleInfoPatchException(
+ "\"module-info-patch " + moduleName + "\" is defined
more than once.");
}
}
/*
@@ -320,7 +372,7 @@ class ToolExecutorForTest extends ToolExecutor {
*/
if (!patches.isEmpty()) {
Path directory = // TODO: replace by Path.resolve(String,
String...) with JDK22.
-
Files.createDirectories(outputDirectory.resolve("META-INF").resolve("maven"));
+
Files.createDirectories(outputDirectory.resolve(META_INF).resolve("maven"));
try (BufferedWriter out =
Files.newBufferedWriter(directory.resolve("module-info-patch.args"))) {
for (ModuleInfoPatch m : patches.values()) {
m.writeTo(configuration, out);
@@ -350,48 +402,44 @@ class ToolExecutorForTest extends ToolExecutor {
}
/**
- * Separates the compilation of {@code module-info} from other classes.
This is needed when the
- * {@code module-info} of the test classes overwrite the {@code
module-info} of the main classes.
- * In the latter case, we need to compile the test {@code module-info}
first in order to substitute
- * the main module-info by the test one before to compile the remaining
test classes.
+ * Returns the output directory of the main classes. This is the directory
to prepend to
+ * the class-path or module-path before to compile the classes managed by
this executor.
+ *
+ * @return the directory to prepend to the class-path or module-path
*/
@Override
- final CompilationTaskSources[] toCompilationTasks(final SourcesForRelease
unit) {
- if (!(SUPPORT_LEGACY && useModulePath && hasTestModuleInfo &&
overwriteMainModuleInfo)) {
- return super.toCompilationTasks(unit);
- }
- CompilationTaskSources moduleInfo = null;
- final List<Path> files = unit.files;
- for (int i = files.size(); --i >= 0; ) {
- if (SourceDirectory.isModuleInfoSource(files.get(i))) {
- moduleInfo = new
CompilationTaskSources(List.of(files.remove(i)));
- if (files.isEmpty()) {
- return new CompilationTaskSources[] {moduleInfo};
- }
- break;
- }
- }
- if (files.isEmpty()) {
- return new CompilationTaskSources[0];
- }
- var task = new CompilationTaskSources(files) {
- /**
- * Substitutes the main {@code module-info.class} by the test's
one, compiles test classes,
- * then restores the original {@code module-info.class}. The test
{@code module-info.class}
- * must have been compiled separately before this method is
invoked.
- */
- @Override
- boolean compile(JavaCompiler.CompilationTask task) throws
IOException {
- try (unit) {
- unit.substituteModuleInfos(mainOutputDirectory,
outputDirectory);
- return super.compile(task);
- }
+ Path getOutputDirectoryOfPreviousPhase() {
+ return mainOutputDirectory;
+ }
+
+ /**
+ * Returns the directory of the classes compiled for the specified module.
+ * If the project is multi-release, this method returns the directory for
the base version.
+ *
+ * @param outputDirectory the output directory which is the root of modules
+ * @param moduleName the name of the module for which the class directory
is desired
+ * @return directories of classes for the given module
+ */
+ @Override
+ Path resolveModuleOutputDirectory(Path outputDirectory, String moduleName)
{
+ if (outputDirectory.equals(mainOutputDirectory)) {
+ Path path = mainOutputDirectoryForModules.get(moduleName);
+ if (path != null) {
+ return path;
}
- };
- if (moduleInfo != null) {
- return new CompilationTaskSources[] {moduleInfo, task};
- } else {
- return new CompilationTaskSources[] {task};
}
+ return super.resolveModuleOutputDirectory(outputDirectory, moduleName);
+ }
+
+ /**
+ * Name of the module when using package hierarchy, or {@code null} if not
applicable.
+ * This is null in a class-path project or in a multi-module project.
+ *
+ * @deprecated This information exists only for compatibility with the
Maven 3 way to build a modular project.
+ */
+ @Override
+ @Deprecated(since = "4.0.0")
+ final String moduleNameFromPackageHierarchy() {
+ return moduleNameFromPackageHierarchy;
}
}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
b/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
index 88f8b04..8606d3d 100644
---
a/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
+++
b/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
@@ -18,8 +18,12 @@
*/
package org.apache.maven.plugin.compiler;
+import javax.annotation.processing.Processor;
+import javax.lang.model.SourceVersion;
+import javax.tools.DiagnosticListener;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
@@ -27,12 +31,18 @@ import javax.tools.StandardLocation;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -52,195 +62,343 @@ import org.apache.maven.api.JavaPathType;
*
* @author Martin Desruisseaux
*/
-final class WorkaroundForPatchModule extends
ForwardingJavaFileManager<StandardJavaFileManager>
- implements StandardJavaFileManager {
+final class WorkaroundForPatchModule implements JavaCompiler {
/**
* Set this flag to {@code false} for testing if this workaround is still
necessary.
*/
static final boolean ENABLED = true;
/**
- * All locations that have been successfully specified to the file manager
through programmatic API.
- * This set excludes the {@code PATCH_MODULE_PATH} locations which were
defined using the workaround
- * described in class Javadoc.
+ * The actual compiler provided by {@link javax.tools}.
*/
- private final Set<JavaFileManager.Location> definedLocations;
+ private final JavaCompiler compiler;
/**
- * The locations that we had to define by formatting a {@code
--patch-module} option.
- * Keys are module names and values are the paths for the associated
module.
- */
- private final Map<String, Collection<? extends Path>> patchesAsOption;
-
- /**
- * Whether the caller needs to create a new file manager.
- * It happens when we have been unable to set a {@code --patch-module}
option on the current file manager.
+ * Creates a new workaround as a wrapper for the given compiler.
+ *
+ * @param compiler the actual compiler provided by {@link javax.tools}
*/
- private boolean needsNewFileManager;
+ WorkaroundForPatchModule(JavaCompiler compiler) {
+ this.compiler = compiler;
+ }
/**
- * Creates a new workaround for the given file manager.
+ * Forwards the call to the wrapped compiler.
+ *
+ * @return the name of the compiler tool
*/
- WorkaroundForPatchModule(final StandardJavaFileManager fileManager) {
- super(fileManager);
- definedLocations = new HashSet<>();
- patchesAsOption = new HashMap<>();
+ @Override
+ public String name() {
+ return compiler.name();
}
/**
- * {@return the original file manager, or {@code null} if the caller needs
to create a new one}
- * The returned value is {@code null} when we have been unable to set a
{@code --patch-module}
- * option on the current file manager. In such case, the caller should
create a new file manager
- * and configure it with {@link #copyTo(StandardJavaFileManager)}.
+ * Forwards the call to the wrapped compiler.
+ *
+ * @return the source versions of the Java programming language supported
by the compiler
*/
- StandardJavaFileManager getFileManagerIfUsable() {
- return needsNewFileManager ? null : fileManager;
+ @Override
+ public Set<SourceVersion> getSourceVersions() {
+ return compiler.getSourceVersions();
}
/**
- * Copies the locations defined in this file manager to the given file
manager.
+ * Forwards the call to the wrapped compiler.
*
- * @param target where to copy the locations
- * @throws IOException if a location cannot be set on the target file
manager
+ * @return whether the given option is supported and if so, the number of
arguments the option takes
*/
- void copyTo(final StandardJavaFileManager target) throws IOException {
- for (JavaFileManager.Location location : definedLocations) {
- target.setLocation(location, fileManager.getLocation(location));
- }
- for (Map.Entry<String, Collection<? extends Path>> entry :
patchesAsOption.entrySet()) {
- Collection<? extends Path> paths = entry.getValue();
- String moduleName = entry.getKey();
- try {
-
target.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName,
paths);
- } catch (UnsupportedOperationException e) {
- specifyAsOption(target, JavaPathType.patchModule(moduleName),
paths, e);
- }
- }
+ @Override
+ public int isSupportedOption(String option) {
+ return compiler.isSupportedOption(option);
}
/**
- * Sets a module path by asking the file manager to parse an option
formatted by this method.
- * Invoked when a module path cannot be specified through the standard
<abbr>API</abbr>.
- * This is the workaround described in class Javadoc.
+ * Forwards the call to the wrapped compiler and wraps the file manager in
a workaround.
*
- * @param fileManager the file manager on which an attempt to set the
location has been made and failed
- * @param type the type of path together with the module name
- * @param paths the paths to set
- * @param cause the exception that occurred when invoking the standard API
- * @throws IllegalArgumentException if this workaround doesn't work neither
+ * @param diagnosticListener a listener for non-fatal diagnostics
+ * @param locale the locale to apply when formatting diagnostics
+ * @param charset the character set used for decoding bytes
+ * @return a file manager with workaround
*/
- private static void specifyAsOption(
- StandardJavaFileManager fileManager,
- JavaPathType.Modular type,
- Collection<? extends Path> paths,
- UnsupportedOperationException cause)
- throws IOException {
-
- String message;
- Iterator<String> it = Arrays.asList(type.option(paths)).iterator();
- if (!fileManager.handleOption(it.next(), it)) {
- message = "Failed to set the %s option for module %s";
- } else if (it.hasNext()) {
- message = "Unexpected number of arguments after the %s option for
module %s";
- } else {
- return;
- }
- JavaPathType rawType = type.rawType();
- throw new IllegalArgumentException(
- String.format(message,
rawType.option().orElse(rawType.name()), type.moduleName()), cause);
+ @Override
+ public StandardJavaFileManager getStandardFileManager(
+ DiagnosticListener<? super JavaFileObject> diagnosticListener,
Locale locale, Charset charset) {
+ return new
FileManager(compiler.getStandardFileManager(diagnosticListener, locale,
charset), locale, charset);
}
/**
- * Adds the given module path to the file manager.
- * If we cannot do that using the programmatic API, formats as a
command-line option.
+ * Forwards the call to the wrapped compiler and wraps the task in a
workaround.
+ *
+ * @param out destination of additional output from the compiler
+ * @param fileManager a file manager created by {@code
getStandardFileManager(…)}
+ * @param diagnosticListener a listener for non-fatal diagnostics
+ * @param options compiler options
+ * @param classes names of classes to be processed by annotation processing
+ * @param compilationUnits the compilation units to compile
+ * @return an object representing the compilation
*/
@Override
- public void setLocationForModule(
- JavaFileManager.Location location, String moduleName, Collection<?
extends Path> paths) throws IOException {
-
- if (paths.isEmpty()) {
- return;
- }
- final boolean isPatch = (location ==
StandardLocation.PATCH_MODULE_PATH);
- if (isPatch && patchesAsOption.replace(moduleName, paths) != null) {
- /*
- * The patch was already specified by formatting the
`--patch-module` option.
- * We cannot do that again, because that option can appear only
once per module.
- */
- needsNewFileManager = true;
- return;
- }
- try {
- fileManager.setLocationForModule(location, moduleName, paths);
- } catch (UnsupportedOperationException e) {
- if (isPatch) {
- specifyAsOption(fileManager,
JavaPathType.patchModule(moduleName), paths, e);
- patchesAsOption.put(moduleName, paths);
- return;
+ public CompilationTask getTask(
+ Writer out,
+ JavaFileManager fileManager,
+ DiagnosticListener<? super JavaFileObject> diagnosticListener,
+ Iterable<String> options,
+ Iterable<String> classes,
+ Iterable<? extends JavaFileObject> compilationUnits) {
+ if (fileManager instanceof FileManager wp) {
+ fileManager = wp.getFileManagerIfUsable();
+ if (fileManager == null) {
+ final StandardJavaFileManager workaround =
+ compiler.getStandardFileManager(diagnosticListener,
wp.locale, wp.charset);
+ try {
+ wp.copyTo(workaround);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ final CompilationTask task =
+ compiler.getTask(out, workaround, diagnosticListener,
options, classes, compilationUnits);
+ return new CompilationTask() {
+ @Override
+ public void setLocale(Locale locale) {
+ task.setLocale(locale);
+ }
+
+ @Override
+ public void setProcessors(Iterable<? extends Processor>
processors) {
+ task.setProcessors(processors);
+ }
+
+ @Override
+ public void addModules(Iterable<String> moduleNames) {
+ task.addModules(moduleNames);
+ }
+
+ @Override
+ public Boolean call() {
+ final Boolean result = task.call();
+ try {
+ workaround.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return result;
+ }
+ };
}
- throw e;
}
- definedLocations.add(fileManager.getLocationForModule(location,
moduleName));
+ return compiler.getTask(out, fileManager, diagnosticListener, options,
classes, compilationUnits);
}
/**
- * Adds the given path to the file manager.
+ * Not used by the Maven Compiler Plugin.
*/
@Override
- public void setLocationFromPaths(JavaFileManager.Location location,
Collection<? extends Path> paths)
- throws IOException {
- fileManager.setLocationFromPaths(location, paths);
- definedLocations.add(location);
+ public int run(InputStream in, OutputStream out, OutputStream err,
String... arguments) {
+ return compiler.run(in, out, err, arguments);
}
- @Override
- public void setLocation(Location location, Iterable<? extends File> files)
throws IOException {
- fileManager.setLocation(location, files);
- definedLocations.add(location);
- }
+ /**
+ * A file manager which fallbacks on the {@code --patch-module}
+ * option when it cannot use {@link StandardLocation#PATCH_MODULE_PATH}.
+ * This is the class where the actual workaround is implemented.
+ */
+ private static final class FileManager extends
ForwardingJavaFileManager<StandardJavaFileManager>
+ implements StandardJavaFileManager {
+ /**
+ * The locale specified by the user when creating this file manager.
+ * Saved for allowing the creation of other file managers.
+ */
+ final Locale locale;
- @Override
- public Iterable<? extends File> getLocation(Location location) {
- return fileManager.getLocation(location);
- }
+ /**
+ * The character set specified by the user when creating this file
manager.
+ * Saved for allowing the creation of other file managers.
+ */
+ final Charset charset;
- @Override
- public Iterable<? extends Path> getLocationAsPaths(Location location) {
- return fileManager.getLocationAsPaths(location);
- }
+ /**
+ * All locations that have been successfully specified to the file
manager through programmatic API.
+ * This set excludes the {@code PATCH_MODULE_PATH} locations which
were defined using the workaround
+ * described in class Javadoc.
+ */
+ private final Set<JavaFileManager.Location> definedLocations;
- @Override
- public Iterable<? extends JavaFileObject> getJavaFileObjects(String...
names) {
- return fileManager.getJavaFileObjects(names);
- }
+ /**
+ * The locations that we had to define by formatting a {@code
--patch-module} option.
+ * Keys are module names and values are the paths for the associated
module.
+ */
+ private final Map<String, Collection<? extends Path>> patchesAsOption;
- @Override
- public Iterable<? extends JavaFileObject> getJavaFileObjects(File...
files) {
- return fileManager.getJavaFileObjects(files);
- }
+ /**
+ * Whether the caller needs to create a new file manager.
+ * It happens when we have been unable to set a {@code --patch-module}
option on the current file manager.
+ */
+ private boolean needsNewFileManager;
- @Override
- public Iterable<? extends JavaFileObject> getJavaFileObjects(Path...
paths) {
- return fileManager.getJavaFileObjects(paths);
- }
+ /**
+ * Creates a new workaround for the given file manager.
+ */
+ FileManager(StandardJavaFileManager fileManager, Locale locale,
Charset charset) {
+ super(fileManager);
+ this.locale = locale;
+ this.charset = charset;
+ definedLocations = new LinkedHashSet<>();
+ patchesAsOption = new LinkedHashMap<>();
+ }
- @Override
- public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromStrings(Iterable<String> names) {
- return fileManager.getJavaFileObjectsFromStrings(names);
- }
+ /**
+ * {@return the original file manager, or {@code null} if the caller
needs to create a new one}
+ * The returned value is {@code null} when we have been unable to set
a {@code --patch-module}
+ * option on the current file manager. In such case, the caller should
create a new file manager
+ * and configure it with {@link #copyTo(StandardJavaFileManager)}.
+ */
+ StandardJavaFileManager getFileManagerIfUsable() {
+ return needsNewFileManager ? null : fileManager;
+ }
- @Override
- public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromFiles(Iterable<? extends File> files) {
- return fileManager.getJavaFileObjectsFromFiles(files);
- }
+ /**
+ * Copies the locations defined in this file manager to the given file
manager.
+ *
+ * @param target where to copy the locations
+ * @throws IOException if a location cannot be set on the target file
manager
+ */
+ void copyTo(final StandardJavaFileManager target) throws IOException {
+ for (JavaFileManager.Location location : definedLocations) {
+ target.setLocation(location,
fileManager.getLocation(location));
+ }
+ for (Map.Entry<String, Collection<? extends Path>> entry :
patchesAsOption.entrySet()) {
+ Collection<? extends Path> paths = entry.getValue();
+ String moduleName = entry.getKey();
+ try {
+
target.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName,
paths);
+ } catch (UnsupportedOperationException e) {
+ specifyAsOption(target,
JavaPathType.patchModule(moduleName), paths, e);
+ }
+ }
+ }
- @Override
- public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromPaths(Collection<? extends Path> paths) {
- return fileManager.getJavaFileObjectsFromPaths(paths);
- }
+ /**
+ * Sets a module path by asking the file manager to parse an option
formatted by this method.
+ * Invoked when a module path cannot be specified through the standard
<abbr>API</abbr>.
+ * This is the workaround described in class Javadoc.
+ *
+ * @param fileManager the file manager on which an attempt to set the
location has been made and failed
+ * @param type the type of path together with the module name
+ * @param paths the paths to set
+ * @param cause the exception that occurred when invoking the standard
API
+ * @throws IllegalArgumentException if this workaround doesn't work
neither
+ */
+ private static void specifyAsOption(
+ StandardJavaFileManager fileManager,
+ JavaPathType.Modular type,
+ Collection<? extends Path> paths,
+ UnsupportedOperationException cause)
+ throws IOException {
- @Override
- public Path asPath(FileObject file) {
- return fileManager.asPath(file);
+ String message;
+ Iterator<String> it = Arrays.asList(type.option(paths)).iterator();
+ if (!fileManager.handleOption(it.next(), it)) {
+ message = "Failed to set the %s option for module %s";
+ } else if (it.hasNext()) {
+ message = "Unexpected number of arguments after the %s option
for module %s";
+ } else {
+ return;
+ }
+ JavaPathType rawType = type.rawType();
+ throw new IllegalArgumentException(
+ String.format(message,
rawType.option().orElse(rawType.name()), type.moduleName()), cause);
+ }
+
+ /**
+ * Adds the given module path to the file manager.
+ * If we cannot do that using the programmatic API, formats as a
command-line option.
+ */
+ @Override
+ public void setLocationForModule(
+ JavaFileManager.Location location, String moduleName,
Collection<? extends Path> paths)
+ throws IOException {
+ if (location == StandardLocation.PATCH_MODULE_PATH) {
+ if (patchesAsOption.replace(moduleName, paths) != null) {
+ /*
+ * The patch was already specified by formatting the
`--patch-module` option.
+ * We cannot do that again, because that option can appear
only once per module.
+ * We nevertheless stored the new paths in
`patchesAsOption` for use by `copyTo(…)`.
+ */
+ needsNewFileManager = true;
+ return;
+ }
+ try {
+ fileManager.setLocationForModule(location, moduleName,
paths);
+ } catch (UnsupportedOperationException e) {
+ specifyAsOption(fileManager,
JavaPathType.patchModule(moduleName), paths, e);
+ patchesAsOption.put(moduleName, paths);
+ return;
+ }
+ } else {
+ fileManager.setLocationForModule(location, moduleName, paths);
+ }
+ definedLocations.add(fileManager.getLocationForModule(location,
moduleName));
+ }
+
+ /**
+ * Adds the given path to the file manager.
+ */
+ @Override
+ public void setLocationFromPaths(JavaFileManager.Location location,
Collection<? extends Path> paths)
+ throws IOException {
+ fileManager.setLocationFromPaths(location, paths);
+ definedLocations.add(location);
+ }
+
+ @Override
+ public void setLocation(Location location, Iterable<? extends File>
files) throws IOException {
+ fileManager.setLocation(location, files);
+ definedLocations.add(location);
+ }
+
+ @Override
+ public Iterable<? extends File> getLocation(Location location) {
+ return fileManager.getLocation(location);
+ }
+
+ @Override
+ public Iterable<? extends Path> getLocationAsPaths(Location location) {
+ return fileManager.getLocationAsPaths(location);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject> getJavaFileObjects(String...
names) {
+ return fileManager.getJavaFileObjects(names);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject> getJavaFileObjects(File...
files) {
+ return fileManager.getJavaFileObjects(files);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject> getJavaFileObjects(Path...
paths) {
+ return fileManager.getJavaFileObjects(paths);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromStrings(Iterable<String> names) {
+ return fileManager.getJavaFileObjectsFromStrings(names);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromFiles(Iterable<? extends File> files) {
+ return fileManager.getJavaFileObjectsFromFiles(files);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromPaths(Collection<? extends Path> paths) {
+ return fileManager.getJavaFileObjectsFromPaths(paths);
+ }
+
+ @Override
+ public Path asPath(FileObject file) {
+ return fileManager.asPath(file);
+ }
}
}