1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.plugin.compiler;
20
21 import java.io.IOException;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24
25 import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
26 import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX;
27 import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
28
29 /**
30 * Helper class for the case where a {@code module-info.java} file defined in the tests
31 * overwrites the file defined in the main classes. It should be a last-resort practice only,
32 * when options such as {@code --add-reads} or {@code --add-exports} are not sufficient.
33 *
34 * <p>The code in this class is useful only when {@link AbstractCompilerMojo#SUPPORT_LEGACY} is true.
35 * This class can be fully deleted if a future version permanently set above-cited flag to false.</p>
36 */
37 final class ModuleInfoOverwrite implements Runnable {
38 /**
39 * Path to the original {@code module-info.java} file. It will need to be temporarily renamed
40 * because otherwise the Java compiler seems to unconditionally compiles it, even if we do not
41 * specify this file in the list of sources to compile.
42 */
43 private final Path testSourceFile;
44
45 /**
46 * Path to the {@code module-info.java.bak} file.
47 */
48 private final Path savedSourceFile;
49
50 /**
51 * Path to the main {@code module-info.class} file to temporarily hide.
52 * This file will be temporarily renamed to {@link #moduleInfoBackup}
53 * before to compile the tests.
54 */
55 private final Path moduleInfoToHide;
56
57 /**
58 * Path to the renamed main {@code module-info.class} file. This file
59 * needs to be renamed as {@link #moduleInfoToHide} after compilation.
60 */
61 private final Path moduleInfoBackup;
62
63 /**
64 * The {@code module-info.class} to use as a replacement for the one which has been renamed.
65 */
66 private final Path moduleInfoReplacement;
67
68 /**
69 * The shutdown hook invoked if the user interrupts the compilation, for example with [Control-C].
70 */
71 private Thread shutdownHook;
72
73 /**
74 * Creates a new instance.
75 */
76 private ModuleInfoOverwrite(Path source, Path main, Path test) {
77 testSourceFile = source;
78 savedSourceFile = source.resolveSibling(MODULE_INFO + JAVA_FILE_SUFFIX + ".bak");
79 moduleInfoToHide = main;
80 moduleInfoBackup = main.resolveSibling(MODULE_INFO + CLASS_FILE_SUFFIX + ".bak");
81 moduleInfoReplacement = test;
82 }
83
84 /**
85 * Returns an instance for the given main output directory, or {@code null} if not needed.
86 * This method should be invoked only if a {@code module-info.java} file exists and may
87 * overwrite a file defined in the main classes.
88 */
89 static ModuleInfoOverwrite create(Path source, Path mainOutputDirectory, Path testOutputDirectory)
90 throws IOException {
91 Path main = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
92 if (Files.isRegularFile(main)) {
93 Path test = testOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
94 if (Files.isRegularFile(test)) {
95 var mo = new ModuleInfoOverwrite(source, main, test);
96 mo.substitute();
97 return mo;
98 }
99 }
100 return null;
101 }
102
103 /**
104 * Replaces the main {@code module-info.class} file by the test one.
105 * The original file is saved in the {@code module-info.class.bak} file.
106 * Then the test {@code module-info.class} is moved to the main directory.
107 * Note that it needs to be moved, not copied or linked, because we need
108 * to temporarily remove {@code module-info.class} from the test directory
109 * (otherwise {@code javac} does not seem to consider that we are patching a module).
110 *
111 * @throws IOException if an error occurred while renaming the file.
112 */
113 private void substitute() throws IOException {
114 Files.move(testSourceFile, savedSourceFile);
115 Files.move(moduleInfoToHide, moduleInfoBackup);
116 Files.move(moduleInfoReplacement, moduleInfoToHide);
117 if (shutdownHook == null) { // Paranoiac check in case this method is invoked twice (should not happen).
118 shutdownHook = new Thread(this);
119 Runtime.getRuntime().addShutdownHook(shutdownHook);
120 }
121 }
122
123 /**
124 * Restores the {@code module-info} file.
125 *
126 * @throws IOException if an error occurred while renaming the file.
127 */
128 void restore() throws IOException {
129 if (shutdownHook != null) {
130 Runtime.getRuntime().removeShutdownHook(shutdownHook);
131 }
132 Files.move(savedSourceFile, testSourceFile); // File to restore in priority.
133 Files.move(moduleInfoToHide, moduleInfoReplacement);
134 Files.move(moduleInfoBackup, moduleInfoToHide);
135 }
136
137 /**
138 * Invoked during JVM shutdown if user interrupted the compilation, for example with [Control-C].
139 */
140 @Override
141 @SuppressWarnings("CallToPrintStackTrace")
142 public void run() {
143 shutdownHook = null;
144 try {
145 restore();
146 } catch (IOException e) {
147 // We cannot do much better because the loggers are shutting down.
148 e.printStackTrace();
149 }
150 }
151 }