View Javadoc
1   package org.codehaus.plexus.compiler.javac;
2   
3   /**
4    * The MIT License
5    *
6    * Copyright (c) 2005, The Codehaus
7    *
8    * Permission is hereby granted, free of charge, to any person obtaining a copy of
9    * this software and associated documentation files (the "Software"), to deal in
10   * the Software without restriction, including without limitation the rights to
11   * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12   * of the Software, and to permit persons to whom the Software is furnished to do
13   * so, subject to the following conditions:
14   *
15   * The above copyright notice and this permission notice shall be included in all
16   * copies or substantial portions of the Software.
17   *
18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24   * SOFTWARE.
25   */
26  
27  /**
28   *
29   * Copyright 2004 The Apache Software Foundation
30   *
31   *  Licensed under the Apache License, Version 2.0 (the "License");
32   *  you may not use this file except in compliance with the License.
33   *  You may obtain a copy of the License at
34   *
35   *     http://www.apache.org/licenses/LICENSE-2.0
36   *
37   *  Unless required by applicable law or agreed to in writing, software
38   *  distributed under the License is distributed on an "AS IS" BASIS,
39   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
40   *  See the License for the specific language governing permissions and
41   *  limitations under the License.
42   */
43  import javax.inject.Inject;
44  import javax.inject.Named;
45  import javax.inject.Singleton;
46  
47  import java.io.BufferedReader;
48  import java.io.File;
49  import java.io.FileWriter;
50  import java.io.IOException;
51  import java.io.PrintWriter;
52  import java.io.StringReader;
53  import java.io.StringWriter;
54  import java.lang.reflect.InvocationTargetException;
55  import java.lang.reflect.Method;
56  import java.net.MalformedURLException;
57  import java.net.URL;
58  import java.net.URLClassLoader;
59  import java.util.ArrayList;
60  import java.util.Arrays;
61  import java.util.Deque;
62  import java.util.HashSet;
63  import java.util.List;
64  import java.util.Map;
65  import java.util.NoSuchElementException;
66  import java.util.Objects;
67  import java.util.Properties;
68  import java.util.Set;
69  import java.util.StringTokenizer;
70  import java.util.concurrent.ConcurrentHashMap;
71  import java.util.concurrent.ConcurrentLinkedDeque;
72  import java.util.regex.Matcher;
73  import java.util.regex.Pattern;
74  
75  import org.codehaus.plexus.compiler.AbstractCompiler;
76  import org.codehaus.plexus.compiler.CompilerConfiguration;
77  import org.codehaus.plexus.compiler.CompilerException;
78  import org.codehaus.plexus.compiler.CompilerMessage;
79  import org.codehaus.plexus.compiler.CompilerOutputStyle;
80  import org.codehaus.plexus.compiler.CompilerResult;
81  import org.codehaus.plexus.util.FileUtils;
82  import org.codehaus.plexus.util.Os;
83  import org.codehaus.plexus.util.StringUtils;
84  import org.codehaus.plexus.util.cli.CommandLineException;
85  import org.codehaus.plexus.util.cli.CommandLineUtils;
86  import org.codehaus.plexus.util.cli.Commandline;
87  
88  import static org.codehaus.plexus.compiler.CompilerMessage.Kind.*;
89  import static org.codehaus.plexus.compiler.javac.JavacCompiler.Messages.*;
90  
91  /**
92   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
93   * @author <a href="mailto:matthew.pocock@ncl.ac.uk">Matthew Pocock</a>
94   * @author <a href="mailto:joerg.wassmer@web.de">J&ouml;rg Wa&szlig;mer</a>
95   * @author Alexander Kriegisch
96   * @author Others
97   *
98   */
99  @Named("javac")
100 @Singleton
101 public class JavacCompiler extends AbstractCompiler {
102 
103     /**
104      * Multi-language compiler messages to parse from forked javac output.
105      * <ul>
106      *   <li>OpenJDK 8+ is delivered with 3 locales (en, ja, zh_CN).</li>
107      *   <li>OpenJDK 21+ is delivered with 4 locales (en, ja, zh_CN, de).</li>
108      * </ul>
109      * Instead of manually duplicating multi-language messages into this class, it would be preferable to fetch the
110      * strings directly from the running JDK:
111      * <pre>{@code
112      * new JavacMessages("com.sun.tools.javac.resources.javac", Locale.getDefault())
113      *   .getLocalizedString("javac.msg.proc.annotation.uncaught.exception")
114      * }</pre>
115      * Hoewever, due to JMS module protection, it would be necessary to run Plexus Compiler (and hence also Maven
116      * Compiler and the whole Maven JVM) with {@code --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED}
117      * on more recent JDK versions. As this cannot be reliably expected and using internal APIs - even though stable
118      * since at least JDK 8 - it is not a future-proof approach. So we refrain from doing so, even though during Plexus
119      * Compiler development it might come in handy.
120      * <p>
121      * TODO: Check compiler.properties and javac.properties in OpenJDK javac source code for
122      *       message changes, relevant new messages, new locales.
123      */
124     protected static class Messages {
125         // compiler.properties -> compiler.err.error (en, ja, zh_CN, de)
126         protected static final String[] ERROR_PREFIXES = {"error: ", "エラー: ", "错误: ", "Fehler: "};
127 
128         // compiler.properties -> compiler.warn.warning (en, ja, zh_CN, de)
129         protected static final String[] WARNING_PREFIXES = {"warning: ", "警告: ", "警告: ", "Warnung: "};
130 
131         // compiler.properties -> compiler.note.note (en, ja, zh_CN, de)
132         protected static final String[] NOTE_PREFIXES = {"Note: ", "ノート: ", "注: ", "Hinweis: "};
133 
134         // compiler.properties -> compiler.misc.verbose.*
135         protected static final String[] MISC_PREFIXES = {"["};
136 
137         // Generic javac error prefix
138         // TODO: In JDK 8, this generic prefix no longer seems to be in use for javac error messages, at least not in
139         //       the Java part of javac. Maybe in C sources? Does javac even use any native classes?
140         protected static final String[] JAVAC_GENERIC_ERROR_PREFIXES = {"javac:"};
141 
142         // Hard-coded, English-only error header in JVM native code, *not* followed by stack trace, but rather
143         // by another text message
144         protected static final String[] VM_INIT_ERROR_HEADERS = {"Error occurred during initialization of VM"};
145 
146         // Hard-coded, English-only error header in class System, followed by stack trace
147         protected static final String[] BOOT_LAYER_INIT_ERROR_HEADERS = {
148             "Error occurred during initialization of boot layer"
149         };
150 
151         // javac.properties-> javac.msg.proc.annotation.uncaught.exception
152         // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21)
153         protected static final String[] ANNOTATION_PROCESSING_ERROR_HEADERS = {
154             "\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n",
155             "\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n\n",
156             "\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n",
157             "\n\nAn annotation processor threw an uncaught exception.\nConsult the following stack trace for details.\n\n",
158             "\n\n注釈処理で捕捉されない例外がスローされました。\n詳細は次のスタックトレースで調査してください。\n\n",
159             "\n\n批注处理程序抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n\n",
160             "\n\nEin Annotationsprozessor hat eine nicht abgefangene Ausnahme ausgelöst.\nDetails finden Sie im folgenden Stacktrace.\n\n"
161         };
162 
163         // javac.properties-> javac.msg.bug
164         // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-9, ja JDK-9, zh_CN JDK-9, en JDK-21, ja JDK-21, zh_CN JDK-21, de
165         // JDK-21)
166         protected static final String[] FILE_A_BUG_ERROR_HEADERS = {
167             "An exception has occurred in the compiler ({0}). Please file a bug at the Java Developer Connection (http://java.sun.com/webapps/bugreport)  after checking the Bug Parade for duplicates. Include your program and the following diagnostic in your report.  Thank you.\n",
168             "コンパイラで例外が発生しました({0})。Bug Paradeで重複がないかをご確認のうえ、Java Developer Connection (http://java.sun.com/webapps/bugreport)でbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。\n",
169             "编译器 ({0}) 中出现异常错误。 如果在 Bug Parade 中没有找到该错误, 请在 Java Developer Connection (http://java.sun.com/webapps/bugreport) 中建立 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。\n",
170             "An exception has occurred in the compiler ({0}). Please file a bug against the Java compiler via the Java bug reporting page (http://bugreport.java.com) after checking the Bug Database (http://bugs.java.com) for duplicates. Include your program and the following diagnostic in your report. Thank you.",
171             "コンパイラで例外が発生しました({0})。Bug Database (http://bugs.java.com)で重複がないかをご確認のうえ、Java bugレポート・ページ(http://bugreport.java.com)でJavaコンパイラに対するbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてください。ご協力ありがとうございます。",
172             "编译器 ({0}) 中出现异常错误。如果在 Bug Database (http://bugs.java.com) 中没有找到该错误, 请通过 Java Bug 报告页 (http://bugreport.java.com) 建立该 Java 编译器 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。",
173             "An exception has occurred in the compiler ({0}). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program, the following diagnostic, and the parameters passed to the Java compiler in your report. Thank you.\n",
174             "コンパイラで例外が発生しました({0})。バグ・データベース(https://bugs.java.com)で重複がないかをご確認のうえ、Javaのバグ・レポート・ページ(https://bugreport.java.com)から、Javaコンパイラに対するバグの登録をお願いいたします。レポートには、該当のプログラム、次の診断内容、およびJavaコンパイラに渡されたパラメータをご入力ください。ご協力ありがとうございます。\n",
175             "编译器 ({0}) 中出现异常错误。如果在 Bug Database (https://bugs.java.com) 中没有找到有关该错误的 Java 编译器 Bug,请通过 Java Bug 报告页 (https://bugreport.java.com) 提交 Java 编译器 Bug。请在报告中附上您的程序、以下诊断信息以及传递到 Java 编译器的参数。谢谢。\n",
176             "Im Compiler ({0}) ist eine Ausnahme aufgetreten. Erstellen Sie auf der Java-Seite zum Melden von Bugs (https://bugreport.java.com) einen Bugbericht, nachdem Sie die Bugdatenbank (https://bugs.java.com) auf Duplikate geprüft haben. Geben Sie in Ihrem Bericht Ihr Programm, die folgende Diagnose und die Parameter an, die Sie dem Java-Compiler übergeben haben. Vielen Dank.\n"
177         };
178 
179         // javac.properties-> javac.msg.resource
180         // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21)
181         protected static final String[] SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS = {
182             "\n\nThe system is out of resources.\nConsult the following stack trace for details.\n",
183             "\n\nシステム・リソースが不足しています。\n詳細は次のスタック・トレースで調査してください。\n",
184             "\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
185             "\n\nThe system is out of resources.\nConsult the following stack trace for details.\n",
186             "\n\nシステム・リソースが不足しています。\n詳細は次のスタックトレースで調査してください。\n",
187             "\n\n系统资源不足。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
188             "\n\nDas System hat keine Ressourcen mehr.\nDetails finden Sie im folgenden Stacktrace.\n"
189         };
190 
191         // javac.properties-> javac.msg.io
192         // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21)
193         protected static final String[] IO_ERROR_HEADERS = {
194             "\n\nAn input/output error occurred.\nConsult the following stack trace for details.\n",
195             "\n\n入出力エラーが発生しました。\n詳細は次のスタック・トレースで調査してください。\n",
196             "\n\n发生输入/输出错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
197             "\n\nAn input/output error occurred.\nConsult the following stack trace for details.\n",
198             "\n\n入出力エラーが発生しました。\n詳細は次のスタックトレースで調査してください。\n",
199             "\n\n发生输入/输出错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
200             "\n\nEin Eingabe-/Ausgabefehler ist aufgetreten.\nDetails finden Sie im folgenden Stacktrace.\n"
201         };
202 
203         // javac.properties-> javac.msg.plugin.uncaught.exception
204         // (en JDK-8, ja JDK-8, zh_CN JDK-8, en JDK-21, ja JDK-21, zh_CN JDK-21, de JDK-21)
205         protected static final String[] PLUGIN_ERROR_HEADERS = {
206             "\n\nA plugin threw an uncaught exception.\nConsult the following stack trace for details.\n",
207             "\n\nプラグインで捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n",
208             "\n\n插件抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
209             "\n\nA plugin threw an uncaught exception.\nConsult the following stack trace for details.\n",
210             "\n\nプラグインで捕捉されない例外がスローされました。\n詳細は次のスタック・トレースで調査してください。\n",
211             "\n\n插件抛出未捕获的异常错误。\n有关详细信息, 请参阅以下堆栈跟踪。\n",
212             "\n\nEin Plug-in hat eine nicht abgefangene Ausnahme ausgel\u00F6st.\nDetails finden Sie im folgenden Stacktrace.\n"
213         };
214     }
215 
216     private static final Object LOCK = new Object();
217     private static final String JAVAC_CLASSNAME = "com.sun.tools.javac.Main";
218 
219     private volatile Class<?> javacClass;
220     private final Deque<Class<?>> javacClasses = new ConcurrentLinkedDeque<>();
221 
222     private static final Pattern JAVA_MAJOR_AND_MINOR_VERSION_PATTERN = Pattern.compile("\\d+(\\.\\d+)?");
223 
224     /** Cache of javac version per executable (never invalidated) */
225     private static final Map<String, String> VERSION_PER_EXECUTABLE = new ConcurrentHashMap<>();
226 
227     @Inject
228     private InProcessCompiler inProcessCompiler;
229 
230     // ----------------------------------------------------------------------
231     //
232     // ----------------------------------------------------------------------
233 
234     public JavacCompiler() {
235         super(CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE, ".java", ".class", null);
236     }
237 
238     // ----------------------------------------------------------------------
239     // Compiler Implementation
240     // ----------------------------------------------------------------------
241 
242     @Override
243     public String getCompilerId() {
244         return "javac";
245     }
246 
247     private String getInProcessJavacVersion() throws CompilerException {
248         return System.getProperty("java.version");
249     }
250 
251     private String getOutOfProcessJavacVersion(String executable) throws CompilerException {
252         String version = VERSION_PER_EXECUTABLE.get(executable);
253         if (version == null) {
254             Commandline cli = new Commandline();
255             cli.setExecutable(executable);
256             /*
257              * The option "-version" should be supported by javac since 1.6 (https://docs.oracle.com/javase/6/docs/technotes/tools/solaris/javac.html)
258              * up to 21 (https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html#standard-options)
259              */
260             cli.addArguments(new String[] {"-version"}); //
261             List<String> out = new ArrayList<>();
262             List<String> err = new ArrayList<>();
263             try {
264                 int exitCode = CommandLineUtils.executeCommandLine(cli, out::add, err::add);
265                 if (exitCode != 0) {
266                     throw new CompilerException("Could not retrieve version from " + executable + ". Exit code "
267                             + exitCode + ", Output: " + String.join(System.lineSeparator(), out) + ", Error: "
268                             + String.join(System.lineSeparator(), err));
269                 }
270             } catch (CommandLineException e) {
271                 throw new CompilerException("Error while executing the external compiler " + executable, e);
272             }
273             version = tryParseVersion(out);
274             if (version == null) {
275                 version = tryParseVersion(err);
276             }
277             VERSION_PER_EXECUTABLE.put(executable, version);
278         }
279         return version;
280     }
281 
282     static String extractMajorAndMinorVersion(String text) {
283         Matcher matcher = JAVA_MAJOR_AND_MINOR_VERSION_PATTERN.matcher(text);
284         if (!matcher.find()) {
285             throw new IllegalArgumentException("Could not extract version from \"" + text + "\"");
286         }
287         return matcher.group();
288     }
289 
290     private String tryParseVersion(List<String> versions) {
291         for (String version : versions) {
292             if (version.startsWith("javac ")) {
293                 version = version.substring(6);
294                 if (version.startsWith("1.")) {
295                     version = version.substring(0, 3);
296                 } else {
297                     version = version.substring(0, 2);
298                 }
299                 return version;
300             }
301         }
302         return null;
303     }
304 
305     @Override
306     public CompilerResult performCompile(CompilerConfiguration config) throws CompilerException {
307         File destinationDir = new File(config.getOutputLocation());
308         if (!destinationDir.exists()) {
309             destinationDir.mkdirs();
310         }
311 
312         String[] sourceFiles = getSourceFiles(config);
313         if ((sourceFiles == null) || (sourceFiles.length == 0)) {
314             return new CompilerResult();
315         }
316 
317         logCompiling(sourceFiles, config);
318 
319         final String javacVersion;
320         final String executable;
321         if (config.isFork()) {
322             executable = getJavacExecutable(config);
323             javacVersion = getOutOfProcessJavacVersion(executable);
324         } else {
325             javacVersion = getInProcessJavacVersion();
326             executable = null;
327         }
328 
329         String[] args = buildCompilerArguments(config, sourceFiles, javacVersion);
330         CompilerResult result;
331 
332         if (config.isFork()) {
333             result = compileOutOfProcess(config, executable, args);
334         } else {
335             if (hasJavaxToolProvider() && !config.isForceJavacCompilerUse()) {
336                 // use fqcn to prevent loading of the class on 1.5 environment !
337                 result = inProcessCompiler().compileInProcess(args, config, sourceFiles);
338             } else {
339                 result = compileInProcess(args, config);
340             }
341         }
342 
343         return result;
344     }
345 
346     protected InProcessCompiler inProcessCompiler() {
347         return inProcessCompiler;
348     }
349 
350     /**
351      * Maps the {@code 1.x} spelling that {@code -source} and {@code -target} accept onto the plain feature
352      * number, the only form {@code --release} takes: {@code 1.8} becomes {@code 8}. Any other value is
353      * passed through unchanged, so javac still reports releases it does not support.
354      *
355      * @param release the configured release version, never empty
356      * @return the value to pass to {@code --release}
357      */
358     static String toReleaseArgument(String release) {
359         if (release.length() == 3 && release.startsWith("1.") && release.charAt(2) >= '1' && release.charAt(2) <= '9') {
360             return release.substring(2);
361         }
362         return release;
363     }
364 
365     /**
366      *
367      * @return {@code true} if the current context class loader has access to {@code javax.tools.ToolProvider}
368      */
369     protected static boolean hasJavaxToolProvider() {
370         try {
371             Thread.currentThread().getContextClassLoader().loadClass("javax.tools.ToolProvider");
372             return true;
373         } catch (Exception e) {
374             return false;
375         }
376     }
377 
378     public String[] createCommandLine(CompilerConfiguration config) throws CompilerException {
379         final String javacVersion;
380         if (config.isFork()) {
381             String executable = getJavacExecutable(config);
382             javacVersion = getOutOfProcessJavacVersion(executable);
383         } else {
384             javacVersion = getInProcessJavacVersion();
385         }
386         return buildCompilerArguments(config, getSourceFiles(config), javacVersion);
387     }
388 
389     public static String[] buildCompilerArguments(
390             CompilerConfiguration config, String[] sourceFiles, String javacVersion) {
391         List<String> args = new ArrayList<>();
392 
393         // ----------------------------------------------------------------------
394         // Set output
395         // ----------------------------------------------------------------------
396 
397         File destinationDir = new File(config.getOutputLocation());
398         args.add("-d");
399         args.add(destinationDir.getAbsolutePath());
400 
401         // ----------------------------------------------------------------------
402         // Set the class and source paths
403         // ----------------------------------------------------------------------
404 
405         List<String> classpathEntries = config.getClasspathEntries();
406         if (classpathEntries != null && !classpathEntries.isEmpty()) {
407             args.add("-classpath");
408             args.add(getPathString(classpathEntries));
409         }
410 
411         List<String> modulepathEntries = config.getModulepathEntries();
412         if (modulepathEntries != null && !modulepathEntries.isEmpty()) {
413             args.add("--module-path");
414             args.add(getPathString(modulepathEntries));
415         }
416 
417         List<String> sourceLocations = config.getSourceLocations();
418         if (sourceLocations != null && !sourceLocations.isEmpty()) {
419             // always pass source path, even if sourceFiles are declared,
420             // needed for jsr269 annotation processing, see MCOMPILER-98
421             args.add("-sourcepath");
422             args.add(getPathString(sourceLocations));
423         }
424         if (!hasJavaxToolProvider() || config.isForceJavacCompilerUse() || config.isFork()) {
425             args.addAll(Arrays.asList(sourceFiles));
426         }
427 
428         if (JavaVersion.JAVA_1_6.isOlderOrEqualTo(javacVersion)) {
429             // now add jdk 1.6 annotation processing related parameters
430 
431             if (config.getGeneratedSourcesDirectory() != null) {
432                 config.getGeneratedSourcesDirectory().mkdirs();
433                 args.add("-s");
434                 args.add(config.getGeneratedSourcesDirectory().getAbsolutePath());
435             }
436             if (config.getProc() != null) {
437                 args.add("-proc:" + config.getProc());
438             }
439             String annotationProcessors = joinAnnotationProcessors(config.getAnnotationProcessors());
440             if (!annotationProcessors.isEmpty()) {
441                 args.add("-processor");
442                 args.add(annotationProcessors);
443             }
444             if (config.getProcessorPathEntries() != null
445                     && !config.getProcessorPathEntries().isEmpty()) {
446                 args.add("-processorpath");
447                 args.add(getPathString(config.getProcessorPathEntries()));
448             }
449             if (config.getProcessorModulePathEntries() != null
450                     && !config.getProcessorModulePathEntries().isEmpty()) {
451                 args.add("--processor-module-path");
452                 args.add(getPathString(config.getProcessorModulePathEntries()));
453             }
454         }
455 
456         if (config.isOptimize()) {
457             args.add("-O");
458         }
459 
460         if (config.isDebug()) {
461             if (StringUtils.isNotEmpty(config.getDebugLevel())) {
462                 args.add("-g:" + config.getDebugLevel());
463             } else {
464                 args.add("-g");
465             }
466         }
467 
468         if (config.isVerbose()) {
469             args.add("-verbose");
470         }
471 
472         if (JavaVersion.JAVA_1_8.isOlderOrEqualTo(javacVersion) && config.isParameters()) {
473             args.add("-parameters");
474         }
475 
476         if (config.isEnablePreview()) {
477             args.add("--enable-preview");
478         }
479 
480         if (config.getImplicitOption() != null) {
481             args.add("-implicit:" + config.getImplicitOption());
482         }
483 
484         if (config.isShowDeprecation()) {
485             args.add("-deprecation");
486 
487             // This is required to actually display the deprecation messages
488             config.setShowWarnings(true);
489         }
490 
491         if (!config.isShowWarnings()) {
492             args.add("-nowarn");
493         } else {
494             String warnings = config.getWarnings();
495             if (config.isShowLint()) {
496                 if (config.isShowWarnings() && StringUtils.isNotEmpty(warnings)) {
497                     args.add("-Xlint:" + warnings);
498                 } else {
499                     args.add("-Xlint");
500                 }
501             }
502         }
503 
504         if (config.isFailOnWarning()) {
505             args.add("-Werror");
506         }
507 
508         if (JavaVersion.JAVA_9.isOlderOrEqualTo(javacVersion) && !StringUtils.isEmpty(config.getReleaseVersion())) {
509             args.add("--release");
510             args.add(toReleaseArgument(config.getReleaseVersion()));
511         } else {
512             // TODO: this could be much improved
513             if (StringUtils.isEmpty(config.getTargetVersion())) {
514                 // Required, or it defaults to the target of your JDK (eg 1.5)
515                 args.add("-target");
516                 args.add("1.1");
517             } else {
518                 args.add("-target");
519                 args.add(config.getTargetVersion());
520             }
521 
522             if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion) && StringUtils.isEmpty(config.getSourceVersion())) {
523                 // If omitted, later JDKs complain about a 1.1 target
524                 args.add("-source");
525                 args.add("1.3");
526             } else if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion)) {
527                 args.add("-source");
528                 args.add(config.getSourceVersion());
529             }
530         }
531 
532         if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion) && !StringUtils.isEmpty(config.getSourceEncoding())) {
533             args.add("-encoding");
534             args.add(config.getSourceEncoding());
535         }
536 
537         if (!StringUtils.isEmpty(config.getModuleVersion())) {
538             args.add("--module-version");
539             args.add(config.getModuleVersion());
540         }
541 
542         for (Map.Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
543             String key = entry.getKey();
544 
545             if (StringUtils.isEmpty(key) || key.startsWith("-J")) {
546                 continue;
547             }
548 
549             args.add(key);
550             String value = entry.getValue();
551             if (StringUtils.isEmpty(value)) {
552                 continue;
553             }
554             args.add(value);
555         }
556 
557         if (!config.isFork() && !args.contains("-XDuseUnsharedTable=false")) {
558             args.add("-XDuseUnsharedTable=true");
559         }
560 
561         return args.toArray(new String[0]);
562     }
563 
564     /**
565      * Represents a particular Java version (through their according version prefixes)
566      */
567     enum JavaVersion {
568         JAVA_1_3_OR_OLDER("1.3", "1.2", "1.1", "1.0"),
569         JAVA_1_4("1.4"),
570         JAVA_1_5("1.5"),
571         JAVA_1_6("1.6"),
572         JAVA_1_7("1.7"),
573         JAVA_1_8("1.8"),
574         JAVA_9("9"); // since Java 9 a different versioning scheme was used (https://openjdk.org/jeps/223)
575         final Set<String> versionPrefixes;
576 
577         JavaVersion(String... versionPrefixes) {
578             this.versionPrefixes = new HashSet<>(Arrays.asList(versionPrefixes));
579         }
580 
581         /**
582          * The internal logic checks if the given version starts with the prefix of one of the enums preceding the current one.
583          *
584          * @param version the version to check
585          * @return {@code true} if the version represented by this enum is older than or equal (in its minor and major version) to a given version
586          */
587         boolean isOlderOrEqualTo(String version) {
588             // go through all previous enums
589             JavaVersion[] allJavaVersionPrefixes = JavaVersion.values();
590             for (int n = ordinal() - 1; n > -1; n--) {
591                 if (allJavaVersionPrefixes[n].versionPrefixes.stream().anyMatch(version::startsWith)) {
592                     return false;
593                 }
594             }
595             return true;
596         }
597     }
598 
599     /**
600      * Compile the java sources in a external process, calling an external executable,
601      * like javac.
602      *
603      * @param config     compiler configuration
604      * @param executable name of the executable to launch
605      * @param args       arguments for the executable launched
606      * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages
607      * @throws CompilerException
608      */
609     protected CompilerResult compileOutOfProcess(CompilerConfiguration config, String executable, String[] args)
610             throws CompilerException {
611         Commandline cli = new Commandline();
612 
613         cli.setWorkingDirectory(config.getWorkingDirectory().getAbsolutePath());
614         cli.setExecutable(executable);
615 
616         try {
617             File argumentsFile =
618                     createFileWithArguments(args, config.getBuildDirectory().getAbsolutePath());
619             cli.addArguments(
620                     new String[] {"@" + argumentsFile.getCanonicalPath().replace(File.separatorChar, '/')});
621 
622             if (!StringUtils.isEmpty(config.getMaxmem())) {
623                 cli.addArguments(new String[] {"-J-Xmx" + config.getMaxmem()});
624             }
625             if (!StringUtils.isEmpty(config.getMeminitial())) {
626                 cli.addArguments(new String[] {"-J-Xms" + config.getMeminitial()});
627             }
628 
629             for (String key : config.getCustomCompilerArgumentsAsMap().keySet()) {
630                 if (StringUtils.isNotEmpty(key) && key.startsWith("-J")) {
631                     cli.addArguments(new String[] {key});
632                 }
633             }
634         } catch (IOException e) {
635             throw new CompilerException("Error creating file with javac arguments", e);
636         }
637 
638         CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
639         int returnCode;
640         List<CompilerMessage> messages;
641 
642         if (getLog().isDebugEnabled()) {
643             String debugFileName = StringUtils.isEmpty(config.getDebugFileName()) ? "javac" : config.getDebugFileName();
644 
645             File commandLineFile = new File(
646                     config.getBuildDirectory(),
647                     StringUtils.trim(debugFileName) + "." + (Os.isFamily(Os.FAMILY_WINDOWS) ? "bat" : "sh"));
648             try {
649                 FileUtils.fileWrite(
650                         commandLineFile.getAbsolutePath(), cli.toString().replaceAll("'", ""));
651 
652                 if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
653                     Runtime.getRuntime().exec(new String[] {"chmod", "a+x", commandLineFile.getAbsolutePath()});
654                 }
655             } catch (IOException e) {
656                 if (getLog().isWarnEnabled()) {
657                     getLog().warn("Unable to write '" + commandLineFile.getName() + "' debug script file", e);
658                 }
659             }
660         }
661 
662         try {
663             // TODO:
664             //   Is it really helpful to parse stdOut and stdErr as a single stream, instead of taking the chance to
665             //   draw extra information from the fact that normal javac output is written to stdOut, while warnings and
666             //   errors are written to stdErr? Of course, chronological correlation of messages would be more difficult
667             //   then, but basically, we are throwing away information here.
668             returnCode = CommandLineUtils.executeCommandLine(cli, out, out);
669 
670             if (getLog().isDebugEnabled()) {
671                 getLog().debug("Compiler output:{}{}", EOL, out.getOutput());
672             }
673 
674             messages = parseModernStream(returnCode, new BufferedReader(new StringReader(out.getOutput())));
675         } catch (CommandLineException | IOException e) {
676             throw new CompilerException("Error while executing the external compiler.", e);
677         }
678 
679         boolean success = returnCode == 0;
680         return new CompilerResult(success, messages);
681     }
682 
683     /**
684      * Compile the java sources in the current JVM, without calling an external executable,
685      * using <code>com.sun.tools.javac.Main</code> class
686      *
687      * @param args   arguments for the compiler as they would be used in the command line javac
688      * @param config compiler configuration
689      * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages
690      * @throws CompilerException
691      */
692     CompilerResult compileInProcess(String[] args, CompilerConfiguration config) throws CompilerException {
693         final Class<?> javacClass = getJavacClass(config);
694         final Thread thread = Thread.currentThread();
695         final ClassLoader contextClassLoader = thread.getContextClassLoader();
696         thread.setContextClassLoader(javacClass.getClassLoader());
697         if (getLog().isDebugEnabled()) {
698             getLog().debug("ttcl changed run compileInProcessWithProperClassloader");
699         }
700         try {
701             return compileInProcessWithProperClassloader(javacClass, args);
702         } finally {
703             releaseJavaccClass(javacClass, config);
704             thread.setContextClassLoader(contextClassLoader);
705         }
706     }
707 
708     protected CompilerResult compileInProcessWithProperClassloader(Class<?> javacClass, String[] args)
709             throws CompilerException {
710         return compileInProcess0(javacClass, args);
711     }
712 
713     /**
714      * Helper method for compileInProcess()
715      */
716     private CompilerResult compileInProcess0(Class<?> javacClass, String[] args) throws CompilerException {
717         StringWriter out = new StringWriter();
718         Integer ok;
719         List<CompilerMessage> messages;
720 
721         try {
722             Method compile = javacClass.getMethod("compile", new Class[] {String[].class, PrintWriter.class});
723             ok = (Integer) compile.invoke(null, new Object[] {args, new PrintWriter(out)});
724 
725             if (getLog().isDebugEnabled()) {
726                 getLog().debug("Compiler output:{}{}", EOL, out.toString());
727             }
728 
729             messages = parseModernStream(ok, new BufferedReader(new StringReader(out.toString())));
730         } catch (NoSuchMethodException | IOException | InvocationTargetException | IllegalAccessException e) {
731             throw new CompilerException("Error while executing the compiler.", e);
732         }
733 
734         boolean success = ok == 0;
735         return new CompilerResult(success, messages);
736     }
737 
738     // Match ~95% of existing JDK exception name patterns (last checked for JDK 21)
739     private static final Pattern STACK_TRACE_FIRST_LINE = Pattern.compile("^(?:[\\w+.-]+\\.)[\\w$]*?(?:"
740             + "Exception|Error|Throwable|Failure|Result|Abort|Fault|ThreadDeath|Overflow|Warning|"
741             + "NotSupported|NotFound|BadArgs|BadClassFile|Illegal|Invalid|Unexpected|Unchecked|Unmatched\\w+"
742             + ").*$");
743 
744     // Match exception causes, existing and omitted stack trace elements
745     /**
746      * A line that is only a tally, such as {@code 2 errors}, {@code 1 error} or the localised {@code 警告 1 個}.
747      */
748     private static final Pattern COUNT_SUMMARY = Pattern.compile("^\\s*(?:\\d+\\s+\\S+|\\S+\\s+\\d+\\s+\\S+)\\s*$");
749 
750     private static final Pattern STACK_TRACE_OTHER_LINE =
751             Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$");
752 
753     /**
754      * Parse the compiler output into a list of compiler messages
755      *
756      * @param exitCode javac exit code (0 on success, non-zero otherwise)
757      * @param input    compiler output (stdOut and stdErr merged into input stream)
758      * @return list of {@link CompilerMessage} objects
759      * @throws IOException if there is a problem reading from the input reader
760      */
761     static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader input) throws IOException {
762         List<CompilerMessage> errors = new ArrayList<>();
763         String line;
764         StringBuilder buffer = new StringBuilder();
765         StringBuilder note = null;
766         boolean hasPointer = false;
767         int stackTraceLineCount = 0;
768 
769         while ((line = input.readLine()) != null) {
770             if (note != null) {
771                 if (isNoteContinuation(line)) {
772                     note.append(EOL).append(line);
773                     continue;
774                 }
775                 errors.add(new CompilerMessage(note.toString(), CompilerMessage.Kind.NOTE));
776                 note = null;
777             }
778 
779             if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE.matcher(line).matches()
780                     || STACK_TRACE_OTHER_LINE.matcher(line).matches()) {
781                 stackTraceLineCount++;
782             } else {
783                 stackTraceLineCount = 0;
784             }
785 
786             // new error block?
787             if (!line.startsWith(" ") && hasPointer) {
788                 // add the error bean
789                 errors.add(parseModernError(exitCode, buffer.toString()));
790                 // reset for next error block
791                 buffer = new StringBuilder(); // this is quicker than clearing it
792                 hasPointer = false;
793             }
794 
795             if (buffer.length() == 0) {
796                 // try to classify output line by type (error, warning etc.)
797                 // TODO: there should be a better way to parse these
798                 if (isError(line)) {
799                     errors.add(new CompilerMessage(line, ERROR));
800                 } else if (isWarning(line)) {
801                     errors.add(new CompilerMessage(line, WARNING));
802                 } else if (isNote(line)) {
803                     // held back until its continuation lines, if any, have been read
804                     note = new StringBuilder(line);
805                 } else if (isMisc(line)) {
806                     // verbose output was set
807                     errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER));
808                 } else {
809                     // add first unclassified line to buffer
810                     buffer.append(line).append(EOL);
811                 }
812             } else {
813                 // add next unclassified line to buffer
814                 buffer.append(line).append(EOL);
815             }
816 
817             if (line.endsWith("^")) {
818                 hasPointer = true;
819             }
820         }
821 
822         if (note != null) {
823             errors.add(new CompilerMessage(note.toString(), CompilerMessage.Kind.NOTE));
824         }
825 
826         String bufferContent = buffer.toString();
827         if (bufferContent.isEmpty()) {
828             return errors;
829         }
830 
831         // javac output not detected by other parsing
832         // maybe better to ignore only the summary and mark the rest as error
833         String cleanedUpMessage;
834         if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null
835                 || (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null
836                 || (cleanedUpMessage = getVMInitError(bufferContent)) != null
837                 || (cleanedUpMessage = getFileABugError(bufferContent)) != null
838                 || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null
839                 || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null
840                 || (cleanedUpMessage = getIOError(bufferContent)) != null
841                 || (cleanedUpMessage = getPluginError(bufferContent)) != null) {
842             errors.add(new CompilerMessage(cleanedUpMessage, ERROR));
843         } else if (hasPointer) {
844             // A compiler message remains in buffer at end of parse stream
845             errors.add(parseModernError(exitCode, bufferContent));
846         } else if (stackTraceLineCount > 0) {
847             // Extract stack trace from end of buffer
848             String[] lines = bufferContent.split("\\R");
849             int linesTotal = lines.length;
850             buffer = new StringBuilder();
851             int firstLine = linesTotal - stackTraceLineCount;
852             for (int i = firstLine; i < linesTotal; i++) {
853                 buffer.append(lines[i]).append(EOL);
854             }
855             errors.add(new CompilerMessage(buffer.toString(), ERROR));
856         } else if (exitCode != 0) {
857             // Nothing in the buffer was recognised, yet the compiler failed. Whatever is left is reported rather
858             // than dropped, so that a failing build is never left without an explanation.
859             String unrecognised = stripCountSummaries(bufferContent);
860             if (!unrecognised.isEmpty()) {
861                 errors.add(new CompilerMessage(unrecognised, ERROR));
862             }
863         }
864 
865         return errors;
866     }
867 
868     /**
869      * Drops javac's trailing tallies, such as {@code 2 errors} or {@code 警告 1 個}, from otherwise unrecognised
870      * output. They repeat what the parsed messages already say, so on their own they are not worth reporting.
871      *
872      * @param content the unrecognised output
873      * @return the same content without its count lines, trimmed
874      */
875     private static String stripCountSummaries(String content) {
876         StringBuilder kept = new StringBuilder();
877         for (String line : content.split("\\R")) {
878             if (!line.trim().isEmpty() && !COUNT_SUMMARY.matcher(line).matches()) {
879                 kept.append(line).append(EOL);
880             }
881         }
882         return kept.toString().trim();
883     }
884 
885     private static boolean isMisc(String message) {
886         return startsWithPrefix(message, MISC_PREFIXES);
887     }
888 
889     private static boolean isNote(String message) {
890         return startsWithPrefix(message, NOTE_PREFIXES);
891     }
892 
893     /**
894      * Tells whether a line continues the note that precedes it. Since JDK 21 javac wraps its notes over several
895      * lines, indenting every line but the first, while the diagnostics that may follow a note all start in the
896      * first column.
897      * <p>
898      * A leading space, not leading whitespace: javac indents note continuations with spaces, whereas a leading tab
899      * marks a stack trace frame, which {@link #STACK_TRACE_OTHER_LINE} is waiting for.
900      *
901      * @param line the line following a note
902      * @return whether the line belongs to that note
903      */
904     private static boolean isNoteContinuation(String line) {
905         return line.startsWith(" ");
906     }
907 
908     private static boolean isWarning(String message) {
909         return startsWithPrefix(message, WARNING_PREFIXES);
910     }
911 
912     private static boolean isError(String message) {
913         return startsWithPrefix(message, ERROR_PREFIXES);
914     }
915 
916     private static String getJavacGenericError(String message) {
917         return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES);
918     }
919 
920     private static String getVMInitError(String message) {
921         return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS);
922     }
923 
924     private static String getBootLayerInitError(String message) {
925         return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS);
926     }
927 
928     private static String getFileABugError(String message) {
929         return getTextStartingWithPrefix(message, FILE_A_BUG_ERROR_HEADERS);
930     }
931 
932     private static String getAnnotationProcessingError(String message) {
933         return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS);
934     }
935 
936     private static String getSystemOutOfResourcesError(String message) {
937         return getTextStartingWithPrefix(message, SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS);
938     }
939 
940     private static String getIOError(String message) {
941         return getTextStartingWithPrefix(message, IO_ERROR_HEADERS);
942     }
943 
944     private static String getPluginError(String message) {
945         return getTextStartingWithPrefix(message, PLUGIN_ERROR_HEADERS);
946     }
947 
948     private static boolean startsWithPrefix(String text, String[] prefixes) {
949         for (String prefix : prefixes) {
950             if (text.startsWith(prefix)) {
951                 return true;
952             }
953         }
954         return false;
955     }
956 
957     /**
958      * Identify and return a known javac error message prefix and all subsequent text - usually a stack trace - from a
959      * javac log output buffer.
960      *
961      * @param text     log buffer to search for a javac error message stack trace
962      * @param prefixes array of strings in Java properties format, e.g. {@code "some error with line feed\nand parameter
963      *                 placeholders {0} and {1}"} in multiple locales (hence the array). For the search, the
964      *                 placeholders may be represented by any text in the log buffer.
965      * @return if found, the error message + all subsequent text, otherwise {@code null}
966      */
967     static String getTextStartingWithPrefix(String text, String[] prefixes) {
968         // Implementation note: The properties format with placeholders  makes it easy to just copy & paste values from
969         // the JDK compared to having to convert them to regular expressions with ".*" instead of "{0}" and quote
970         // special regex characters. This makes the implementation of this method more complex and potentially a bit
971         // slower, but hopefully is worth the effort for the convenience of future developers maintaining this class.
972 
973         // Normalise line feeds to the UNIX format found in JDK multi-line messages in properties files
974         text = text.replaceAll("\\R", "\n");
975 
976         // Search text for given error message prefixes/headers, until the first match is found
977         for (String prefix : prefixes) {
978             // Split properties message along placeholders like "{0}", "{1}" etc.
979             String[] prefixParts = prefix.split("\\{\\d+\\}");
980             for (int i = 0; i < prefixParts.length; i++) {
981                 // Make sure to treat split sections as literal text in search regex by enclosing them in "\Q" and "\E".
982                 // See https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, search for "Quotation".
983                 prefixParts[i] = "\\Q" + prefixParts[i] + "\\E";
984             }
985             // Join message parts, replacing properties placeholders by ".*" regex ones
986             prefix = String.join(".*?", prefixParts);
987             // Find prefix + subsequent text in Pattern.DOTALL mode, represented in regex as "(?s)".
988             // This matches across line break boundaries.
989             Matcher matcher = Pattern.compile("(?s).*(" + prefix + ".*)").matcher(text);
990             if (matcher.matches()) {
991                 // Match -> cut off text before header and replace UNIX line breaks by platform ones again
992                 return matcher.replaceFirst("$1").replaceAll("\n", EOL);
993             }
994         }
995 
996         // No match
997         return null;
998     }
999 
1000     /**
1001      * Construct a compiler message object from a compiler output line
1002      *
1003      * @param exitCode javac exit code
1004      * @param error    compiler output line
1005      * @return compiler message object
1006      */
1007     static CompilerMessage parseModernError(int exitCode, String error) {
1008         final StringTokenizer tokens = new StringTokenizer(error, ":");
1009         CompilerMessage.Kind messageKind = exitCode == 0 ? WARNING : ERROR;
1010 
1011         try {
1012             // With Java 6 error output lines from the compiler got longer. For backward compatibility
1013             // and the time being, we eat up all (if any) tokens up to the erroneous file and source
1014             // line indicator tokens.
1015 
1016             boolean tokenIsAnInteger;
1017             StringBuilder file = null;
1018             String currentToken = null;
1019 
1020             do {
1021                 if (currentToken != null) {
1022                     if (file == null) {
1023                         file = new StringBuilder(currentToken);
1024                     } else {
1025                         file.append(':').append(currentToken);
1026                     }
1027                 }
1028 
1029                 currentToken = tokens.nextToken();
1030                 // Probably the only backward compatible means of checking if a string is an integer.
1031                 tokenIsAnInteger = true;
1032 
1033                 try {
1034                     Integer.parseInt(currentToken);
1035                 } catch (NumberFormatException e) {
1036                     tokenIsAnInteger = false;
1037                 }
1038             } while (!tokenIsAnInteger);
1039 
1040             final String lineIndicator = currentToken;
1041             final int startOfFileName = Objects.requireNonNull(file).toString().lastIndexOf(']');
1042             if (startOfFileName > -1) {
1043                 file = new StringBuilder(file.substring(startOfFileName + 1 + EOL.length()));
1044             }
1045 
1046             final int line = Integer.parseInt(lineIndicator);
1047             final StringBuilder msgBuffer = new StringBuilder();
1048             String msg = tokens.nextToken(EOL).substring(2);
1049 
1050             // Remove "error: " and "warning: " prefixes
1051             String prefix;
1052             if ((prefix = getErrorPrefix(msg)) != null) {
1053                 messageKind = ERROR;
1054                 msg = msg.substring(prefix.length());
1055             } else if ((prefix = getWarningPrefix(msg)) != null) {
1056                 messageKind = WARNING;
1057                 msg = msg.substring(prefix.length());
1058             }
1059             msgBuffer.append(msg).append(EOL);
1060 
1061             String context = tokens.nextToken(EOL);
1062             String pointer = null;
1063 
1064             do {
1065                 final String msgLine = tokens.nextToken(EOL);
1066                 if (pointer != null) {
1067                     msgBuffer.append(msgLine);
1068                     msgBuffer.append(EOL);
1069                 } else if (msgLine.endsWith("^")) {
1070                     pointer = msgLine;
1071                 } else {
1072                     msgBuffer.append(context);
1073                     msgBuffer.append(EOL);
1074                     context = msgLine;
1075                 }
1076             } while (tokens.hasMoreTokens());
1077 
1078             msgBuffer.append(EOL);
1079 
1080             final String message = msgBuffer.toString();
1081             final int startcolumn = Objects.requireNonNull(pointer).indexOf("^") + 1;
1082             int endcolumn = (context == null) ? startcolumn : context.indexOf(" ", startcolumn);
1083             if (endcolumn == -1) {
1084                 endcolumn = Objects.requireNonNull(context).length();
1085             }
1086 
1087             return new CompilerMessage(
1088                     file.toString(), messageKind, line, startcolumn, line, endcolumn, message.trim());
1089         } catch (NoSuchElementException e) {
1090             return new CompilerMessage("no more tokens - could not parse error message: " + error, messageKind);
1091         } catch (Exception e) {
1092             return new CompilerMessage("could not parse error message: " + error, messageKind);
1093         }
1094     }
1095 
1096     private static String getMessagePrefix(String message, String[] prefixes) {
1097         for (String prefix : prefixes) {
1098             if (message.startsWith(prefix)) {
1099                 return prefix;
1100             }
1101         }
1102         return null;
1103     }
1104 
1105     private static String getWarningPrefix(String message) {
1106         return getMessagePrefix(message, WARNING_PREFIXES);
1107     }
1108 
1109     private static String getErrorPrefix(String message) {
1110         return getMessagePrefix(message, ERROR_PREFIXES);
1111     }
1112 
1113     /**
1114      * put args into a temp file to be referenced using the @ option in javac command line
1115      *
1116      * @param args
1117      * @return the temporary file wth the arguments
1118      * @throws IOException
1119      */
1120     private File createFileWithArguments(String[] args, String outputDirectory) throws IOException {
1121         PrintWriter writer = null;
1122         try {
1123             File tempFile;
1124             if (getLog().isDebugEnabled()) {
1125                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments", new File(outputDirectory));
1126             } else {
1127                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments");
1128                 tempFile.deleteOnExit();
1129             }
1130 
1131             writer = new PrintWriter(new FileWriter(tempFile));
1132             for (String arg : args) {
1133                 writer.println(quoteArgument(arg));
1134             }
1135             writer.flush();
1136 
1137             return tempFile;
1138 
1139         } finally {
1140             if (writer != null) {
1141                 writer.close();
1142             }
1143         }
1144     }
1145 
1146     /**
1147      * Quotes an argument according to javac command-line argument file syntax.
1148      */
1149     static String quoteArgument(String argument) {
1150         StringBuilder quoted = new StringBuilder(argument.length() + 2);
1151         quoted.append('"');
1152         for (int i = 0; i < argument.length(); i++) {
1153             char c = argument.charAt(i);
1154             switch (c) {
1155                 case '\\':
1156                     quoted.append("\\\\");
1157                     break;
1158                 case '"':
1159                     quoted.append("\\\"");
1160                     break;
1161                 case '\n':
1162                     quoted.append("\\n");
1163                     break;
1164                 case '\r':
1165                     quoted.append("\\r");
1166                     break;
1167                 case '\t':
1168                     quoted.append("\\t");
1169                     break;
1170                 case '\f':
1171                     quoted.append("\\f");
1172                     break;
1173                 default:
1174                     quoted.append(c);
1175                     break;
1176             }
1177         }
1178         return quoted.append('"').toString();
1179     }
1180 
1181     /**
1182      * Get the path of the javac tool executable to use.
1183      * Either given through explicit configuration or via {@link #getJavacExecutable()}.
1184      * @param config the configuration
1185      * @return the path of the javac tool
1186      */
1187     protected String getJavacExecutable(CompilerConfiguration config) {
1188         String executable = config.getExecutable();
1189 
1190         if (StringUtils.isEmpty(executable)) {
1191             try {
1192                 executable = getJavacExecutable();
1193             } catch (IOException e) {
1194                 if (getLog().isWarnEnabled()) {
1195                     getLog().warn("Unable to autodetect 'javac' path, using 'javac' from the environment.");
1196                 }
1197                 executable = "javac";
1198             }
1199         }
1200         return executable;
1201     }
1202 
1203     /**
1204      * Get the path of the javac tool executable: try to find it depending the OS or the <code>java.home</code>
1205      * system property or the <code>JAVA_HOME</code> environment variable.
1206      *
1207      * @return the path of the javac tool
1208      * @throws IOException if not found
1209      */
1210     private static String getJavacExecutable() throws IOException {
1211         String javacCommand = "javac" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : "");
1212         String javaHome = System.getProperty("java.home");
1213         File javacExe;
1214 
1215         if (Os.isName("AIX")) {
1216             javacExe = new File(javaHome + File.separator + ".." + File.separator + "sh", javacCommand);
1217         } else if (Os.isName("Mac OS X")) {
1218             javacExe = new File(javaHome + File.separator + "bin", javacCommand);
1219         } else {
1220             javacExe = new File(javaHome + File.separator + ".." + File.separator + "bin", javacCommand);
1221         }
1222 
1223         // ----------------------------------------------------------------------
1224         // Try to find javacExe from JAVA_HOME environment variable
1225         // ----------------------------------------------------------------------
1226         if (!javacExe.isFile()) {
1227             Properties env = CommandLineUtils.getSystemEnvVars();
1228             javaHome = env.getProperty("JAVA_HOME");
1229             if (StringUtils.isEmpty(javaHome)) {
1230                 throw new IOException("The environment variable JAVA_HOME is not correctly set.");
1231             }
1232             if (!new File(javaHome).isDirectory()) {
1233                 throw new IOException("The environment variable JAVA_HOME=" + javaHome
1234                         + " doesn't exist or is not a valid directory.");
1235             }
1236             javacExe = new File(env.getProperty("JAVA_HOME") + File.separator + "bin", javacCommand);
1237         }
1238 
1239         if (!javacExe.isFile()) {
1240             throw new IOException("The javadoc executable '" + javacExe
1241                     + "' doesn't exist or is not a file. Verify the JAVA_HOME environment variable.");
1242         }
1243 
1244         return javacExe.getAbsolutePath();
1245     }
1246 
1247     private void releaseJavaccClass(Class<?> javaccClass, CompilerConfiguration compilerConfiguration) {
1248         if (compilerConfiguration.getCompilerReuseStrategy()
1249                 == CompilerConfiguration.CompilerReuseStrategy.ReuseCreated) {
1250             javacClasses.add(javaccClass);
1251         }
1252     }
1253 
1254     /**
1255      * Find the main class of JavaC. Return the same class for subsequent calls.
1256      *
1257      * @return the non-null class.
1258      * @throws CompilerException if the class has not been found.
1259      */
1260     private Class<?> getJavacClass(CompilerConfiguration compilerConfiguration) throws CompilerException {
1261         Class<?> c;
1262         switch (compilerConfiguration.getCompilerReuseStrategy()) {
1263             case AlwaysNew:
1264                 return createJavacClass();
1265             case ReuseCreated:
1266                 c = javacClasses.poll();
1267                 if (c == null) {
1268                     c = createJavacClass();
1269                 }
1270                 return c;
1271             case ReuseSame:
1272             default:
1273                 c = javacClass;
1274                 if (c == null) {
1275                     synchronized (this) {
1276                         c = javacClass;
1277                         if (c == null) {
1278                             javacClass = c = createJavacClass();
1279                         }
1280                     }
1281                 }
1282                 return c;
1283         }
1284     }
1285 
1286     /**
1287      * Helper method for create Javac class
1288      */
1289     protected Class<?> createJavacClass() throws CompilerException {
1290         try {
1291             // look whether JavaC is on Maven's classpath
1292             // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, JavacCompiler.class.getClassLoader() );
1293             return JavacCompiler.class.getClassLoader().loadClass(JavacCompiler.JAVAC_CLASSNAME);
1294         } catch (ClassNotFoundException ex) {
1295             // ok
1296         }
1297 
1298         final File toolsJar = new File(System.getProperty("java.home"), "../lib/tools.jar");
1299         if (!toolsJar.exists()) {
1300             throw new CompilerException("tools.jar not found: " + toolsJar);
1301         }
1302 
1303         try {
1304             // Combined classloader with no parent/child relationship, so classes in our classloader
1305             // can reference classes in tools.jar
1306             URL[] originalUrls = ((URLClassLoader) JavacCompiler.class.getClassLoader()).getURLs();
1307             URL[] urls = new URL[originalUrls.length + 1];
1308             urls[0] = toolsJar.toURI().toURL();
1309             System.arraycopy(originalUrls, 0, urls, 1, originalUrls.length);
1310             ClassLoader javacClassLoader = new URLClassLoader(urls);
1311 
1312             final Thread thread = Thread.currentThread();
1313             final ClassLoader contextClassLoader = thread.getContextClassLoader();
1314             thread.setContextClassLoader(javacClassLoader);
1315             try {
1316                 // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, javacClassLoader );
1317                 return javacClassLoader.loadClass(JavacCompiler.JAVAC_CLASSNAME);
1318             } finally {
1319                 thread.setContextClassLoader(contextClassLoader);
1320             }
1321         } catch (MalformedURLException ex) {
1322             throw new CompilerException(
1323                     "Could not convert the file reference to tools.jar to a URL, path to tools.jar: '"
1324                             + toolsJar.getAbsolutePath() + "'.",
1325                     ex);
1326         } catch (ClassNotFoundException ex) {
1327             throw new CompilerException(
1328                     "Unable to locate the Javac Compiler in:" + EOL + "  " + toolsJar + EOL
1329                             + "Please ensure you are using JDK 1.4 or above and" + EOL
1330                             + "not a JRE (the com.sun.tools.javac.Main class is required)." + EOL
1331                             + "In most cases you can change the location of your Java" + EOL
1332                             + "installation by setting the JAVA_HOME environment variable.",
1333                     ex);
1334         }
1335     }
1336 }