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     private static final Pattern STACK_TRACE_OTHER_LINE =
738             Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$");
739 
740     /**
741      * Parse the compiler output into a list of compiler messages
742      *
743      * @param exitCode javac exit code (0 on success, non-zero otherwise)
744      * @param input    compiler output (stdOut and stdErr merged into input stream)
745      * @return list of {@link CompilerMessage} objects
746      * @throws IOException if there is a problem reading from the input reader
747      */
748     static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader input) throws IOException {
749         List<CompilerMessage> errors = new ArrayList<>();
750         String line;
751         StringBuilder buffer = new StringBuilder();
752         boolean hasPointer = false;
753         int stackTraceLineCount = 0;
754 
755         while ((line = input.readLine()) != null) {
756             if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE.matcher(line).matches()
757                     || STACK_TRACE_OTHER_LINE.matcher(line).matches()) {
758                 stackTraceLineCount++;
759             } else {
760                 stackTraceLineCount = 0;
761             }
762 
763             // new error block?
764             if (!line.startsWith(" ") && hasPointer) {
765                 // add the error bean
766                 errors.add(parseModernError(exitCode, buffer.toString()));
767                 // reset for next error block
768                 buffer = new StringBuilder(); // this is quicker than clearing it
769                 hasPointer = false;
770             }
771 
772             if (buffer.length() == 0) {
773                 // try to classify output line by type (error, warning etc.)
774                 // TODO: there should be a better way to parse these
775                 if (isError(line)) {
776                     errors.add(new CompilerMessage(line, ERROR));
777                 } else if (isWarning(line)) {
778                     errors.add(new CompilerMessage(line, WARNING));
779                 } else if (isNote(line)) {
780                     // skip, JDK telling us deprecated APIs are used but -Xlint:deprecation isn't set
781                 } else if (isMisc(line)) {
782                     // verbose output was set
783                     errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER));
784                 } else {
785                     // add first unclassified line to buffer
786                     buffer.append(line).append(EOL);
787                 }
788             } else {
789                 // add next unclassified line to buffer
790                 buffer.append(line).append(EOL);
791             }
792 
793             if (line.endsWith("^")) {
794                 hasPointer = true;
795             }
796         }
797 
798         String bufferContent = buffer.toString();
799         if (bufferContent.isEmpty()) {
800             return errors;
801         }
802 
803         // javac output not detected by other parsing
804         // maybe better to ignore only the summary and mark the rest as error
805         String cleanedUpMessage;
806         if ((cleanedUpMessage = getJavacGenericError(bufferContent)) != null
807                 || (cleanedUpMessage = getBootLayerInitError(bufferContent)) != null
808                 || (cleanedUpMessage = getVMInitError(bufferContent)) != null
809                 || (cleanedUpMessage = getFileABugError(bufferContent)) != null
810                 || (cleanedUpMessage = getAnnotationProcessingError(bufferContent)) != null
811                 || (cleanedUpMessage = getSystemOutOfResourcesError(bufferContent)) != null
812                 || (cleanedUpMessage = getIOError(bufferContent)) != null
813                 || (cleanedUpMessage = getPluginError(bufferContent)) != null) {
814             errors.add(new CompilerMessage(cleanedUpMessage, ERROR));
815         } else if (hasPointer) {
816             // A compiler message remains in buffer at end of parse stream
817             errors.add(parseModernError(exitCode, bufferContent));
818         } else if (stackTraceLineCount > 0) {
819             // Extract stack trace from end of buffer
820             String[] lines = bufferContent.split("\\R");
821             int linesTotal = lines.length;
822             buffer = new StringBuilder();
823             int firstLine = linesTotal - stackTraceLineCount;
824             for (int i = firstLine; i < linesTotal; i++) {
825                 buffer.append(lines[i]).append(EOL);
826             }
827             errors.add(new CompilerMessage(buffer.toString(), ERROR));
828         }
829         // TODO: Add something like this? Check if it creates more value or more unnecessary log output in general.
830         // else {
831         //     // Fall-back, if still no error or stack trace was recognised
832         //     errors.add(new CompilerMessage(bufferContent, exitCode == 0 ? OTHER : ERROR));
833         // }
834 
835         return errors;
836     }
837 
838     private static boolean isMisc(String message) {
839         return startsWithPrefix(message, MISC_PREFIXES);
840     }
841 
842     private static boolean isNote(String message) {
843         return startsWithPrefix(message, NOTE_PREFIXES);
844     }
845 
846     private static boolean isWarning(String message) {
847         return startsWithPrefix(message, WARNING_PREFIXES);
848     }
849 
850     private static boolean isError(String message) {
851         return startsWithPrefix(message, ERROR_PREFIXES);
852     }
853 
854     private static String getJavacGenericError(String message) {
855         return getTextStartingWithPrefix(message, JAVAC_GENERIC_ERROR_PREFIXES);
856     }
857 
858     private static String getVMInitError(String message) {
859         return getTextStartingWithPrefix(message, VM_INIT_ERROR_HEADERS);
860     }
861 
862     private static String getBootLayerInitError(String message) {
863         return getTextStartingWithPrefix(message, BOOT_LAYER_INIT_ERROR_HEADERS);
864     }
865 
866     private static String getFileABugError(String message) {
867         return getTextStartingWithPrefix(message, FILE_A_BUG_ERROR_HEADERS);
868     }
869 
870     private static String getAnnotationProcessingError(String message) {
871         return getTextStartingWithPrefix(message, ANNOTATION_PROCESSING_ERROR_HEADERS);
872     }
873 
874     private static String getSystemOutOfResourcesError(String message) {
875         return getTextStartingWithPrefix(message, SYSTEM_OUT_OF_RESOURCES_ERROR_HEADERS);
876     }
877 
878     private static String getIOError(String message) {
879         return getTextStartingWithPrefix(message, IO_ERROR_HEADERS);
880     }
881 
882     private static String getPluginError(String message) {
883         return getTextStartingWithPrefix(message, PLUGIN_ERROR_HEADERS);
884     }
885 
886     private static boolean startsWithPrefix(String text, String[] prefixes) {
887         for (String prefix : prefixes) {
888             if (text.startsWith(prefix)) {
889                 return true;
890             }
891         }
892         return false;
893     }
894 
895     /**
896      * Identify and return a known javac error message prefix and all subsequent text - usually a stack trace - from a
897      * javac log output buffer.
898      *
899      * @param text     log buffer to search for a javac error message stack trace
900      * @param prefixes array of strings in Java properties format, e.g. {@code "some error with line feed\nand parameter
901      *                 placeholders {0} and {1}"} in multiple locales (hence the array). For the search, the
902      *                 placeholders may be represented by any text in the log buffer.
903      * @return if found, the error message + all subsequent text, otherwise {@code null}
904      */
905     static String getTextStartingWithPrefix(String text, String[] prefixes) {
906         // Implementation note: The properties format with placeholders  makes it easy to just copy & paste values from
907         // the JDK compared to having to convert them to regular expressions with ".*" instead of "{0}" and quote
908         // special regex characters. This makes the implementation of this method more complex and potentially a bit
909         // slower, but hopefully is worth the effort for the convenience of future developers maintaining this class.
910 
911         // Normalise line feeds to the UNIX format found in JDK multi-line messages in properties files
912         text = text.replaceAll("\\R", "\n");
913 
914         // Search text for given error message prefixes/headers, until the first match is found
915         for (String prefix : prefixes) {
916             // Split properties message along placeholders like "{0}", "{1}" etc.
917             String[] prefixParts = prefix.split("\\{\\d+\\}");
918             for (int i = 0; i < prefixParts.length; i++) {
919                 // Make sure to treat split sections as literal text in search regex by enclosing them in "\Q" and "\E".
920                 // See https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, search for "Quotation".
921                 prefixParts[i] = "\\Q" + prefixParts[i] + "\\E";
922             }
923             // Join message parts, replacing properties placeholders by ".*" regex ones
924             prefix = String.join(".*?", prefixParts);
925             // Find prefix + subsequent text in Pattern.DOTALL mode, represented in regex as "(?s)".
926             // This matches across line break boundaries.
927             Matcher matcher = Pattern.compile("(?s).*(" + prefix + ".*)").matcher(text);
928             if (matcher.matches()) {
929                 // Match -> cut off text before header and replace UNIX line breaks by platform ones again
930                 return matcher.replaceFirst("$1").replaceAll("\n", EOL);
931             }
932         }
933 
934         // No match
935         return null;
936     }
937 
938     /**
939      * Construct a compiler message object from a compiler output line
940      *
941      * @param exitCode javac exit code
942      * @param error    compiler output line
943      * @return compiler message object
944      */
945     static CompilerMessage parseModernError(int exitCode, String error) {
946         final StringTokenizer tokens = new StringTokenizer(error, ":");
947         CompilerMessage.Kind messageKind = exitCode == 0 ? WARNING : ERROR;
948 
949         try {
950             // With Java 6 error output lines from the compiler got longer. For backward compatibility
951             // and the time being, we eat up all (if any) tokens up to the erroneous file and source
952             // line indicator tokens.
953 
954             boolean tokenIsAnInteger;
955             StringBuilder file = null;
956             String currentToken = null;
957 
958             do {
959                 if (currentToken != null) {
960                     if (file == null) {
961                         file = new StringBuilder(currentToken);
962                     } else {
963                         file.append(':').append(currentToken);
964                     }
965                 }
966 
967                 currentToken = tokens.nextToken();
968                 // Probably the only backward compatible means of checking if a string is an integer.
969                 tokenIsAnInteger = true;
970 
971                 try {
972                     Integer.parseInt(currentToken);
973                 } catch (NumberFormatException e) {
974                     tokenIsAnInteger = false;
975                 }
976             } while (!tokenIsAnInteger);
977 
978             final String lineIndicator = currentToken;
979             final int startOfFileName = Objects.requireNonNull(file).toString().lastIndexOf(']');
980             if (startOfFileName > -1) {
981                 file = new StringBuilder(file.substring(startOfFileName + 1 + EOL.length()));
982             }
983 
984             final int line = Integer.parseInt(lineIndicator);
985             final StringBuilder msgBuffer = new StringBuilder();
986             String msg = tokens.nextToken(EOL).substring(2);
987 
988             // Remove "error: " and "warning: " prefixes
989             String prefix;
990             if ((prefix = getErrorPrefix(msg)) != null) {
991                 messageKind = ERROR;
992                 msg = msg.substring(prefix.length());
993             } else if ((prefix = getWarningPrefix(msg)) != null) {
994                 messageKind = WARNING;
995                 msg = msg.substring(prefix.length());
996             }
997             msgBuffer.append(msg).append(EOL);
998 
999             String context = tokens.nextToken(EOL);
1000             String pointer = null;
1001 
1002             do {
1003                 final String msgLine = tokens.nextToken(EOL);
1004                 if (pointer != null) {
1005                     msgBuffer.append(msgLine);
1006                     msgBuffer.append(EOL);
1007                 } else if (msgLine.endsWith("^")) {
1008                     pointer = msgLine;
1009                 } else {
1010                     msgBuffer.append(context);
1011                     msgBuffer.append(EOL);
1012                     context = msgLine;
1013                 }
1014             } while (tokens.hasMoreTokens());
1015 
1016             msgBuffer.append(EOL);
1017 
1018             final String message = msgBuffer.toString();
1019             final int startcolumn = Objects.requireNonNull(pointer).indexOf("^");
1020             int endcolumn = (context == null) ? startcolumn : context.indexOf(" ", startcolumn);
1021             if (endcolumn == -1) {
1022                 endcolumn = Objects.requireNonNull(context).length();
1023             }
1024 
1025             return new CompilerMessage(
1026                     file.toString(), messageKind, line, startcolumn, line, endcolumn, message.trim());
1027         } catch (NoSuchElementException e) {
1028             return new CompilerMessage("no more tokens - could not parse error message: " + error, messageKind);
1029         } catch (Exception e) {
1030             return new CompilerMessage("could not parse error message: " + error, messageKind);
1031         }
1032     }
1033 
1034     private static String getMessagePrefix(String message, String[] prefixes) {
1035         for (String prefix : prefixes) {
1036             if (message.startsWith(prefix)) {
1037                 return prefix;
1038             }
1039         }
1040         return null;
1041     }
1042 
1043     private static String getWarningPrefix(String message) {
1044         return getMessagePrefix(message, WARNING_PREFIXES);
1045     }
1046 
1047     private static String getErrorPrefix(String message) {
1048         return getMessagePrefix(message, ERROR_PREFIXES);
1049     }
1050 
1051     /**
1052      * put args into a temp file to be referenced using the @ option in javac command line
1053      *
1054      * @param args
1055      * @return the temporary file wth the arguments
1056      * @throws IOException
1057      */
1058     private File createFileWithArguments(String[] args, String outputDirectory) throws IOException {
1059         PrintWriter writer = null;
1060         try {
1061             File tempFile;
1062             if (getLog().isDebugEnabled()) {
1063                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments", new File(outputDirectory));
1064             } else {
1065                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments");
1066                 tempFile.deleteOnExit();
1067             }
1068 
1069             writer = new PrintWriter(new FileWriter(tempFile));
1070             for (String arg : args) {
1071                 String argValue = arg.replace(File.separatorChar, '/');
1072                 writer.write("\"" + argValue + "\"");
1073                 writer.println();
1074             }
1075             writer.flush();
1076 
1077             return tempFile;
1078 
1079         } finally {
1080             if (writer != null) {
1081                 writer.close();
1082             }
1083         }
1084     }
1085 
1086     /**
1087      * Get the path of the javac tool executable to use.
1088      * Either given through explicit configuration or via {@link #getJavacExecutable()}.
1089      * @param config the configuration
1090      * @return the path of the javac tool
1091      */
1092     protected String getJavacExecutable(CompilerConfiguration config) {
1093         String executable = config.getExecutable();
1094 
1095         if (StringUtils.isEmpty(executable)) {
1096             try {
1097                 executable = getJavacExecutable();
1098             } catch (IOException e) {
1099                 if (getLog().isWarnEnabled()) {
1100                     getLog().warn("Unable to autodetect 'javac' path, using 'javac' from the environment.");
1101                 }
1102                 executable = "javac";
1103             }
1104         }
1105         return executable;
1106     }
1107 
1108     /**
1109      * Get the path of the javac tool executable: try to find it depending the OS or the <code>java.home</code>
1110      * system property or the <code>JAVA_HOME</code> environment variable.
1111      *
1112      * @return the path of the javac tool
1113      * @throws IOException if not found
1114      */
1115     private static String getJavacExecutable() throws IOException {
1116         String javacCommand = "javac" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : "");
1117         String javaHome = System.getProperty("java.home");
1118         File javacExe;
1119 
1120         if (Os.isName("AIX")) {
1121             javacExe = new File(javaHome + File.separator + ".." + File.separator + "sh", javacCommand);
1122         } else if (Os.isName("Mac OS X")) {
1123             javacExe = new File(javaHome + File.separator + "bin", javacCommand);
1124         } else {
1125             javacExe = new File(javaHome + File.separator + ".." + File.separator + "bin", javacCommand);
1126         }
1127 
1128         // ----------------------------------------------------------------------
1129         // Try to find javacExe from JAVA_HOME environment variable
1130         // ----------------------------------------------------------------------
1131         if (!javacExe.isFile()) {
1132             Properties env = CommandLineUtils.getSystemEnvVars();
1133             javaHome = env.getProperty("JAVA_HOME");
1134             if (StringUtils.isEmpty(javaHome)) {
1135                 throw new IOException("The environment variable JAVA_HOME is not correctly set.");
1136             }
1137             if (!new File(javaHome).isDirectory()) {
1138                 throw new IOException("The environment variable JAVA_HOME=" + javaHome
1139                         + " doesn't exist or is not a valid directory.");
1140             }
1141             javacExe = new File(env.getProperty("JAVA_HOME") + File.separator + "bin", javacCommand);
1142         }
1143 
1144         if (!javacExe.isFile()) {
1145             throw new IOException("The javadoc executable '" + javacExe
1146                     + "' doesn't exist or is not a file. Verify the JAVA_HOME environment variable.");
1147         }
1148 
1149         return javacExe.getAbsolutePath();
1150     }
1151 
1152     private void releaseJavaccClass(Class<?> javaccClass, CompilerConfiguration compilerConfiguration) {
1153         if (compilerConfiguration.getCompilerReuseStrategy()
1154                 == CompilerConfiguration.CompilerReuseStrategy.ReuseCreated) {
1155             javacClasses.add(javaccClass);
1156         }
1157     }
1158 
1159     /**
1160      * Find the main class of JavaC. Return the same class for subsequent calls.
1161      *
1162      * @return the non-null class.
1163      * @throws CompilerException if the class has not been found.
1164      */
1165     private Class<?> getJavacClass(CompilerConfiguration compilerConfiguration) throws CompilerException {
1166         Class<?> c;
1167         switch (compilerConfiguration.getCompilerReuseStrategy()) {
1168             case AlwaysNew:
1169                 return createJavacClass();
1170             case ReuseCreated:
1171                 c = javacClasses.poll();
1172                 if (c == null) {
1173                     c = createJavacClass();
1174                 }
1175                 return c;
1176             case ReuseSame:
1177             default:
1178                 c = javacClass;
1179                 if (c == null) {
1180                     synchronized (this) {
1181                         c = javacClass;
1182                         if (c == null) {
1183                             javacClass = c = createJavacClass();
1184                         }
1185                     }
1186                 }
1187                 return c;
1188         }
1189     }
1190 
1191     /**
1192      * Helper method for create Javac class
1193      */
1194     protected Class<?> createJavacClass() throws CompilerException {
1195         try {
1196             // look whether JavaC is on Maven's classpath
1197             // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, JavacCompiler.class.getClassLoader() );
1198             return JavacCompiler.class.getClassLoader().loadClass(JavacCompiler.JAVAC_CLASSNAME);
1199         } catch (ClassNotFoundException ex) {
1200             // ok
1201         }
1202 
1203         final File toolsJar = new File(System.getProperty("java.home"), "../lib/tools.jar");
1204         if (!toolsJar.exists()) {
1205             throw new CompilerException("tools.jar not found: " + toolsJar);
1206         }
1207 
1208         try {
1209             // Combined classloader with no parent/child relationship, so classes in our classloader
1210             // can reference classes in tools.jar
1211             URL[] originalUrls = ((URLClassLoader) JavacCompiler.class.getClassLoader()).getURLs();
1212             URL[] urls = new URL[originalUrls.length + 1];
1213             urls[0] = toolsJar.toURI().toURL();
1214             System.arraycopy(originalUrls, 0, urls, 1, originalUrls.length);
1215             ClassLoader javacClassLoader = new URLClassLoader(urls);
1216 
1217             final Thread thread = Thread.currentThread();
1218             final ClassLoader contextClassLoader = thread.getContextClassLoader();
1219             thread.setContextClassLoader(javacClassLoader);
1220             try {
1221                 // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, javacClassLoader );
1222                 return javacClassLoader.loadClass(JavacCompiler.JAVAC_CLASSNAME);
1223             } finally {
1224                 thread.setContextClassLoader(contextClassLoader);
1225             }
1226         } catch (MalformedURLException ex) {
1227             throw new CompilerException(
1228                     "Could not convert the file reference to tools.jar to a URL, path to tools.jar: '"
1229                             + toolsJar.getAbsolutePath() + "'.",
1230                     ex);
1231         } catch (ClassNotFoundException ex) {
1232             throw new CompilerException(
1233                     "Unable to locate the Javac Compiler in:" + EOL + "  " + toolsJar + EOL
1234                             + "Please ensure you are using JDK 1.4 or above and" + EOL
1235                             + "not a JRE (the com.sun.tools.javac.Main class is required)." + EOL
1236                             + "In most cases you can change the location of your Java" + EOL
1237                             + "installation by setting the JAVA_HOME environment variable.",
1238                     ex);
1239         }
1240     }
1241 }