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.Properties;
67  import java.util.Set;
68  import java.util.StringTokenizer;
69  import java.util.concurrent.ConcurrentHashMap;
70  import java.util.concurrent.ConcurrentLinkedDeque;
71  import java.util.regex.Matcher;
72  import java.util.regex.Pattern;
73  
74  import org.codehaus.plexus.compiler.AbstractCompiler;
75  import org.codehaus.plexus.compiler.CompilerConfiguration;
76  import org.codehaus.plexus.compiler.CompilerException;
77  import org.codehaus.plexus.compiler.CompilerMessage;
78  import org.codehaus.plexus.compiler.CompilerOutputStyle;
79  import org.codehaus.plexus.compiler.CompilerResult;
80  import org.codehaus.plexus.util.FileUtils;
81  import org.codehaus.plexus.util.Os;
82  import org.codehaus.plexus.util.StringUtils;
83  import org.codehaus.plexus.util.cli.CommandLineException;
84  import org.codehaus.plexus.util.cli.CommandLineUtils;
85  import org.codehaus.plexus.util.cli.Commandline;
86  
87  /**
88   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
89   * @author <a href="mailto:matthew.pocock@ncl.ac.uk">Matthew Pocock</a>
90   * @author <a href="mailto:joerg.wassmer@web.de">J&ouml;rg Wa&szlig;mer</a>
91   * @author Others
92   *
93   */
94  @Named("javac")
95  @Singleton
96  public class JavacCompiler extends AbstractCompiler {
97  
98      // see compiler.warn.warning in compiler.properties of javac sources
99      private static final String[] WARNING_PREFIXES = {"warning: ", "\u8b66\u544a: ", "\u8b66\u544a\uff1a "};
100 
101     // see compiler.note.note in compiler.properties of javac sources
102     private static final String[] NOTE_PREFIXES = {"Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a "};
103 
104     // see compiler.misc.verbose in compiler.properties of javac sources
105     private static final String[] MISC_PREFIXES = {"["};
106 
107     private static final Object LOCK = new Object();
108 
109     private static final String JAVAC_CLASSNAME = "com.sun.tools.javac.Main";
110 
111     private volatile Class<?> javacClass;
112 
113     private final Deque<Class<?>> javacClasses = new ConcurrentLinkedDeque<>();
114 
115     private static final Pattern JAVA_MAJOR_AND_MINOR_VERSION_PATTERN = Pattern.compile("\\d+(\\.\\d+)?");
116 
117     /** Cache of javac version per executable (never invalidated) */
118     private static final Map<String, String> VERSION_PER_EXECUTABLE = new ConcurrentHashMap<>();
119 
120     @Inject
121     private InProcessCompiler inProcessCompiler;
122 
123     // ----------------------------------------------------------------------
124     //
125     // ----------------------------------------------------------------------
126 
127     public JavacCompiler() {
128         super(CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE, ".java", ".class", null);
129     }
130 
131     // ----------------------------------------------------------------------
132     // Compiler Implementation
133     // ----------------------------------------------------------------------
134 
135     @Override
136     public String getCompilerId() {
137         return "javac";
138     }
139 
140     private String getInProcessJavacVersion() throws CompilerException {
141         return System.getProperty("java.version");
142     }
143 
144     private String getOutOfProcessJavacVersion(String executable) throws CompilerException {
145         String version = VERSION_PER_EXECUTABLE.get(executable);
146         if (version == null) {
147             Commandline cli = new Commandline();
148             cli.setExecutable(executable);
149             /*
150              * The option "-version" should be supported by javac since 1.6 (https://docs.oracle.com/javase/6/docs/technotes/tools/solaris/javac.html)
151              * up to 21 (https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html#standard-options)
152              */
153             cli.addArguments(new String[] {"-version"}); //
154             CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
155             try {
156                 int exitCode = CommandLineUtils.executeCommandLine(cli, out, out);
157                 if (exitCode != 0) {
158                     throw new CompilerException("Could not retrieve version from " + executable + ". Exit code "
159                             + exitCode + ", Output: " + out.getOutput());
160                 }
161             } catch (CommandLineException e) {
162                 throw new CompilerException("Error while executing the external compiler " + executable, e);
163             }
164             version = extractMajorAndMinorVersion(out.getOutput());
165             VERSION_PER_EXECUTABLE.put(executable, version);
166         }
167         return version;
168     }
169 
170     static String extractMajorAndMinorVersion(String text) {
171         Matcher matcher = JAVA_MAJOR_AND_MINOR_VERSION_PATTERN.matcher(text);
172         if (!matcher.find()) {
173             throw new IllegalArgumentException("Could not extract version from \"" + text + "\"");
174         }
175         return matcher.group();
176     }
177 
178     @Override
179     public CompilerResult performCompile(CompilerConfiguration config) throws CompilerException {
180         File destinationDir = new File(config.getOutputLocation());
181 
182         if (!destinationDir.exists()) {
183             destinationDir.mkdirs();
184         }
185 
186         String[] sourceFiles = getSourceFiles(config);
187 
188         if ((sourceFiles == null) || (sourceFiles.length == 0)) {
189             return new CompilerResult();
190         }
191 
192         logCompiling(sourceFiles, config);
193 
194         final String javacVersion;
195         final String executable;
196         if (config.isFork()) {
197             executable = getJavacExecutable(config);
198             javacVersion = getOutOfProcessJavacVersion(executable);
199         } else {
200             javacVersion = getInProcessJavacVersion();
201             executable = null;
202         }
203 
204         String[] args = buildCompilerArguments(config, sourceFiles, javacVersion);
205 
206         CompilerResult result;
207 
208         if (config.isFork()) {
209 
210             result = compileOutOfProcess(config, executable, args);
211         } else {
212             if (hasJavaxToolProvider() && !config.isForceJavacCompilerUse()) {
213                 // use fqcn to prevent loading of the class on 1.5 environment !
214                 result = inProcessCompiler().compileInProcess(args, config, sourceFiles);
215             } else {
216                 result = compileInProcess(args, config);
217             }
218         }
219 
220         return result;
221     }
222 
223     protected InProcessCompiler inProcessCompiler() {
224         return inProcessCompiler;
225     }
226 
227     /**
228      *
229      * @return {@code true} if the current context class loader has access to {@code javax.tools.ToolProvider}
230      */
231     protected static boolean hasJavaxToolProvider() {
232         try {
233             Thread.currentThread().getContextClassLoader().loadClass("javax.tools.ToolProvider");
234             return true;
235         } catch (Exception e) {
236             return false;
237         }
238     }
239 
240     public String[] createCommandLine(CompilerConfiguration config) throws CompilerException {
241         final String javacVersion;
242         if (config.isFork()) {
243             String executable = getJavacExecutable(config);
244             javacVersion = getOutOfProcessJavacVersion(executable);
245         } else {
246             javacVersion = getInProcessJavacVersion();
247         }
248         return buildCompilerArguments(config, getSourceFiles(config), javacVersion);
249     }
250 
251     public static String[] buildCompilerArguments(
252             CompilerConfiguration config, String[] sourceFiles, String javacVersion) {
253         List<String> args = new ArrayList<>();
254 
255         // ----------------------------------------------------------------------
256         // Set output
257         // ----------------------------------------------------------------------
258 
259         File destinationDir = new File(config.getOutputLocation());
260 
261         args.add("-d");
262 
263         args.add(destinationDir.getAbsolutePath());
264 
265         // ----------------------------------------------------------------------
266         // Set the class and source paths
267         // ----------------------------------------------------------------------
268 
269         List<String> classpathEntries = config.getClasspathEntries();
270         if (classpathEntries != null && !classpathEntries.isEmpty()) {
271             args.add("-classpath");
272 
273             args.add(getPathString(classpathEntries));
274         }
275 
276         List<String> modulepathEntries = config.getModulepathEntries();
277         if (modulepathEntries != null && !modulepathEntries.isEmpty()) {
278             args.add("--module-path");
279 
280             args.add(getPathString(modulepathEntries));
281         }
282 
283         List<String> sourceLocations = config.getSourceLocations();
284         if (sourceLocations != null && !sourceLocations.isEmpty()) {
285             // always pass source path, even if sourceFiles are declared,
286             // needed for jsr269 annotation processing, see MCOMPILER-98
287             args.add("-sourcepath");
288 
289             args.add(getPathString(sourceLocations));
290         }
291         if (!hasJavaxToolProvider() || config.isForceJavacCompilerUse() || config.isFork()) {
292             args.addAll(Arrays.asList(sourceFiles));
293         }
294 
295         if (JavaVersion.JAVA_1_6.isOlderOrEqualTo(javacVersion)) {
296             // now add jdk 1.6 annotation processing related parameters
297 
298             if (config.getGeneratedSourcesDirectory() != null) {
299                 config.getGeneratedSourcesDirectory().mkdirs();
300 
301                 args.add("-s");
302                 args.add(config.getGeneratedSourcesDirectory().getAbsolutePath());
303             }
304             if (config.getProc() != null) {
305                 args.add("-proc:" + config.getProc());
306             }
307             if (config.getAnnotationProcessors() != null) {
308                 args.add("-processor");
309                 String[] procs = config.getAnnotationProcessors();
310                 StringBuilder buffer = new StringBuilder();
311                 for (int i = 0; i < procs.length; i++) {
312                     if (i > 0) {
313                         buffer.append(",");
314                     }
315 
316                     buffer.append(procs[i]);
317                 }
318                 args.add(buffer.toString());
319             }
320             if (config.getProcessorPathEntries() != null
321                     && !config.getProcessorPathEntries().isEmpty()) {
322                 args.add("-processorpath");
323                 args.add(getPathString(config.getProcessorPathEntries()));
324             }
325             if (config.getProcessorModulePathEntries() != null
326                     && !config.getProcessorModulePathEntries().isEmpty()) {
327                 args.add("--processor-module-path");
328                 args.add(getPathString(config.getProcessorModulePathEntries()));
329             }
330         }
331 
332         if (config.isOptimize()) {
333             args.add("-O");
334         }
335 
336         if (config.isDebug()) {
337             if (StringUtils.isNotEmpty(config.getDebugLevel())) {
338                 args.add("-g:" + config.getDebugLevel());
339             } else {
340                 args.add("-g");
341             }
342         }
343 
344         if (config.isVerbose()) {
345             args.add("-verbose");
346         }
347 
348         if (JavaVersion.JAVA_1_8.isOlderOrEqualTo(javacVersion) && config.isParameters()) {
349             args.add("-parameters");
350         }
351 
352         if (config.isEnablePreview()) {
353             args.add("--enable-preview");
354         }
355 
356         if (config.getImplicitOption() != null) {
357             args.add("-implicit:" + config.getImplicitOption());
358         }
359 
360         if (config.isShowDeprecation()) {
361             args.add("-deprecation");
362 
363             // This is required to actually display the deprecation messages
364             config.setShowWarnings(true);
365         }
366 
367         if (!config.isShowWarnings()) {
368             args.add("-nowarn");
369         } else {
370             String warnings = config.getWarnings();
371             if (config.isShowLint()) {
372                 if (config.isShowWarnings() && StringUtils.isNotEmpty(warnings)) {
373                     args.add("-Xlint:" + warnings);
374                 } else {
375                     args.add("-Xlint");
376                 }
377             }
378         }
379 
380         if (config.isFailOnWarning()) {
381             args.add("-Werror");
382         }
383 
384         if (JavaVersion.JAVA_9.isOlderOrEqualTo(javacVersion) && !StringUtils.isEmpty(config.getReleaseVersion())) {
385             args.add("--release");
386             args.add(config.getReleaseVersion());
387         } else {
388             // TODO: this could be much improved
389             if (StringUtils.isEmpty(config.getTargetVersion())) {
390                 // Required, or it defaults to the target of your JDK (eg 1.5)
391                 args.add("-target");
392                 args.add("1.1");
393             } else {
394                 args.add("-target");
395                 args.add(config.getTargetVersion());
396             }
397 
398             if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion) && StringUtils.isEmpty(config.getSourceVersion())) {
399                 // If omitted, later JDKs complain about a 1.1 target
400                 args.add("-source");
401                 args.add("1.3");
402             } else if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion)) {
403                 args.add("-source");
404                 args.add(config.getSourceVersion());
405             }
406         }
407 
408         if (JavaVersion.JAVA_1_4.isOlderOrEqualTo(javacVersion) && !StringUtils.isEmpty(config.getSourceEncoding())) {
409             args.add("-encoding");
410             args.add(config.getSourceEncoding());
411         }
412 
413         if (!StringUtils.isEmpty(config.getModuleVersion())) {
414             args.add("--module-version");
415             args.add(config.getModuleVersion());
416         }
417 
418         for (Map.Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
419             String key = entry.getKey();
420 
421             if (StringUtils.isEmpty(key) || key.startsWith("-J")) {
422                 continue;
423             }
424 
425             args.add(key);
426 
427             String value = entry.getValue();
428 
429             if (StringUtils.isEmpty(value)) {
430                 continue;
431             }
432 
433             args.add(value);
434         }
435 
436         if (!config.isFork() && !args.contains("-XDuseUnsharedTable=false")) {
437             args.add("-XDuseUnsharedTable=true");
438         }
439 
440         return args.toArray(new String[0]);
441     }
442 
443     /**
444      * Represents a particular Java version (through their according version prefixes)
445      */
446     enum JavaVersion {
447         JAVA_1_3_OR_OLDER("1.3", "1.2", "1.1", "1.0"),
448         JAVA_1_4("1.4"),
449         JAVA_1_5("1.5"),
450         JAVA_1_6("1.6"),
451         JAVA_1_7("1.7"),
452         JAVA_1_8("1.8"),
453         JAVA_9("9"); // since Java 9 a different versioning scheme was used (https://openjdk.org/jeps/223)
454         final Set<String> versionPrefixes;
455 
456         JavaVersion(String... versionPrefixes) {
457             this.versionPrefixes = new HashSet<>(Arrays.asList(versionPrefixes));
458         }
459 
460         /**
461          * The internal logic checks if the given version starts with the prefix of one of the enums preceding the current one.
462          *
463          * @param version the version to check
464          * @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
465          */
466         boolean isOlderOrEqualTo(String version) {
467             // go through all previous enums
468             JavaVersion[] allJavaVersionPrefixes = JavaVersion.values();
469             for (int n = ordinal() - 1; n > -1; n--) {
470                 if (allJavaVersionPrefixes[n].versionPrefixes.stream().anyMatch(version::startsWith)) {
471                     return false;
472                 }
473             }
474             return true;
475         }
476     }
477 
478     /**
479      * Compile the java sources in a external process, calling an external executable,
480      * like javac.
481      *
482      * @param config     compiler configuration
483      * @param executable name of the executable to launch
484      * @param args       arguments for the executable launched
485      * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages
486      * @throws CompilerException
487      */
488     protected CompilerResult compileOutOfProcess(CompilerConfiguration config, String executable, String[] args)
489             throws CompilerException {
490         Commandline cli = new Commandline();
491 
492         cli.setWorkingDirectory(config.getWorkingDirectory().getAbsolutePath());
493 
494         cli.setExecutable(executable);
495 
496         try {
497             File argumentsFile =
498                     createFileWithArguments(args, config.getBuildDirectory().getAbsolutePath());
499             cli.addArguments(
500                     new String[] {"@" + argumentsFile.getCanonicalPath().replace(File.separatorChar, '/')});
501 
502             if (!StringUtils.isEmpty(config.getMaxmem())) {
503                 cli.addArguments(new String[] {"-J-Xmx" + config.getMaxmem()});
504             }
505 
506             if (!StringUtils.isEmpty(config.getMeminitial())) {
507                 cli.addArguments(new String[] {"-J-Xms" + config.getMeminitial()});
508             }
509 
510             for (String key : config.getCustomCompilerArgumentsAsMap().keySet()) {
511                 if (StringUtils.isNotEmpty(key) && key.startsWith("-J")) {
512                     cli.addArguments(new String[] {key});
513                 }
514             }
515         } catch (IOException e) {
516             throw new CompilerException("Error creating file with javac arguments", e);
517         }
518 
519         CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
520 
521         int returnCode;
522 
523         List<CompilerMessage> messages;
524 
525         if (getLog().isDebugEnabled()) {
526             String debugFileName = StringUtils.isEmpty(config.getDebugFileName()) ? "javac" : config.getDebugFileName();
527 
528             File commandLineFile = new File(
529                     config.getBuildDirectory(),
530                     StringUtils.trim(debugFileName) + "." + (Os.isFamily(Os.FAMILY_WINDOWS) ? "bat" : "sh"));
531             try {
532                 FileUtils.fileWrite(
533                         commandLineFile.getAbsolutePath(), cli.toString().replaceAll("'", ""));
534 
535                 if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
536                     Runtime.getRuntime().exec(new String[] {"chmod", "a+x", commandLineFile.getAbsolutePath()});
537                 }
538             } catch (IOException e) {
539                 if (getLog().isWarnEnabled()) {
540                     getLog().warn("Unable to write '" + commandLineFile.getName() + "' debug script file", e);
541                 }
542             }
543         }
544 
545         try {
546             returnCode = CommandLineUtils.executeCommandLine(cli, out, out);
547 
548             messages = parseModernStream(returnCode, new BufferedReader(new StringReader(out.getOutput())));
549         } catch (CommandLineException | IOException e) {
550             throw new CompilerException("Error while executing the external compiler.", e);
551         }
552 
553         boolean success = returnCode == 0;
554         return new CompilerResult(success, messages);
555     }
556 
557     /**
558      * Compile the java sources in the current JVM, without calling an external executable,
559      * using <code>com.sun.tools.javac.Main</code> class
560      *
561      * @param args   arguments for the compiler as they would be used in the command line javac
562      * @param config compiler configuration
563      * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages
564      * @throws CompilerException
565      */
566     CompilerResult compileInProcess(String[] args, CompilerConfiguration config) throws CompilerException {
567         final Class<?> javacClass = getJavacClass(config);
568         final Thread thread = Thread.currentThread();
569         final ClassLoader contextClassLoader = thread.getContextClassLoader();
570         thread.setContextClassLoader(javacClass.getClassLoader());
571         if (getLog().isDebugEnabled()) {
572             getLog().debug("ttcl changed run compileInProcessWithProperClassloader");
573         }
574         try {
575             return compileInProcessWithProperClassloader(javacClass, args);
576         } finally {
577             releaseJavaccClass(javacClass, config);
578             thread.setContextClassLoader(contextClassLoader);
579         }
580     }
581 
582     protected CompilerResult compileInProcessWithProperClassloader(Class<?> javacClass, String[] args)
583             throws CompilerException {
584         return compileInProcess0(javacClass, args);
585     }
586 
587     /**
588      * Helper method for compileInProcess()
589      */
590     private static CompilerResult compileInProcess0(Class<?> javacClass, String[] args) throws CompilerException {
591         StringWriter out = new StringWriter();
592 
593         Integer ok;
594 
595         List<CompilerMessage> messages;
596 
597         try {
598             Method compile = javacClass.getMethod("compile", new Class[] {String[].class, PrintWriter.class});
599 
600             ok = (Integer) compile.invoke(null, new Object[] {args, new PrintWriter(out)});
601 
602             messages = parseModernStream(ok, new BufferedReader(new StringReader(out.toString())));
603         } catch (NoSuchMethodException | IOException | InvocationTargetException | IllegalAccessException e) {
604             throw new CompilerException("Error while executing the compiler.", e);
605         }
606 
607         boolean success = ok == 0;
608         return new CompilerResult(success, messages);
609     }
610 
611     // Match ~95% of existing JDK exception name patterns (last checked for JDK 21)
612     private static final Pattern STACK_TRACE_FIRST_LINE = Pattern.compile("^(?:[\\w+.-]+\\.)[\\w$]*?(?:"
613             + "Exception|Error|Throwable|Failure|Result|Abort|Fault|ThreadDeath|Overflow|Warning|"
614             + "NotSupported|NotFound|BadArgs|BadClassFile|Illegal|Invalid|Unexpected|Unchecked|Unmatched\\w+"
615             + ").*$");
616 
617     // Match exception causes, existing and omitted stack trace elements
618     private static final Pattern STACK_TRACE_OTHER_LINE =
619             Pattern.compile("^(?:Caused by:\\s.*|\\s*at .*|\\s*\\.\\.\\.\\s\\d+\\smore)$");
620 
621     // Match generic javac errors with 'javac:' prefix, JMV init and boot layer init errors
622     private static final Pattern JAVAC_OR_JVM_ERROR =
623             Pattern.compile("^(?:javac:|Error occurred during initialization of (?:boot layer|VM)).*", Pattern.DOTALL);
624 
625     /**
626      * Parse the output from the compiler into a list of CompilerMessage objects
627      *
628      * @param exitCode The exit code of javac.
629      * @param input    The output of the compiler
630      * @return List of CompilerMessage objects
631      * @throws IOException
632      */
633     static List<CompilerMessage> parseModernStream(int exitCode, BufferedReader input) throws IOException {
634         List<CompilerMessage> errors = new ArrayList<>();
635 
636         String line;
637 
638         StringBuilder buffer = new StringBuilder();
639 
640         boolean hasPointer = false;
641         int stackTraceLineCount = 0;
642 
643         while (true) {
644             line = input.readLine();
645 
646             if (line == null) {
647                 // javac output not detected by other parsing
648                 // maybe better to ignore only the summary and mark the rest as error
649                 String bufferAsString = buffer.toString();
650                 if (buffer.length() > 0) {
651                     if (JAVAC_OR_JVM_ERROR.matcher(bufferAsString).matches()) {
652                         errors.add(new CompilerMessage(bufferAsString, CompilerMessage.Kind.ERROR));
653                     } else if (hasPointer) {
654                         // A compiler message remains in buffer at end of parse stream
655                         errors.add(parseModernError(exitCode, bufferAsString));
656                     } else if (stackTraceLineCount > 0) {
657                         // Extract stack trace from end of buffer
658                         String[] lines = bufferAsString.split("\\R");
659                         int linesTotal = lines.length;
660                         buffer = new StringBuilder();
661                         int firstLine = linesTotal - stackTraceLineCount;
662 
663                         // Salvage Javac localized message 'javac.msg.bug' ("An exception has occurred in the
664                         // compiler ... Please file a bug")
665                         if (firstLine > 0) {
666                             final String lineBeforeStackTrace = lines[firstLine - 1];
667                             // One of those two URL substrings should always appear, without regard to JVM locale.
668                             // TODO: Update, if the URL changes, last checked for JDK 21.
669                             if (lineBeforeStackTrace.contains("java.sun.com/webapps/bugreport")
670                                     || lineBeforeStackTrace.contains("bugreport.java.com")) {
671                                 firstLine--;
672                             }
673                         }
674 
675                         // Note: For message 'javac.msg.proc.annotation.uncaught.exception' ("An annotation processor
676                         // threw an uncaught exception"), there is no locale-independent substring, and the header is
677                         // also multi-line. It was discarded in the removed method 'parseAnnotationProcessorStream',
678                         // and we continue to do so.
679 
680                         for (int i = firstLine; i < linesTotal; i++) {
681                             buffer.append(lines[i]).append(EOL);
682                         }
683                         errors.add(new CompilerMessage(buffer.toString(), CompilerMessage.Kind.ERROR));
684                     }
685                 }
686                 return errors;
687             }
688 
689             if (stackTraceLineCount == 0 && STACK_TRACE_FIRST_LINE.matcher(line).matches()
690                     || STACK_TRACE_OTHER_LINE.matcher(line).matches()) {
691                 stackTraceLineCount++;
692             } else {
693                 stackTraceLineCount = 0;
694             }
695 
696             // new error block?
697             if (!line.startsWith(" ") && hasPointer) {
698                 // add the error bean
699                 errors.add(parseModernError(exitCode, buffer.toString()));
700 
701                 // reset for next error block
702                 buffer = new StringBuilder(); // this is quicker than clearing it
703 
704                 hasPointer = false;
705             }
706 
707             // TODO: there should be a better way to parse these
708             if ((buffer.length() == 0) && line.startsWith("error: ")) {
709                 errors.add(new CompilerMessage(line, CompilerMessage.Kind.ERROR));
710             } else if ((buffer.length() == 0) && line.startsWith("warning: ")) {
711                 errors.add(new CompilerMessage(line, CompilerMessage.Kind.WARNING));
712             } else if ((buffer.length() == 0) && isNote(line)) {
713                 // skip, JDK 1.5 telling us deprecated APIs are used but -Xlint:deprecation isn't set
714             } else if ((buffer.length() == 0) && isMisc(line)) {
715                 // verbose output was set
716                 errors.add(new CompilerMessage(line, CompilerMessage.Kind.OTHER));
717             } else {
718                 buffer.append(line);
719 
720                 buffer.append(EOL);
721             }
722 
723             if (line.endsWith("^")) {
724                 hasPointer = true;
725             }
726         }
727     }
728 
729     private static boolean isMisc(String line) {
730         return startsWithPrefix(line, MISC_PREFIXES);
731     }
732 
733     private static boolean isNote(String line) {
734         return startsWithPrefix(line, NOTE_PREFIXES);
735     }
736 
737     private static boolean startsWithPrefix(String line, String[] prefixes) {
738         for (String prefix : prefixes) {
739             if (line.startsWith(prefix)) {
740                 return true;
741             }
742         }
743         return false;
744     }
745 
746     /**
747      * Construct a CompilerMessage object from a line of the compiler output
748      *
749      * @param exitCode The exit code from javac.
750      * @param error    output line from the compiler
751      * @return the CompilerMessage object
752      */
753     static CompilerMessage parseModernError(int exitCode, String error) {
754         final StringTokenizer tokens = new StringTokenizer(error, ":");
755 
756         boolean isError = exitCode != 0;
757 
758         try {
759             // With Java 6 error output lines from the compiler got longer. For backward compatibility
760             // .. and the time being, we eat up all (if any) tokens up to the erroneous file and source
761             // .. line indicator tokens.
762 
763             boolean tokenIsAnInteger;
764 
765             StringBuilder file = null;
766 
767             String currentToken = null;
768 
769             do {
770                 if (currentToken != null) {
771                     if (file == null) {
772                         file = new StringBuilder(currentToken);
773                     } else {
774                         file.append(':').append(currentToken);
775                     }
776                 }
777 
778                 currentToken = tokens.nextToken();
779 
780                 // Probably the only backward compatible means of checking if a string is an integer.
781 
782                 tokenIsAnInteger = true;
783 
784                 try {
785                     Integer.parseInt(currentToken);
786                 } catch (NumberFormatException e) {
787                     tokenIsAnInteger = false;
788                 }
789             } while (!tokenIsAnInteger);
790 
791             final String lineIndicator = currentToken;
792 
793             final int startOfFileName = file.toString().lastIndexOf(']');
794 
795             if (startOfFileName > -1) {
796                 file = new StringBuilder(file.substring(startOfFileName + 1 + EOL.length()));
797             }
798 
799             final int line = Integer.parseInt(lineIndicator);
800 
801             final StringBuilder msgBuffer = new StringBuilder();
802 
803             String msg = tokens.nextToken(EOL).substring(2);
804 
805             // Remove the 'warning: ' prefix
806             final String warnPrefix = getWarnPrefix(msg);
807             if (warnPrefix != null) {
808                 isError = false;
809                 msg = msg.substring(warnPrefix.length());
810             } else {
811                 isError = exitCode != 0;
812             }
813 
814             msgBuffer.append(msg);
815 
816             msgBuffer.append(EOL);
817 
818             String context = tokens.nextToken(EOL);
819 
820             String pointer = null;
821 
822             do {
823                 final String msgLine = tokens.nextToken(EOL);
824 
825                 if (pointer != null) {
826                     msgBuffer.append(msgLine);
827 
828                     msgBuffer.append(EOL);
829                 } else if (msgLine.endsWith("^")) {
830                     pointer = msgLine;
831                 } else {
832                     msgBuffer.append(context);
833 
834                     msgBuffer.append(EOL);
835 
836                     context = msgLine;
837                 }
838             } while (tokens.hasMoreTokens());
839 
840             msgBuffer.append(EOL);
841 
842             final String message = msgBuffer.toString();
843 
844             final int startcolumn = pointer.indexOf("^");
845 
846             int endcolumn = (context == null) ? startcolumn : context.indexOf(" ", startcolumn);
847 
848             if (endcolumn == -1) {
849                 endcolumn = context.length();
850             }
851 
852             return new CompilerMessage(file.toString(), isError, line, startcolumn, line, endcolumn, message.trim());
853         } catch (NoSuchElementException e) {
854             return new CompilerMessage("no more tokens - could not parse error message: " + error, isError);
855         } catch (Exception e) {
856             return new CompilerMessage("could not parse error message: " + error, isError);
857         }
858     }
859 
860     private static String getWarnPrefix(String msg) {
861         for (String warningPrefix : WARNING_PREFIXES) {
862             if (msg.startsWith(warningPrefix)) {
863                 return warningPrefix;
864             }
865         }
866         return null;
867     }
868 
869     /**
870      * put args into a temp file to be referenced using the @ option in javac command line
871      *
872      * @param args
873      * @return the temporary file wth the arguments
874      * @throws IOException
875      */
876     private File createFileWithArguments(String[] args, String outputDirectory) throws IOException {
877         PrintWriter writer = null;
878         try {
879             File tempFile;
880             if (getLog().isDebugEnabled()) {
881                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments", new File(outputDirectory));
882             } else {
883                 tempFile = File.createTempFile(JavacCompiler.class.getName(), "arguments");
884                 tempFile.deleteOnExit();
885             }
886 
887             writer = new PrintWriter(new FileWriter(tempFile));
888 
889             for (String arg : args) {
890                 String argValue = arg.replace(File.separatorChar, '/');
891 
892                 writer.write("\"" + argValue + "\"");
893 
894                 writer.println();
895             }
896 
897             writer.flush();
898 
899             return tempFile;
900 
901         } finally {
902             if (writer != null) {
903                 writer.close();
904             }
905         }
906     }
907 
908     /**
909      * Get the path of the javac tool executable to use.
910      * Either given through explicit configuration or via {@link #getJavacExecutable()}.
911      * @param config the configuration
912      * @return the path of the javac tool
913      */
914     protected String getJavacExecutable(CompilerConfiguration config) {
915         String executable = config.getExecutable();
916 
917         if (StringUtils.isEmpty(executable)) {
918             try {
919                 executable = getJavacExecutable();
920             } catch (IOException e) {
921                 if (getLog().isWarnEnabled()) {
922                     getLog().warn("Unable to autodetect 'javac' path, using 'javac' from the environment.");
923                 }
924                 executable = "javac";
925             }
926         }
927         return executable;
928     }
929 
930     /**
931      * Get the path of the javac tool executable: try to find it depending the OS or the <code>java.home</code>
932      * system property or the <code>JAVA_HOME</code> environment variable.
933      *
934      * @return the path of the javac tool
935      * @throws IOException if not found
936      */
937     private static String getJavacExecutable() throws IOException {
938         String javacCommand = "javac" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : "");
939 
940         String javaHome = System.getProperty("java.home");
941         File javacExe;
942         if (Os.isName("AIX")) {
943             javacExe = new File(javaHome + File.separator + ".." + File.separator + "sh", javacCommand);
944         } else if (Os.isName("Mac OS X")) {
945             javacExe = new File(javaHome + File.separator + "bin", javacCommand);
946         } else {
947             javacExe = new File(javaHome + File.separator + ".." + File.separator + "bin", javacCommand);
948         }
949 
950         // ----------------------------------------------------------------------
951         // Try to find javacExe from JAVA_HOME environment variable
952         // ----------------------------------------------------------------------
953         if (!javacExe.isFile()) {
954             Properties env = CommandLineUtils.getSystemEnvVars();
955             javaHome = env.getProperty("JAVA_HOME");
956             if (StringUtils.isEmpty(javaHome)) {
957                 throw new IOException("The environment variable JAVA_HOME is not correctly set.");
958             }
959             if (!new File(javaHome).isDirectory()) {
960                 throw new IOException("The environment variable JAVA_HOME=" + javaHome
961                         + " doesn't exist or is not a valid directory.");
962             }
963 
964             javacExe = new File(env.getProperty("JAVA_HOME") + File.separator + "bin", javacCommand);
965         }
966 
967         if (!javacExe.isFile()) {
968             throw new IOException("The javadoc executable '" + javacExe
969                     + "' doesn't exist or is not a file. Verify the JAVA_HOME environment variable.");
970         }
971 
972         return javacExe.getAbsolutePath();
973     }
974 
975     private void releaseJavaccClass(Class<?> javaccClass, CompilerConfiguration compilerConfiguration) {
976         if (compilerConfiguration.getCompilerReuseStrategy()
977                 == CompilerConfiguration.CompilerReuseStrategy.ReuseCreated) {
978             javacClasses.add(javaccClass);
979         }
980     }
981 
982     /**
983      * Find the main class of JavaC. Return the same class for subsequent calls.
984      *
985      * @return the non-null class.
986      * @throws CompilerException if the class has not been found.
987      */
988     private Class<?> getJavacClass(CompilerConfiguration compilerConfiguration) throws CompilerException {
989         Class<?> c;
990         switch (compilerConfiguration.getCompilerReuseStrategy()) {
991             case AlwaysNew:
992                 return createJavacClass();
993             case ReuseCreated:
994                 c = javacClasses.poll();
995                 if (c == null) {
996                     c = createJavacClass();
997                 }
998                 return c;
999             case ReuseSame:
1000             default:
1001                 c = javacClass;
1002                 if (c == null) {
1003                     synchronized (this) {
1004                         c = javacClass;
1005                         if (c == null) {
1006                             javacClass = c = createJavacClass();
1007                         }
1008                     }
1009                 }
1010                 return c;
1011         }
1012     }
1013 
1014     /**
1015      * Helper method for create Javac class
1016      */
1017     protected Class<?> createJavacClass() throws CompilerException {
1018         try {
1019             // look whether JavaC is on Maven's classpath
1020             // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, JavacCompiler.class.getClassLoader() );
1021             return JavacCompiler.class.getClassLoader().loadClass(JavacCompiler.JAVAC_CLASSNAME);
1022         } catch (ClassNotFoundException ex) {
1023             // ok
1024         }
1025 
1026         final File toolsJar = new File(System.getProperty("java.home"), "../lib/tools.jar");
1027         if (!toolsJar.exists()) {
1028             throw new CompilerException("tools.jar not found: " + toolsJar);
1029         }
1030 
1031         try {
1032             // Combined classloader with no parent/child relationship, so classes in our classloader
1033             // can reference classes in tools.jar
1034             URL[] originalUrls = ((URLClassLoader) JavacCompiler.class.getClassLoader()).getURLs();
1035             URL[] urls = new URL[originalUrls.length + 1];
1036             urls[0] = toolsJar.toURI().toURL();
1037             System.arraycopy(originalUrls, 0, urls, 1, originalUrls.length);
1038             ClassLoader javacClassLoader = new URLClassLoader(urls);
1039 
1040             final Thread thread = Thread.currentThread();
1041             final ClassLoader contextClassLoader = thread.getContextClassLoader();
1042             thread.setContextClassLoader(javacClassLoader);
1043             try {
1044                 // return Class.forName( JavacCompiler.JAVAC_CLASSNAME, true, javacClassLoader );
1045                 return javacClassLoader.loadClass(JavacCompiler.JAVAC_CLASSNAME);
1046             } finally {
1047                 thread.setContextClassLoader(contextClassLoader);
1048             }
1049         } catch (MalformedURLException ex) {
1050             throw new CompilerException(
1051                     "Could not convert the file reference to tools.jar to a URL, path to tools.jar: '"
1052                             + toolsJar.getAbsolutePath() + "'.",
1053                     ex);
1054         } catch (ClassNotFoundException ex) {
1055             throw new CompilerException(
1056                     "Unable to locate the Javac Compiler in:" + EOL + "  " + toolsJar + EOL
1057                             + "Please ensure you are using JDK 1.4 or above and" + EOL
1058                             + "not a JRE (the com.sun.tools.javac.Main class is required)." + EOL
1059                             + "In most cases you can change the location of your Java" + EOL
1060                             + "installation by setting the JAVA_HOME environment variable.",
1061                     ex);
1062         }
1063     }
1064 }