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      *
352      * @return {@code true} if the current context class loader has access to {@code javax.tools.ToolProvider}
353      */
354     protected static boolean hasJavaxToolProvider() {
355         try {
356             Thread.currentThread().getContextClassLoader().loadClass("javax.tools.ToolProvider");
357             return true;
358         } catch (Exception e) {
359             return false;
360         }
361     }
362 
363     public String[] createCommandLine(CompilerConfiguration config) throws CompilerException {
364         final String javacVersion;
365         if (config.isFork()) {
366             String executable = getJavacExecutable(config);
367             javacVersion = getOutOfProcessJavacVersion(executable);
368         } else {
369             javacVersion = getInProcessJavacVersion();
370         }
371         return buildCompilerArguments(config, getSourceFiles(config), javacVersion);
372     }
373 
374     public static String[] buildCompilerArguments(
375             CompilerConfiguration config, String[] sourceFiles, String javacVersion) {
376         List<String> args = new ArrayList<>();
377 
378         // ----------------------------------------------------------------------
379         // Set output
380         // ----------------------------------------------------------------------
381 
382         File destinationDir = new File(config.getOutputLocation());
383         args.add("-d");
384         args.add(destinationDir.getAbsolutePath());
385 
386         // ----------------------------------------------------------------------
387         // Set the class and source paths
388         // ----------------------------------------------------------------------
389 
390         List<String> classpathEntries = config.getClasspathEntries();
391         if (classpathEntries != null && !classpathEntries.isEmpty()) {
392             args.add("-classpath");
393             args.add(getPathString(classpathEntries));
394         }
395 
396         List<String> modulepathEntries = config.getModulepathEntries();
397         if (modulepathEntries != null && !modulepathEntries.isEmpty()) {
398             args.add("--module-path");
399             args.add(getPathString(modulepathEntries));
400         }
401 
402         List<String> sourceLocations = config.getSourceLocations();
403         if (sourceLocations != null && !sourceLocations.isEmpty()) {
404             // always pass source path, even if sourceFiles are declared,
405             // needed for jsr269 annotation processing, see MCOMPILER-98
406             args.add("-sourcepath");
407             args.add(getPathString(sourceLocations));
408         }
409         if (!hasJavaxToolProvider() || config.isForceJavacCompilerUse() || config.isFork()) {
410             args.addAll(Arrays.asList(sourceFiles));
411         }
412 
413         if (JavaVersion.JAVA_1_6.isOlderOrEqualTo(javacVersion)) {
414             // now add jdk 1.6 annotation processing related parameters
415 
416             if (config.getGeneratedSourcesDirectory() != null) {
417                 config.getGeneratedSourcesDirectory().mkdirs();
418                 args.add("-s");
419                 args.add(config.getGeneratedSourcesDirectory().getAbsolutePath());
420             }
421             if (config.getProc() != null) {
422                 args.add("-proc:" + config.getProc());
423             }
424             String annotationProcessors = joinAnnotationProcessors(config.getAnnotationProcessors());
425             if (!annotationProcessors.isEmpty()) {
426                 args.add("-processor");
427                 args.add(annotationProcessors);
428             }
429             if (config.getProcessorPathEntries() != null
430                     && !config.getProcessorPathEntries().isEmpty()) {
431                 args.add("-processorpath");
432                 args.add(getPathString(config.getProcessorPathEntries()));
433             }
434             if (config.getProcessorModulePathEntries() != null
435                     && !config.getProcessorModulePathEntries().isEmpty()) {
436                 args.add("--processor-module-path");
437                 args.add(getPathString(config.getProcessorModulePathEntries()));
438             }
439         }
440 
441         if (config.isOptimize()) {
442             args.add("-O");
443         }
444 
445         if (config.isDebug()) {
446             if (StringUtils.isNotEmpty(config.getDebugLevel())) {
447                 args.add("-g:" + config.getDebugLevel());
448             } else {
449                 args.add("-g");
450             }
451         }
452 
453         if (config.isVerbose()) {
454             args.add("-verbose");
455         }
456 
457         if (JavaVersion.JAVA_1_8.isOlderOrEqualTo(javacVersion) && config.isParameters()) {
458             args.add("-parameters");
459         }
460 
461         if (config.isEnablePreview()) {
462             args.add("--enable-preview");
463         }
464 
465         if (config.getImplicitOption() != null) {
466             args.add("-implicit:" + config.getImplicitOption());
467         }
468 
469         if (config.isShowDeprecation()) {
470             args.add("-deprecation");
471 
472             // This is required to actually display the deprecation messages
473             config.setShowWarnings(true);
474         }
475 
476         if (!config.isShowWarnings()) {
477             args.add("-nowarn");
478         } else {
479             String warnings = config.getWarnings();
480             if (config.isShowLint()) {
481                 if (config.isShowWarnings() && StringUtils.isNotEmpty(warnings)) {
482                     args.add("-Xlint:" + warnings);
483                 } else {
484                     args.add("-Xlint");
485                 }
486             }
487         }
488 
489         if (config.isFailOnWarning()) {
490             args.add("-Werror");
491         }
492 
493         if (JavaVersion.JAVA_9.isOlderOrEqualTo(javacVersion) && !StringUtils.isEmpty(config.getReleaseVersion())) {
494             args.add("--release");
495             args.add(config.getReleaseVersion());
496         } else {
497             // TODO: this could be much improved
498             if (StringUtils.isEmpty(config.getTargetVersion())) {
499                 // Required, or it defaults to the target of your JDK (eg 1.5)
500                 args.add("-target");
501                 args.add("1.1");
502             } else {
503                 args.add("-target");
504                 args.add(config.getTargetVersion());
505             }
506 
507             if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion) && StringUtils.isEmpty(config.getSourceVersion())) {
508                 // If omitted, later JDKs complain about a 1.1 target
509                 args.add("-source");
510                 args.add("1.3");
511             } else if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion)) {
512                 args.add("-source");
513                 args.add(config.getSourceVersion());
514             }
515         }
516 
517         if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion) && !StringUtils.isEmpty(config.getSourceEncoding())) {
518             args.add("-encoding");
519             args.add(config.getSourceEncoding());
520         }
521 
522         if (!StringUtils.isEmpty(config.getModuleVersion())) {
523             args.add("--module-version");
524             args.add(config.getModuleVersion());
525         }
526 
527         for (Map.Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
528             String key = entry.getKey();
529 
530             if (StringUtils.isEmpty(key) || key.startsWith("-J")) {
531                 continue;
532             }
533 
534             args.add(key);
535             String value = entry.getValue();
536             if (StringUtils.isEmpty(value)) {
537                 continue;
538             }
539             args.add(value);
540         }
541 
542         if (!config.isFork() && !args.contains("-XDuseUnsharedTable=false")) {
543             args.add("-XDuseUnsharedTable=true");
544         }
545 
546         return args.toArray(new String[0]);
547     }
548 
549     /**
550      * Represents a particular Java version (through their according version prefixes)
551      */
552     enum JavaVersion {
553         JAVA_1_3_OR_OLDER("1.3", "1.2", "1.1", "1.0"),
554         JAVA_1_4("1.4"),
555         JAVA_1_5("1.5"),
556         JAVA_1_6("1.6"),
557         JAVA_1_7("1.7"),
558         JAVA_1_8("1.8"),
559         JAVA_9("9"); // since Java 9 a different versioning scheme was used (https://openjdk.org/jeps/223)
560         final Set<String> versionPrefixes;
561 
562         JavaVersion(String... versionPrefixes) {
563             this.versionPrefixes = new HashSet<>(Arrays.asList(versionPrefixes));
564         }
565 
566         /**
567          * The internal logic checks if the given version starts with the prefix of one of the enums preceding the current one.
568          *
569          * @param version the version to check
570          * @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
571          */
572         boolean isOlderOrEqualTo(String version) {
573             // go through all previous enums
574             JavaVersion[] allJavaVersionPrefixes = JavaVersion.values();
575             for (int n = ordinal() - 1; n > -1; n--) {
576                 if (allJavaVersionPrefixes[n].versionPrefixes.stream().anyMatch(version::startsWith)) {
577                     return false;
578                 }
579             }
580             return true;
581         }
582     }
583 
584     /**
585      * Compile the java sources in a external process, calling an external executable,
586      * like javac.
587      *
588      * @param config     compiler configuration
589      * @param executable name of the executable to launch
590      * @param args       arguments for the executable launched
591      * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages
592      * @throws CompilerException
593      */
594     protected CompilerResult compileOutOfProcess(CompilerConfiguration config, String executable, String[] args)
595             throws CompilerException {
596         Commandline cli = new Commandline();
597 
598         cli.setWorkingDirectory(config.getWorkingDirectory().getAbsolutePath());
599         cli.setExecutable(executable);
600 
601         try {
602             File argumentsFile =
603                     createFileWithArguments(args, config.getBuildDirectory().getAbsolutePath());
604             cli.addArguments(
605                     new String[] {"@" + argumentsFile.getCanonicalPath().replace(File.separatorChar, '/')});
606 
607             if (!StringUtils.isEmpty(config.getMaxmem())) {
608                 cli.addArguments(new String[] {"-J-Xmx" + config.getMaxmem()});
609             }
610             if (!StringUtils.isEmpty(config.getMeminitial())) {
611                 cli.addArguments(new String[] {"-J-Xms" + config.getMeminitial()});
612             }
613 
614             for (String key : config.getCustomCompilerArgumentsAsMap().keySet()) {
615                 if (StringUtils.isNotEmpty(key) && key.startsWith("-J")) {
616                     cli.addArguments(new String[] {key});
617                 }
618             }
619         } catch (IOException e) {
620             throw new CompilerException("Error creating file with javac arguments", e);
621         }
622 
623         CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
624         int returnCode;
625         List<CompilerMessage> messages;
626 
627         if (getLog().isDebugEnabled()) {
628             String debugFileName = StringUtils.isEmpty(config.getDebugFileName()) ? "javac" : config.getDebugFileName();
629 
630             File commandLineFile = new File(
631                     config.getBuildDirectory(),
632                     StringUtils.trim(debugFileName) + "." + (Os.isFamily(Os.FAMILY_WINDOWS) ? "bat" : "sh"));
633             try {
634                 FileUtils.fileWrite(
635                         commandLineFile.getAbsolutePath(), cli.toString().replaceAll("'", ""));
636 
637                 if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
638                     Runtime.getRuntime().exec(new String[] {"chmod", "a+x", commandLineFile.getAbsolutePath()});
639                 }
640             } catch (IOException e) {
641                 if (getLog().isWarnEnabled()) {
642                     getLog().warn("Unable to write '" + commandLineFile.getName() + "' debug script file", e);
643                 }
644             }
645         }
646 
647         try {
648             // TODO:
649             //   Is it really helpful to parse stdOut and stdErr as a single stream, instead of taking the chance to
650             //   draw extra information from the fact that normal javac output is written to stdOut, while warnings and
651             //   errors are written to stdErr? Of course, chronological correlation of messages would be more difficult
652             //   then, but basically, we are throwing away information here.
653             returnCode = CommandLineUtils.executeCommandLine(cli, out, out);
654 
655             if (getLog().isDebugEnabled()) {
656                 getLog().debug("Compiler output:{}{}", EOL, out.getOutput());
657             }
658 
659             messages = parseModernStream(returnCode, new BufferedReader(new StringReader(out.getOutput())));
660         } catch (CommandLineException | IOException e) {
661             throw new CompilerException("Error while executing the external compiler.", e);
662         }
663 
664         boolean success = returnCode == 0;
665         return new CompilerResult(success, messages);
666     }
667 
668     /**
669      * Compile the java sources in the current JVM, without calling an external executable,
670      * using <code>com.sun.tools.javac.Main</code> class
671      *
672      * @param args   arguments for the compiler as they would be used in the command line javac
673      * @param config compiler configuration
674      * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages
675      * @throws CompilerException
676      */
677     CompilerResult compileInProcess(String[] args, CompilerConfiguration config) throws CompilerException {
678         final Class<?> javacClass = getJavacClass(config);
679         final Thread thread = Thread.currentThread();
680         final ClassLoader contextClassLoader = thread.getContextClassLoader();
681         thread.setContextClassLoader(javacClass.getClassLoader());
682         if (getLog().isDebugEnabled()) {
683             getLog().debug("ttcl changed run compileInProcessWithProperClassloader");
684         }
685         try {
686             return compileInProcessWithProperClassloader(javacClass, args);
687         } finally {
688             releaseJavaccClass(javacClass, config);
689             thread.setContextClassLoader(contextClassLoader);
690         }
691     }
692 
693     protected CompilerResult compileInProcessWithProperClassloader(Class<?> javacClass, String[] args)
694             throws CompilerException {
695         return compileInProcess0(javacClass, args);
696     }
697 
698     /**
699      * Helper method for compileInProcess()
700      */
701     private CompilerResult compileInProcess0(Class<?> javacClass, String[] args) throws CompilerException {
702         StringWriter out = new StringWriter();
703         Integer ok;
704         List<CompilerMessage> messages;
705 
706         try {
707             Method compile = javacClass.getMethod("compile", new Class[] {String[].class, PrintWriter.class});
708             ok = (Integer) compile.invoke(null, new Object[] {args, new PrintWriter(out)});
709 
710             if (getLog().isDebugEnabled()) {
711                 getLog().debug("Compiler output:{}{}", EOL, out.toString());
712             }
713 
714             messages = parseModernStream(ok, new BufferedReader(new StringReader(out.toString())));
715         } catch (NoSuchMethodException | IOException | InvocationTargetException | IllegalAccessException e) {
716             throw new CompilerException("Error while executing the compiler.", e);
717         }
718 
719         boolean success = ok == 0;
720         return new CompilerResult(success, messages);
721     }
722 
723     // Match ~95% of existing JDK exception name patterns (last checked for JDK 21)
724     private static final Pattern STACK_TRACE_FIRST_LINE = Pattern.compile("^(?:[\\w+.-]+\\.)[\\w$]*?(?:"
725             + "Exception|Error|Throwable|Failure|Result|Abort|Fault|ThreadDeath|Overflow|Warning|"
726             + "NotSupported|NotFound|BadArgs|BadClassFile|Illegal|Invalid|Unexpected|Unchecked|Unmatched\\w+"
727             + ").*$");
728 
729     // Match exception causes, existing and omitted stack trace elements
730     /**
731      * A line that is only a tally, such as {@code 2 errors}, {@code 1 error} or the localised {@code 警告 1 個}.
732      */
733     private static final Pattern COUNT_SUMMARY = Pattern.compile("^\\s*(?:\\d+\\s+\\S+|\\S+\\s+\\d+\\s+\\S+)\\s*$");
734 
735     private static final Pattern STACK_TRACE_OTHER_LINE =
736             Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$");
737 
738     /**
739      * Parse the compiler output into a list of compiler messages
740      *
741      * @param exitCode javac exit code (0 on success, non-zero otherwise)
742      * @param input    compiler output (stdOut and stdErr merged into input stream)
743      * @return list of {@link CompilerMessage} objects
744      * @throws IOException if there is a problem reading from the input reader
745      */
746     static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader input) throws IOException {
747         List<CompilerMessage> errors = new ArrayList<>();
748         String line;
749         StringBuilder buffer = new StringBuilder();
750         StringBuilder note = null;
751         boolean hasPointer = false;
752         int stackTraceLineCount = 0;
753 
754         while ((line = input.readLine()) != null) {
755             if (note != null) {
756                 if (isNoteContinuation(line)) {
757                     note.append(EOL).append(line);
758                     continue;
759                 }
760                 errors.add(new CompilerMessage(note.toString(), CompilerMessage.Kind.NOTE));
761                 note = null;
762             }
763 
764             if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE.matcher(line).matches()
765                     || STACK_TRACE_OTHER_LINE.matcher(line).matches()) {
766                 stackTraceLineCount++;
767             } else {
768                 stackTraceLineCount = 0;
769             }
770 
771             // new error block?
772             if (!line.startsWith(" ") && hasPointer) {
773                 // add the error bean
774                 errors.add(parseModernError(exitCode, buffer.toString()));
775                 // reset for next error block
776                 buffer = new StringBuilder(); // this is quicker than clearing it
777                 hasPointer = false;
778             }
779 
780             if (buffer.length() == 0) {
781                 // try to classify output line by type (error, warning etc.)
782                 // TODO: there should be a better way to parse these
783                 if (isError(line)) {
784                     errors.add(new CompilerMessage(line, ERROR));
785                 } else if (isWarning(line)) {
786                     errors.add(new CompilerMessage(line, WARNING));
787                 } else if (isNote(line)) {
788                     // held back until its continuation lines, if any, have been read
789                     note = new StringBuilder(line);
790                 } else if (isMisc(line)) {
791                     // verbose output was set
792                     errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER));
793                 } else {
794                     // add first unclassified line to buffer
795                     buffer.append(line).append(EOL);
796                 }
797             } else {
798                 // add next unclassified line to buffer
799                 buffer.append(line).append(EOL);
800             }
801 
802             if (line.endsWith("^")) {
803                 hasPointer = true;
804             }
805         }
806 
807         if (note != null) {
808             errors.add(new CompilerMessage(note.toString(), CompilerMessage.Kind.NOTE));
809         }
810 
811         String bufferContent = buffer.toString();
812         if (bufferContent.isEmpty()) {
813             return errors;
814         }
815 
816         // javac output not detected by other parsing
817         // maybe better to ignore only the summary and mark the rest as error
818         String cleanedUpMessage;
819         if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null
820                 || (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null
821                 || (cleanedUpMessage = getVMInitError(bufferContent)) != null
822                 || (cleanedUpMessage = getFileABugError(bufferContent)) != null
823                 || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null
824                 || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null
825                 || (cleanedUpMessage = getIOError(bufferContent)) != null
826                 || (cleanedUpMessage = getPluginError(bufferContent)) != null) {
827             errors.add(new CompilerMessage(cleanedUpMessage, ERROR));
828         } else if (hasPointer) {
829             // A compiler message remains in buffer at end of parse stream
830             errors.add(parseModernError(exitCode, bufferContent));
831         } else if (stackTraceLineCount > 0) {
832             // Extract stack trace from end of buffer
833             String[] lines = bufferContent.split("\\R");
834             int linesTotal = lines.length;
835             buffer = new StringBuilder();
836             int firstLine = linesTotal - stackTraceLineCount;
837             for (int i = firstLine; i < linesTotal; i++) {
838                 buffer.append(lines[i]).append(EOL);
839             }
840             errors.add(new CompilerMessage(buffer.toString(), ERROR));
841         } else if (exitCode != 0) {
842             // Nothing in the buffer was recognised, yet the compiler failed. Whatever is left is reported rather
843             // than dropped, so that a failing build is never left without an explanation.
844             String unrecognised = stripCountSummaries(bufferContent);
845             if (!unrecognised.isEmpty()) {
846                 errors.add(new CompilerMessage(unrecognised, ERROR));
847             }
848         }
849 
850         return errors;
851     }
852 
853     /**
854      * Drops javac's trailing tallies, such as {@code 2 errors} or {@code 警告 1 個}, from otherwise unrecognised
855      * output. They repeat what the parsed messages already say, so on their own they are not worth reporting.
856      *
857      * @param content the unrecognised output
858      * @return the same content without its count lines, trimmed
859      */
860     private static String stripCountSummaries(String content) {
861         StringBuilder kept = new StringBuilder();
862         for (String line : content.split("\\R")) {
863             if (!line.trim().isEmpty() && !COUNT_SUMMARY.matcher(line).matches()) {
864                 kept.append(line).append(EOL);
865             }
866         }
867         return kept.toString().trim();
868     }
869 
870     private static boolean isMisc(String message) {
871         return startsWithPrefix(message, MISC_PREFIXES);
872     }
873 
874     private static boolean isNote(String message) {
875         return startsWithPrefix(message, NOTE_PREFIXES);
876     }
877 
878     /**
879      * Tells whether a line continues the note that precedes it. Since JDK 21 javac wraps its notes over several
880      * lines, indenting every line but the first, while the diagnostics that may follow a note all start in the
881      * first column.
882      * <p>
883      * A leading space, not leading whitespace: javac indents note continuations with spaces, whereas a leading tab
884      * marks a stack trace frame, which {@link #STACK_TRACE_OTHER_LINE} is waiting for.
885      *
886      * @param line the line following a note
887      * @return whether the line belongs to that note
888      */
889     private static boolean isNoteContinuation(String line) {
890         return line.startsWith(" ");
891     }
892 
893     private static boolean isWarning(String message) {
894         return startsWithPrefix(message, WARNING_PREFIXES);
895     }
896 
897     private static boolean isError(String message) {
898         return startsWithPrefix(message, ERROR_PREFIXES);
899     }
900 
901     private static String getJavacGenericError(String message) {
902         return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES);
903     }
904 
905     private static String getVMInitError(String message) {
906         return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS);
907     }
908 
909     private static String getBootLayerInitError(String message) {
910         return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS);
911     }
912 
913     private static String getFileABugError(String message) {
914         return getTextStartingWithPrefix(message, FILE_A_BUG_ERROR_HEADERS);
915     }
916 
917     private static String getAnnotationProcessingError(String message) {
918         return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS);
919     }
920 
921     private static String getSystemOutOfResourcesError(String message) {
922         return getTextStartingWithPrefix(message, SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS);
923     }
924 
925     private static String getIOError(String message) {
926         return getTextStartingWithPrefix(message, IO_ERROR_HEADERS);
927     }
928 
929     private static String getPluginError(String message) {
930         return getTextStartingWithPrefix(message, PLUGIN_ERROR_HEADERS);
931     }
932 
933     private static boolean startsWithPrefix(String text, String[] prefixes) {
934         for (String prefix : prefixes) {
935             if (text.startsWith(prefix)) {
936                 return true;
937             }
938         }
939         return false;
940     }
941 
942     /**
943      * Identify and return a known javac error message prefix and all subsequent text - usually a stack trace - from a
944      * javac log output buffer.
945      *
946      * @param text     log buffer to search for a javac error message stack trace
947      * @param prefixes array of strings in Java properties format, e.g. {@code "some error with line feed\nand parameter
948      *                 placeholders {0} and {1}"} in multiple locales (hence the array). For the search, the
949      *                 placeholders may be represented by any text in the log buffer.
950      * @return if found, the error message + all subsequent text, otherwise {@code null}
951      */
952     static String getTextStartingWithPrefix(String text, String[] prefixes) {
953         // Implementation note: The properties format with placeholders  makes it easy to just copy & paste values from
954         // the JDK compared to having to convert them to regular expressions with ".*" instead of "{0}" and quote
955         // special regex characters. This makes the implementation of this method more complex and potentially a bit
956         // slower, but hopefully is worth the effort for the convenience of future developers maintaining this class.
957 
958         // Normalise line feeds to the UNIX format found in JDK multi-line messages in properties files
959         text = text.replaceAll("\\R", "\n");
960 
961         // Search text for given error message prefixes/headers, until the first match is found
962         for (String prefix : prefixes) {
963             // Split properties message along placeholders like "{0}", "{1}" etc.
964             String[] prefixParts = prefix.split("\\{\\d+\\}");
965             for (int i = 0; i < prefixParts.length; i++) {
966                 // Make sure to treat split sections as literal text in search regex by enclosing them in "\Q" and "\E".
967                 // See https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, search for "Quotation".
968                 prefixParts[i] = "\\Q" + prefixParts[i] + "\\E";
969             }
970             // Join message parts, replacing properties placeholders by ".*" regex ones
971             prefix = String.join(".*?", prefixParts);
972             // Find prefix + subsequent text in Pattern.DOTALL mode, represented in regex as "(?s)".
973             // This matches across line break boundaries.
974             Matcher matcher = Pattern.compile("(?s).*(" + prefix + ".*)").matcher(text);
975             if (matcher.matches()) {
976                 // Match -> cut off text before header and replace UNIX line breaks by platform ones again
977                 return matcher.replaceFirst("$1").replaceAll("\n", EOL);
978             }
979         }
980 
981         // No match
982         return null;
983     }
984 
985     /**
986      * Construct a compiler message object from a compiler output line
987      *
988      * @param exitCode javac exit code
989      * @param error    compiler output line
990      * @return compiler message object
991      */
992     static CompilerMessage parseModernError(int exitCode, String error) {
993         final StringTokenizer tokens = new StringTokenizer(error, ":");
994         CompilerMessage.Kind messageKind = exitCode == 0 ? WARNING : ERROR;
995 
996         try {
997             // With Java 6 error output lines from the compiler got longer. For backward compatibility
998             // and the time being, we eat up all (if any) tokens up to the erroneous file and source
999             // line indicator tokens.
1000 
1001             boolean tokenIsAnInteger;
1002             StringBuilder file = null;
1003             String currentToken = null;
1004 
1005             do {
1006                 if (currentToken != null) {
1007                     if (file == null) {
1008                         file = new StringBuilder(currentToken);
1009                     } else {
1010                         file.append(':').append(currentToken);
1011                     }
1012                 }
1013 
1014                 currentToken = tokens.nextToken();
1015                 // Probably the only backward compatible means of checking if a string is an integer.
1016                 tokenIsAnInteger = true;
1017 
1018                 try {
1019                     Integer.parseInt(currentToken);
1020                 } catch (NumberFormatException e) {
1021                     tokenIsAnInteger = false;
1022                 }
1023             } while (!tokenIsAnInteger);
1024 
1025             final String lineIndicator = currentToken;
1026             final int startOfFileName = Objects.requireNonNull(file).toString().lastIndexOf(']');
1027             if (startOfFileName > -1) {
1028                 file = new StringBuilder(file.substring(startOfFileName + 1 + EOL.length()));
1029             }
1030 
1031             final int line = Integer.parseInt(lineIndicator);
1032             final StringBuilder msgBuffer = new StringBuilder();
1033             String msg = tokens.nextToken(EOL).substring(2);
1034 
1035             // Remove "error: " and "warning: " prefixes
1036             String prefix;
1037             if ((prefix = getErrorPrefix(msg)) != null) {
1038                 messageKind = ERROR;
1039                 msg = msg.substring(prefix.length());
1040             } else if ((prefix = getWarningPrefix(msg)) != null) {
1041                 messageKind = WARNING;
1042                 msg = msg.substring(prefix.length());
1043             }
1044             msgBuffer.append(msg).append(EOL);
1045 
1046             String context = tokens.nextToken(EOL);
1047             String pointer = null;
1048 
1049             do {
1050                 final String msgLine = tokens.nextToken(EOL);
1051                 if (pointer != null) {
1052                     msgBuffer.append(msgLine);
1053                     msgBuffer.append(EOL);
1054                 } else if (msgLine.endsWith("^")) {
1055                     pointer = msgLine;
1056                 } else {
1057                     msgBuffer.append(context);
1058                     msgBuffer.append(EOL);
1059                     context = msgLine;
1060                 }
1061             } while (tokens.hasMoreTokens());
1062 
1063             msgBuffer.append(EOL);
1064 
1065             final String message = msgBuffer.toString();
1066             final int startcolumn = Objects.requireNonNull(pointer).indexOf("^") + 1;
1067             int endcolumn = (context == null) ? startcolumn : context.indexOf(" ", startcolumn);
1068             if (endcolumn == -1) {
1069                 endcolumn = Objects.requireNonNull(context).length();
1070             }
1071 
1072             return new CompilerMessage(
1073                     file.toString(), messageKind, line, startcolumn, line, endcolumn, message.trim());
1074         } catch (NoSuchElementException e) {
1075             return new CompilerMessage("no more tokens - could not parse error message: " + error, messageKind);
1076         } catch (Exception e) {
1077             return new CompilerMessage("could not parse error message: " + error, messageKind);
1078         }
1079     }
1080 
1081     private static String getMessagePrefix(String message, String[] prefixes) {
1082         for (String prefix : prefixes) {
1083             if (message.startsWith(prefix)) {
1084                 return prefix;
1085             }
1086         }
1087         return null;
1088     }
1089 
1090     private static String getWarningPrefix(String message) {
1091         return getMessagePrefix(message, WARNING_PREFIXES);
1092     }
1093 
1094     private static String getErrorPrefix(String message) {
1095         return getMessagePrefix(message, ERROR_PREFIXES);
1096     }
1097 
1098     /**
1099      * put args into a temp file to be referenced using the @ option in javac command line
1100      *
1101      * @param args
1102      * @return the temporary file wth the arguments
1103      * @throws IOException
1104      */
1105     private File createFileWithArguments(String[] args, String outputDirectory) throws IOException {
1106         PrintWriter writer = null;
1107         try {
1108             File tempFile;
1109             if (getLog().isDebugEnabled()) {
1110                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments", new File(outputDirectory));
1111             } else {
1112                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments");
1113                 tempFile.deleteOnExit();
1114             }
1115 
1116             writer = new PrintWriter(new FileWriter(tempFile));
1117             for (String arg : args) {
1118                 writer.println(quoteArgument(arg));
1119             }
1120             writer.flush();
1121 
1122             return tempFile;
1123 
1124         } finally {
1125             if (writer != null) {
1126                 writer.close();
1127             }
1128         }
1129     }
1130 
1131     /**
1132      * Quotes an argument according to javac command-line argument file syntax.
1133      */
1134     static String quoteArgument(String argument) {
1135         StringBuilder quoted = new StringBuilder(argument.length() + 2);
1136         quoted.append('"');
1137         for (int i = 0; i < argument.length(); i++) {
1138             char c = argument.charAt(i);
1139             switch (c) {
1140                 case '\\':
1141                     quoted.append("\\\\");
1142                     break;
1143                 case '"':
1144                     quoted.append("\\\"");
1145                     break;
1146                 case '\n':
1147                     quoted.append("\\n");
1148                     break;
1149                 case '\r':
1150                     quoted.append("\\r");
1151                     break;
1152                 case '\t':
1153                     quoted.append("\\t");
1154                     break;
1155                 case '\f':
1156                     quoted.append("\\f");
1157                     break;
1158                 default:
1159                     quoted.append(c);
1160                     break;
1161             }
1162         }
1163         return quoted.append('"').toString();
1164     }
1165 
1166     /**
1167      * Get the path of the javac tool executable to use.
1168      * Either given through explicit configuration or via {@link #getJavacExecutable()}.
1169      * @param config the configuration
1170      * @return the path of the javac tool
1171      */
1172     protected String getJavacExecutable(CompilerConfiguration config) {
1173         String executable = config.getExecutable();
1174 
1175         if (StringUtils.isEmpty(executable)) {
1176             try {
1177                 executable = getJavacExecutable();
1178             } catch (IOException e) {
1179                 if (getLog().isWarnEnabled()) {
1180                     getLog().warn("Unable to autodetect 'javac' path, using 'javac' from the environment.");
1181                 }
1182                 executable = "javac";
1183             }
1184         }
1185         return executable;
1186     }
1187 
1188     /**
1189      * Get the path of the javac tool executable: try to find it depending the OS or the <code>java.home</code>
1190      * system property or the <code>JAVA_HOME</code> environment variable.
1191      *
1192      * @return the path of the javac tool
1193      * @throws IOException if not found
1194      */
1195     private static String getJavacExecutable() throws IOException {
1196         String javacCommand = "javac" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : "");
1197         String javaHome = System.getProperty("java.home");
1198         File javacExe;
1199 
1200         if (Os.isName("AIX")) {
1201             javacExe = new File(javaHome + File.separator + ".." + File.separator + "sh", javacCommand);
1202         } else if (Os.isName("Mac OS X")) {
1203             javacExe = new File(javaHome + File.separator + "bin", javacCommand);
1204         } else {
1205             javacExe = new File(javaHome + File.separator + ".." + File.separator + "bin", javacCommand);
1206         }
1207 
1208         // ----------------------------------------------------------------------
1209         // Try to find javacExe from JAVA_HOME environment variable
1210         // ----------------------------------------------------------------------
1211         if (!javacExe.isFile()) {
1212             Properties env = CommandLineUtils.getSystemEnvVars();
1213             javaHome = env.getProperty("JAVA_HOME");
1214             if (StringUtils.isEmpty(javaHome)) {
1215                 throw new IOException("The environment variable JAVA_HOME is not correctly set.");
1216             }
1217             if (!new File(javaHome).isDirectory()) {
1218                 throw new IOException("The environment variable JAVA_HOME=" + javaHome
1219                         + " doesn't exist or is not a valid directory.");
1220             }
1221             javacExe = new File(env.getProperty("JAVA_HOME") + File.separator + "bin", javacCommand);
1222         }
1223 
1224         if (!javacExe.isFile()) {
1225             throw new IOException("The javadoc executable '" + javacExe
1226                     + "' doesn't exist or is not a file. Verify the JAVA_HOME environment variable.");
1227         }
1228 
1229         return javacExe.getAbsolutePath();
1230     }
1231 
1232     private void releaseJavaccClass(Class<?> javaccClass, CompilerConfiguration compilerConfiguration) {
1233         if (compilerConfiguration.getCompilerReuseStrategy()
1234                 == CompilerConfiguration.CompilerReuseStrategy.ReuseCreated) {
1235             javacClasses.add(javaccClass);
1236         }
1237     }
1238 
1239     /**
1240      * Find the main class of JavaC. Return the same class for subsequent calls.
1241      *
1242      * @return the non-null class.
1243      * @throws CompilerException if the class has not been found.
1244      */
1245     private Class<?> getJavacClass(CompilerConfiguration compilerConfiguration) throws CompilerException {
1246         Class<?> c;
1247         switch (compilerConfiguration.getCompilerReuseStrategy()) {
1248             case AlwaysNew:
1249                 return createJavacClass();
1250             case ReuseCreated:
1251                 c = javacClasses.poll();
1252                 if (c == null) {
1253                     c = createJavacClass();
1254                 }
1255                 return c;
1256             case ReuseSame:
1257             default:
1258                 c = javacClass;
1259                 if (c == null) {
1260                     synchronized (this) {
1261                         c = javacClass;
1262                         if (c == null) {
1263                             javacClass = c = createJavacClass();
1264                         }
1265                     }
1266                 }
1267                 return c;
1268         }
1269     }
1270 
1271     /**
1272      * Helper method for create Javac class
1273      */
1274     protected Class<?> createJavacClass() throws CompilerException {
1275         try {
1276             // look whether JavaC is on Maven's classpath
1277             // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, JavacCompiler.class.getClassLoader() );
1278             return JavacCompiler.class.getClassLoader().loadClass(JavacCompiler.JAVAC_CLASSNAME);
1279         } catch (ClassNotFoundException ex) {
1280             // ok
1281         }
1282 
1283         final File toolsJar = new File(System.getProperty("java.home"), "../lib/tools.jar");
1284         if (!toolsJar.exists()) {
1285             throw new CompilerException("tools.jar not found: " + toolsJar);
1286         }
1287 
1288         try {
1289             // Combined classloader with no parent/child relationship, so classes in our classloader
1290             // can reference classes in tools.jar
1291             URL[] originalUrls = ((URLClassLoader) JavacCompiler.class.getClassLoader()).getURLs();
1292             URL[] urls = new URL[originalUrls.length + 1];
1293             urls[0] = toolsJar.toURI().toURL();
1294             System.arraycopy(originalUrls, 0, urls, 1, originalUrls.length);
1295             ClassLoader javacClassLoader = new URLClassLoader(urls);
1296 
1297             final Thread thread = Thread.currentThread();
1298             final ClassLoader contextClassLoader = thread.getContextClassLoader();
1299             thread.setContextClassLoader(javacClassLoader);
1300             try {
1301                 // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, javacClassLoader );
1302                 return javacClassLoader.loadClass(JavacCompiler.JAVAC_CLASSNAME);
1303             } finally {
1304                 thread.setContextClassLoader(contextClassLoader);
1305             }
1306         } catch (MalformedURLException ex) {
1307             throw new CompilerException(
1308                     "Could not convert the file reference to tools.jar to a URL, path to tools.jar: '"
1309                             + toolsJar.getAbsolutePath() + "'.",
1310                     ex);
1311         } catch (ClassNotFoundException ex) {
1312             throw new CompilerException(
1313                     "Unable to locate the Javac Compiler in:" + EOL + "  " + toolsJar + EOL
1314                             + "Please ensure you are using JDK 1.4 or above and" + EOL
1315                             + "not a JRE (the com.sun.tools.javac.Main class is required)." + EOL
1316                             + "In most cases you can change the location of your Java" + EOL
1317                             + "installation by setting the JAVA_HOME environment variable.",
1318                     ex);
1319         }
1320     }
1321 }