1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.plugin.compiler;
20
21 import javax.annotation.processing.Processor;
22 import javax.lang.model.SourceVersion;
23 import javax.tools.DiagnosticListener;
24 import javax.tools.FileObject;
25 import javax.tools.ForwardingJavaFileManager;
26 import javax.tools.JavaCompiler;
27 import javax.tools.JavaFileManager;
28 import javax.tools.JavaFileObject;
29 import javax.tools.StandardJavaFileManager;
30 import javax.tools.StandardLocation;
31
32 import java.io.File;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.OutputStream;
36 import java.io.UncheckedIOException;
37 import java.io.Writer;
38 import java.nio.charset.Charset;
39 import java.nio.file.Path;
40 import java.util.Arrays;
41 import java.util.Collection;
42 import java.util.Iterator;
43 import java.util.LinkedHashMap;
44 import java.util.LinkedHashSet;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.Set;
48
49 import org.apache.maven.api.JavaPathType;
50
51 /**
52 * Workaround for a {@code javax.tools} method which seems not yet supported on all compilers.
53 * At least with OpenJDK 24, an {@link UnsupportedOperationException} may occur during the call to
54 * {@code fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths)}.
55 * The workaround is to format the paths in a {@code --patch-module} option instead.
56 * The problem is that we can specify this option only once per file manager instance.
57 *
58 * <p>We may remove this workaround in a future version of the Maven Compiler Plugin
59 * if the {@code UnsupportedOperationException} is fixed in a future Java release.
60 * For checking if this workaround is still necessary, set {@link #ENABLED} to {@code false}
61 * and run the JUnit tests.</p>
62 *
63 * @author Martin Desruisseaux
64 */
65 final class WorkaroundForPatchModule implements JavaCompiler {
66 /**
67 * Set this flag to {@code false} for testing if this workaround is still necessary.
68 */
69 static final boolean ENABLED = true;
70
71 /**
72 * The actual compiler provided by {@link javax.tools}.
73 */
74 private final JavaCompiler compiler;
75
76 /**
77 * Creates a new workaround as a wrapper for the given compiler.
78 *
79 * @param compiler the actual compiler provided by {@link javax.tools}
80 */
81 WorkaroundForPatchModule(JavaCompiler compiler) {
82 this.compiler = compiler;
83 }
84
85 /**
86 * Forwards the call to the wrapped compiler.
87 *
88 * @return the name of the compiler tool
89 */
90 @Override
91 public String name() {
92 return compiler.name();
93 }
94
95 /**
96 * Forwards the call to the wrapped compiler.
97 *
98 * @return the source versions of the Java programming language supported by the compiler
99 */
100 @Override
101 public Set<SourceVersion> getSourceVersions() {
102 return compiler.getSourceVersions();
103 }
104
105 /**
106 * Forwards the call to the wrapped compiler.
107 *
108 * @return whether the given option is supported and if so, the number of arguments the option takes
109 */
110 @Override
111 public int isSupportedOption(String option) {
112 return compiler.isSupportedOption(option);
113 }
114
115 /**
116 * Forwards the call to the wrapped compiler and wraps the file manager in a workaround.
117 *
118 * @param diagnosticListener a listener for non-fatal diagnostics
119 * @param locale the locale to apply when formatting diagnostics
120 * @param charset the character set used for decoding bytes
121 * @return a file manager with workaround
122 */
123 @Override
124 public StandardJavaFileManager getStandardFileManager(
125 DiagnosticListener<? super JavaFileObject> diagnosticListener, Locale locale, Charset charset) {
126 return new FileManager(compiler.getStandardFileManager(diagnosticListener, locale, charset), locale, charset);
127 }
128
129 /**
130 * Forwards the call to the wrapped compiler and wraps the task in a workaround.
131 *
132 * @param out destination of additional output from the compiler
133 * @param fileManager a file manager created by {@code getStandardFileManager(…)}
134 * @param diagnosticListener a listener for non-fatal diagnostics
135 * @param options compiler options
136 * @param classes names of classes to be processed by annotation processing
137 * @param compilationUnits the compilation units to compile
138 * @return an object representing the compilation
139 */
140 @Override
141 public CompilationTask getTask(
142 Writer out,
143 JavaFileManager fileManager,
144 DiagnosticListener<? super JavaFileObject> diagnosticListener,
145 Iterable<String> options,
146 Iterable<String> classes,
147 Iterable<? extends JavaFileObject> compilationUnits) {
148 if (fileManager instanceof FileManager wp) {
149 fileManager = wp.getFileManagerIfUsable();
150 if (fileManager == null) {
151 final StandardJavaFileManager workaround =
152 compiler.getStandardFileManager(diagnosticListener, wp.locale, wp.charset);
153 try {
154 wp.copyTo(workaround);
155 } catch (IOException e) {
156 throw new UncheckedIOException(e);
157 }
158 final CompilationTask task =
159 compiler.getTask(out, workaround, diagnosticListener, options, classes, compilationUnits);
160 return new CompilationTask() {
161 @Override
162 public void setLocale(Locale locale) {
163 task.setLocale(locale);
164 }
165
166 @Override
167 public void setProcessors(Iterable<? extends Processor> processors) {
168 task.setProcessors(processors);
169 }
170
171 @Override
172 public void addModules(Iterable<String> moduleNames) {
173 task.addModules(moduleNames);
174 }
175
176 @Override
177 public Boolean call() {
178 final Boolean result = task.call();
179 try {
180 workaround.close();
181 } catch (IOException e) {
182 throw new UncheckedIOException(e);
183 }
184 return result;
185 }
186 };
187 }
188 }
189 return compiler.getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits);
190 }
191
192 /**
193 * Not used by the Maven Compiler Plugin.
194 */
195 @Override
196 public int run(InputStream in, OutputStream out, OutputStream err, String... arguments) {
197 return compiler.run(in, out, err, arguments);
198 }
199
200 /**
201 * A file manager which fallbacks on the {@code --patch-module}
202 * option when it cannot use {@link StandardLocation#PATCH_MODULE_PATH}.
203 * This is the class where the actual workaround is implemented.
204 */
205 private static final class FileManager extends ForwardingJavaFileManager<StandardJavaFileManager>
206 implements StandardJavaFileManager {
207 /**
208 * The locale specified by the user when creating this file manager.
209 * Saved for allowing the creation of other file managers.
210 */
211 final Locale locale;
212
213 /**
214 * The character set specified by the user when creating this file manager.
215 * Saved for allowing the creation of other file managers.
216 */
217 final Charset charset;
218
219 /**
220 * All locations that have been successfully specified to the file manager through programmatic API.
221 * This set excludes the {@code PATCH_MODULE_PATH} locations which were defined using the workaround
222 * described in class Javadoc.
223 */
224 private final Set<JavaFileManager.Location> definedLocations;
225
226 /**
227 * The locations that we had to define by formatting a {@code --patch-module} option.
228 * Keys are module names and values are the paths for the associated module.
229 */
230 private final Map<String, Collection<? extends Path>> patchesAsOption;
231
232 /**
233 * Whether the caller needs to create a new file manager.
234 * It happens when we have been unable to set a {@code --patch-module} option on the current file manager.
235 */
236 private boolean needsNewFileManager;
237
238 /**
239 * Creates a new workaround for the given file manager.
240 */
241 FileManager(StandardJavaFileManager fileManager, Locale locale, Charset charset) {
242 super(fileManager);
243 this.locale = locale;
244 this.charset = charset;
245 definedLocations = new LinkedHashSet<>();
246 patchesAsOption = new LinkedHashMap<>();
247 }
248
249 /**
250 * {@return the original file manager, or {@code null} if the caller needs to create a new one}
251 * The returned value is {@code null} when we have been unable to set a {@code --patch-module}
252 * option on the current file manager. In such case, the caller should create a new file manager
253 * and configure it with {@link #copyTo(StandardJavaFileManager)}.
254 */
255 StandardJavaFileManager getFileManagerIfUsable() {
256 return needsNewFileManager ? null : fileManager;
257 }
258
259 /**
260 * Copies the locations defined in this file manager to the given file manager.
261 *
262 * @param target where to copy the locations
263 * @throws IOException if a location cannot be set on the target file manager
264 */
265 void copyTo(final StandardJavaFileManager target) throws IOException {
266 for (JavaFileManager.Location location : definedLocations) {
267 target.setLocation(location, fileManager.getLocation(location));
268 }
269 for (Map.Entry<String, Collection<? extends Path>> entry : patchesAsOption.entrySet()) {
270 Collection<? extends Path> paths = entry.getValue();
271 String moduleName = entry.getKey();
272 try {
273 target.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths);
274 } catch (UnsupportedOperationException e) {
275 specifyAsOption(target, JavaPathType.patchModule(moduleName), paths, e);
276 }
277 }
278 }
279
280 /**
281 * Sets a module path by asking the file manager to parse an option formatted by this method.
282 * Invoked when a module path cannot be specified through the standard <abbr>API</abbr>.
283 * This is the workaround described in class Javadoc.
284 *
285 * @param fileManager the file manager on which an attempt to set the location has been made and failed
286 * @param type the type of path together with the module name
287 * @param paths the paths to set
288 * @param cause the exception that occurred when invoking the standard API
289 * @throws IllegalArgumentException if this workaround doesn't work neither
290 */
291 private static void specifyAsOption(
292 StandardJavaFileManager fileManager,
293 JavaPathType.Modular type,
294 Collection<? extends Path> paths,
295 UnsupportedOperationException cause)
296 throws IOException {
297
298 String message;
299 Iterator<String> it = Arrays.asList(type.option(paths)).iterator();
300 if (!fileManager.handleOption(it.next(), it)) {
301 message = "Failed to set the %s option for module %s";
302 } else if (it.hasNext()) {
303 message = "Unexpected number of arguments after the %s option for module %s";
304 } else {
305 return;
306 }
307 JavaPathType rawType = type.rawType();
308 throw new IllegalArgumentException(
309 String.format(message, rawType.option().orElse(rawType.name()), type.moduleName()), cause);
310 }
311
312 /**
313 * Adds the given module path to the file manager.
314 * If we cannot do that using the programmatic API, formats as a command-line option.
315 */
316 @Override
317 public void setLocationForModule(
318 JavaFileManager.Location location, String moduleName, Collection<? extends Path> paths)
319 throws IOException {
320 if (location == StandardLocation.PATCH_MODULE_PATH) {
321 if (patchesAsOption.replace(moduleName, paths) != null) {
322 /*
323 * The patch was already specified by formatting the `--patch-module` option.
324 * We cannot do that again, because that option can appear only once per module.
325 * We nevertheless stored the new paths in `patchesAsOption` for use by `copyTo(…)`.
326 */
327 needsNewFileManager = true;
328 return;
329 }
330 try {
331 fileManager.setLocationForModule(location, moduleName, paths);
332 } catch (UnsupportedOperationException e) {
333 specifyAsOption(fileManager, JavaPathType.patchModule(moduleName), paths, e);
334 patchesAsOption.put(moduleName, paths);
335 return;
336 }
337 } else {
338 fileManager.setLocationForModule(location, moduleName, paths);
339 }
340 definedLocations.add(fileManager.getLocationForModule(location, moduleName));
341 }
342
343 /**
344 * Adds the given path to the file manager.
345 */
346 @Override
347 public void setLocationFromPaths(JavaFileManager.Location location, Collection<? extends Path> paths)
348 throws IOException {
349 fileManager.setLocationFromPaths(location, paths);
350 definedLocations.add(location);
351 }
352
353 @Override
354 public void setLocation(Location location, Iterable<? extends File> files) throws IOException {
355 fileManager.setLocation(location, files);
356 definedLocations.add(location);
357 }
358
359 @Override
360 public Iterable<? extends File> getLocation(Location location) {
361 return fileManager.getLocation(location);
362 }
363
364 @Override
365 public Iterable<? extends Path> getLocationAsPaths(Location location) {
366 return fileManager.getLocationAsPaths(location);
367 }
368
369 @Override
370 public Iterable<? extends JavaFileObject> getJavaFileObjects(String... names) {
371 return fileManager.getJavaFileObjects(names);
372 }
373
374 @Override
375 public Iterable<? extends JavaFileObject> getJavaFileObjects(File... files) {
376 return fileManager.getJavaFileObjects(files);
377 }
378
379 @Override
380 public Iterable<? extends JavaFileObject> getJavaFileObjects(Path... paths) {
381 return fileManager.getJavaFileObjects(paths);
382 }
383
384 @Override
385 public Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(Iterable<String> names) {
386 return fileManager.getJavaFileObjectsFromStrings(names);
387 }
388
389 @Override
390 public Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(Iterable<? extends File> files) {
391 return fileManager.getJavaFileObjectsFromFiles(files);
392 }
393
394 @Override
395 public Iterable<? extends JavaFileObject> getJavaFileObjectsFromPaths(Collection<? extends Path> paths) {
396 return fileManager.getJavaFileObjectsFromPaths(paths);
397 }
398
399 @Override
400 public Path asPath(FileObject file) {
401 return fileManager.asPath(file);
402 }
403 }
404 }