View Javadoc
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 }