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