View Javadoc
1   package org.codehaus.plexus.compiler.eclipse;
2   
3   /**
4    * The MIT License
5    * <p>
6    * Copyright (c) 2005, The Codehaus
7    * <p>
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   * <p>
15   * The above copyright notice and this permission notice shall be included in all
16   * copies or substantial portions of the Software.
17   * <p>
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  import javax.inject.Named;
27  import javax.inject.Singleton;
28  import javax.tools.Diagnostic;
29  import javax.tools.DiagnosticListener;
30  import javax.tools.JavaCompiler;
31  import javax.tools.JavaFileObject;
32  import javax.tools.StandardJavaFileManager;
33  
34  import java.io.File;
35  import java.io.PrintWriter;
36  import java.io.StringWriter;
37  import java.nio.charset.Charset;
38  import java.nio.charset.IllegalCharsetNameException;
39  import java.nio.charset.UnsupportedCharsetException;
40  import java.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map.Entry;
46  import java.util.ServiceLoader;
47  
48  import org.codehaus.plexus.compiler.AbstractCompiler;
49  import org.codehaus.plexus.compiler.CompilerConfiguration;
50  import org.codehaus.plexus.compiler.CompilerException;
51  import org.codehaus.plexus.compiler.CompilerMessage;
52  import org.codehaus.plexus.compiler.CompilerOutputStyle;
53  import org.codehaus.plexus.compiler.CompilerResult;
54  import org.codehaus.plexus.util.StringUtils;
55  import org.eclipse.jdt.core.compiler.CompilationProgress;
56  import org.eclipse.jdt.core.compiler.batch.BatchCompiler;
57  
58  /**
59   *
60   */
61  @Named("eclipse")
62  @Singleton
63  public class EclipseJavaCompiler extends AbstractCompiler {
64      public EclipseJavaCompiler() {
65          super(CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE, ".java", ".class", null);
66      }
67  
68      // ----------------------------------------------------------------------
69      // Compiler Implementation
70      // ----------------------------------------------------------------------
71      boolean errorsAsWarnings = false;
72  
73      @Override
74      public String getCompilerId() {
75          return "eclipse";
76      }
77  
78      @Override
79      public CompilerResult performCompile(CompilerConfiguration config) throws CompilerException {
80          List<String> args = new ArrayList<>();
81          args.add("-noExit"); // Make sure ecj does not System.exit on us 8-/
82  
83          // Build settings from configuration
84          if (config.isDebug()) {
85              args.add("-preserveAllLocals");
86              args.add("-g:lines,vars,source");
87          } else {
88              args.add("-g:lines,source");
89          }
90  
91          String releaseVersion = decodeVersion(config.getReleaseVersion());
92          // EcjFailureException: Failed to run the ecj compiler: option -source is not supported when --release is used
93          if (releaseVersion != null) {
94              args.add("--release");
95              args.add(releaseVersion);
96          } else {
97              String sourceVersion = decodeVersion(config.getSourceVersion());
98  
99              if (sourceVersion != null) {
100                 args.add("-source");
101                 args.add(sourceVersion);
102             }
103 
104             String targetVersion = decodeVersion(config.getTargetVersion());
105 
106             if (targetVersion != null) {
107                 args.add("-target");
108                 args.add(targetVersion);
109             }
110         }
111 
112         if (StringUtils.isNotEmpty(config.getSourceEncoding())) {
113             args.add("-encoding");
114             args.add(config.getSourceEncoding());
115         }
116 
117         if (!config.isShowWarnings()) {
118             args.add("-warn:none");
119         } else {
120             String warnings = config.getWarnings();
121             StringBuilder warns =
122                     StringUtils.isEmpty(warnings) ? new StringBuilder() : new StringBuilder(warnings).append(',');
123 
124             if (config.isShowDeprecation()) {
125                 append(warns, "+deprecation");
126             } else {
127                 append(warns, "-deprecation");
128             }
129 
130             // -- Make room for more warnings to be enabled/disabled
131             args.add("-warn:" + warns);
132         }
133 
134         if (config.isParameters()) {
135             args.add("-parameters");
136         }
137 
138         if (config.isFailOnWarning()) {
139             args.add("-failOnWarning");
140         }
141 
142         // Set Eclipse-specific options
143         // compiler-specific extra options override anything else in the config object...
144         this.errorsAsWarnings = processCustomArguments(config, args);
145 
146         // Output path
147         args.add("-d");
148         args.add(config.getOutputLocation());
149 
150         // -- classpath
151         // must be done before annotation processors: https://bugs.eclipse.org/bugs/show_bug.cgi?id=573833
152         List<String> classpathEntries = new ArrayList<>(config.getClasspathEntries());
153         classpathEntries.add(config.getOutputLocation());
154         args.add("-classpath");
155         args.add(getPathString(classpathEntries));
156 
157         List<String> modulepathEntries = config.getModulepathEntries();
158         if (modulepathEntries != null && !modulepathEntries.isEmpty()) {
159             args.add("--module-path");
160             args.add(getPathString(modulepathEntries));
161         }
162 
163         // Annotation processors defined?
164         // must be done after classpath: https://bugs.eclipse.org/bugs/show_bug.cgi?id=573833
165         if (!isPreJava1_6(config)) {
166             File generatedSourcesDir = config.getGeneratedSourcesDirectory();
167             if (generatedSourcesDir != null) {
168                 generatedSourcesDir.mkdirs();
169 
170                 // -- option to specify where annotation processor is to generate its output
171                 args.add("-s");
172                 args.add(generatedSourcesDir.getAbsolutePath());
173             }
174 
175             // now add jdk 1.6 annotation processing related parameters
176             String annotationProcessors = joinAnnotationProcessors(config.getAnnotationProcessors());
177             List<String> processorPathEntries = config.getProcessorPathEntries();
178             List<String> processorModulePathEntries = config.getProcessorModulePathEntries();
179 
180             if (!annotationProcessors.isEmpty()
181                     || (processorPathEntries != null && processorPathEntries.size() > 0)
182                     || (processorModulePathEntries != null && processorModulePathEntries.size() > 0)) {
183                 if (!annotationProcessors.isEmpty()) {
184                     args.add("-processor");
185                     args.add(annotationProcessors);
186                 }
187 
188                 if (processorPathEntries != null && processorPathEntries.size() > 0) {
189                     if (isReplaceProcessorPath(config)) {
190                         args.add("--processor-module-path");
191                     } else {
192                         args.add("-processorpath");
193                     }
194                     args.add(getPathString(processorPathEntries));
195                 }
196 
197                 if (processorModulePathEntries != null && processorModulePathEntries.size() > 0) {
198                     args.add("--processor-module-path");
199                     args.add(getPathString(processorModulePathEntries));
200                 }
201 
202                 if (config.getProc() != null) {
203                     args.add("-proc:" + config.getProc());
204                 }
205             }
206         }
207 
208         // Collect sources
209         List<String> allSources = Arrays.asList(getSourceFiles(config));
210         List<CompilerMessage> messageList = new ArrayList<>();
211         if (allSources.isEmpty()) {
212             // -- Nothing to do -> bail out
213             return new CompilerResult(true, messageList);
214         }
215 
216         allSources = resortSourcesToPutModuleInfoFirst(allSources);
217 
218         logCompiling(null, config);
219 
220         // Compile
221         try {
222             StringWriter sw = new StringWriter();
223             PrintWriter devNull = new PrintWriter(sw);
224             JavaCompiler compiler = getEcj();
225             boolean success = false;
226             if (compiler != null) {
227                 getLog().debug("Using JSR-199 EclipseCompiler");
228                 // ECJ JSR-199 compiles against the latest Java version it supports if no source
229                 // version is given explicitly. BatchCompiler uses 1.3 as default. So check
230                 // whether a source version is specified, and if not supply 8 explicitly.
231                 if (!haveSourceOrReleaseArgument(args)) {
232                     getLog().debug("ecj: no source level nor release specified, defaulting to Java 8");
233                     args.add("-source");
234                     args.add("8");
235                 }
236 
237                 // Also check for the encoding. Could have been set via the CompilerConfig
238                 // above, or also via the arguments explicitly. We need the charset for the
239                 // StandardJavaFileManager below.
240                 String encoding = null;
241                 Iterator<String> allArgs = args.iterator();
242                 while (encoding == null && allArgs.hasNext()) {
243                     String option = allArgs.next();
244                     if ("-encoding".equals(option) && allArgs.hasNext()) {
245                         encoding = allArgs.next();
246                     }
247                 }
248                 final Locale defaultLocale = Locale.getDefault();
249                 final List<CompilerMessage> messages = messageList;
250                 DiagnosticListener<? super JavaFileObject> messageCollector = new DiagnosticListener<JavaFileObject>() {
251 
252                     @Override
253                     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
254                         // Convert to Plexus' CompilerMessage and append to messageList
255                         String fileName = "Unknown source";
256                         try {
257                             JavaFileObject file = diagnostic.getSource();
258                             if (file != null) {
259                                 fileName = file.getName();
260                             }
261                         } catch (NullPointerException e) {
262                             // ECJ bug: diagnostic.getSource() may throw an NPE if there is no source
263                         }
264                         long startColumn = diagnostic.getColumnNumber();
265                         // endColumn may be wrong if the endPosition is not on the same line.
266                         long endColumn = startColumn + (diagnostic.getEndPosition() - diagnostic.getStartPosition());
267                         CompilerMessage message = new CompilerMessage(
268                                 fileName,
269                                 convert(diagnostic.getKind()),
270                                 (int) diagnostic.getLineNumber(),
271                                 (int) startColumn,
272                                 (int) diagnostic.getLineNumber(),
273                                 (int) endColumn,
274                                 diagnostic.getMessage(defaultLocale));
275                         messages.add(message);
276                     }
277                 };
278                 Charset charset = null;
279                 if (encoding != null) {
280                     encoding = encoding.trim();
281                     try {
282                         charset = Charset.forName(encoding);
283                     } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
284                         getLog().warn("ecj: invalid or unsupported character set '" + encoding + "', using default");
285                         // charset remains null
286                     }
287                 }
288                 if (charset == null) {
289                     charset = Charset.defaultCharset();
290                 }
291                 if (getLog().isDebugEnabled()) {
292                     getLog().debug("ecj: using character set " + charset.displayName());
293                     getLog().debug("ecj command line: " + args);
294                     getLog().debug("ecj input source files: " + allSources);
295                 }
296 
297                 try (StandardJavaFileManager manager =
298                         compiler.getStandardFileManager(messageCollector, defaultLocale, charset)) {
299                     Iterable<? extends JavaFileObject> units = manager.getJavaFileObjectsFromStrings(allSources);
300                     success =
301                             Boolean.TRUE.equals(compiler.getTask(devNull, manager, messageCollector, args, null, units)
302                                     .call());
303                 } catch (RuntimeException e) {
304                     throw new EcjFailureException(e.getLocalizedMessage());
305                 }
306                 getLog().debug(sw.toString());
307             } else {
308                 // Use the BatchCompiler and send all errors to xml temp file.
309                 File errorF = null;
310                 try {
311                     errorF = File.createTempFile("ecjerr-", ".xml");
312                     getLog().debug("Using legacy BatchCompiler; error file " + errorF);
313 
314                     args.add("-log");
315                     args.add(errorF.toString());
316                     args.addAll(allSources);
317 
318                     getLog().debug("ecj command line: " + args);
319 
320                     success = BatchCompiler.compile(
321                             args.toArray(new String[args.size()]), devNull, devNull, new CompilationProgress() {
322                                 @Override
323                                 public void begin(int i) {}
324 
325                                 @Override
326                                 public void done() {}
327 
328                                 @Override
329                                 public boolean isCanceled() {
330                                     return false;
331                                 }
332 
333                                 @Override
334                                 public void setTaskName(String s) {}
335 
336                                 @Override
337                                 public void worked(int i, int i1) {}
338                             });
339                     getLog().debug(sw.toString());
340 
341                     if (errorF.length() < 80) {
342                         throw new EcjFailureException(sw.toString());
343                     }
344                     messageList = new EcjResponseParser().parse(errorF, errorsAsWarnings);
345                 } finally {
346                     if (null != errorF) {
347                         try {
348                             errorF.delete();
349                         } catch (Exception x) {
350                         }
351                     }
352                 }
353             }
354             boolean hasError = false;
355             for (CompilerMessage compilerMessage : messageList) {
356                 if (compilerMessage.isError()) {
357                     hasError = true;
358                     break;
359                 }
360             }
361             if (!hasError && !success && !errorsAsWarnings) {
362                 CompilerMessage.Kind kind =
363                         errorsAsWarnings ? CompilerMessage.Kind.WARNING : CompilerMessage.Kind.ERROR;
364 
365                 // -- Compiler reported failure but we do not seem to have one -> probable
366                 // exception
367                 CompilerMessage cm = new CompilerMessage(
368                         "[ecj] The compiler reported an error but has not written it to its logging", kind);
369                 messageList.add(cm);
370                 hasError = true;
371 
372                 // -- Try to find the actual message by reporting the last 5 lines as a message
373                 String stdout = getLastLines(sw.toString(), 5);
374                 if (stdout.length() > 0) {
375                     cm = new CompilerMessage("[ecj] The following line(s) might indicate the issue:\n" + stdout, kind);
376                     messageList.add(cm);
377                 }
378             }
379             return new CompilerResult(!hasError || errorsAsWarnings, messageList);
380         } catch (EcjFailureException x) {
381             throw x;
382         } catch (Exception x) {
383             throw new RuntimeException(x); // sigh
384         }
385     }
386 
387     private static final String OPT_REPLACE_PROCESSOR_PATH = "replaceProcessorPathWithProcessorModulePath";
388     private static final String OPT_REPLACE_PROCESSOR_PATH_ = "-" + OPT_REPLACE_PROCESSOR_PATH;
389 
390     static boolean isReplaceProcessorPath(CompilerConfiguration config) {
391         for (Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
392             String opt = entry.getKey();
393             if (opt.equals(OPT_REPLACE_PROCESSOR_PATH) || opt.equals(OPT_REPLACE_PROCESSOR_PATH_)) {
394                 return true;
395             }
396         }
397         return false;
398     }
399 
400     static List<String> resortSourcesToPutModuleInfoFirst(List<String> allSources) {
401         List<String> resorted = new ArrayList<>(allSources.size());
402 
403         for (String mi : allSources) {
404             if (mi.endsWith("module-info.java")) {
405                 resorted.add(mi);
406             }
407         }
408 
409         for (String nmi : allSources) {
410             if (!nmi.endsWith("module-info.java")) {
411                 resorted.add(nmi);
412             }
413         }
414 
415         return resorted;
416     }
417 
418     static boolean processCustomArguments(CompilerConfiguration config, List<String> args) {
419         boolean result = false;
420 
421         for (Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
422             String opt = entry.getKey();
423             String optionValue = entry.getValue();
424 
425             // handle errorsAsWarnings options
426             if (opt.equals("errorsAsWarnings") || opt.equals("-errorsAsWarnings")) {
427                 result = true;
428                 continue;
429             }
430 
431             if (opt.equals("-properties")) {
432                 if (null != optionValue) {
433                     File propFile = new File(optionValue);
434                     if (!propFile.exists() || !propFile.isFile()) {
435                         throw new IllegalArgumentException(
436                                 "Properties file specified by -properties " + propFile + " does not exist");
437                     }
438                 }
439             }
440 
441             // -- Write .class files even when error occur, but make sure methods with compile errors do abort when
442             // called
443             if (opt.equals("-proceedOnError")) {
444                 // Generate a class file even with errors, but make methods with errors fail when called
445                 args.add("-proceedOnError:Fatal");
446                 continue;
447             }
448 
449             if (!opt.equals(OPT_REPLACE_PROCESSOR_PATH) && !opt.equals(OPT_REPLACE_PROCESSOR_PATH_)) {
450                 /*
451                  * The compiler mojo makes quite a mess of passing arguments, depending on exactly WHICH
452                  * way is used to pass them. The method method using <compilerArguments> uses the tag names
453                  * of its contents to denote option names, and so the compiler mojo happily adds a '-' to
454                  * all of the names there and adds them to the "custom compiler arguments" map as a
455                  * name, value pair where the name always contains a single '-'. The Eclipse compiler (and
456                  * javac too, btw) has options with two dashes (like --add-modules for java 9). These cannot
457                  * be passed using a <compilerArguments> tag.
458                  *
459                  * The other method is to use <compilerArgs>, where each SINGLE argument needs to be passed
460                  * using an <arg>xxxx</arg> tag. In there the xxx is not manipulated by the compiler mojo, so
461                  * if it starts with a dash or more dashes these are perfectly preserved. But of course these
462                  * single <arg> entries are not a pair. So the compiler mojo adds them as pairs of (xxxx, null).
463                  *
464                  * We use that knowledge here: if a pair has a null value then do not mess up the key but
465                  * render it as a single value. This should ensure that something like:
466                  * <compilerArgs>
467                  *     <arg>--add-modules</arg>
468                  *     <arg>java.se.ee</arg>
469                  * </compilerArgs>
470                  *
471                  * is actually added to the command like as such.
472                  *
473                  * (btw: the above example will still give an error when using ecj <= 4.8M6:
474                  *      invalid module name: java.se.ee
475                  * but that seems to be a bug in ecj).
476                  */
477                 if (null == optionValue) {
478                     // -- We have an option from compilerArgs: use the key as-is as a single option value
479                     args.add(opt);
480                 } else {
481                     if (!opt.startsWith("-")) {
482                         opt = "-" + opt;
483                     }
484                     args.add(opt);
485                     args.add(optionValue);
486                 }
487             }
488         }
489         return result;
490     }
491 
492     private static boolean haveSourceOrReleaseArgument(List<String> args) {
493         Iterator<String> allArgs = args.iterator();
494         while (allArgs.hasNext()) {
495             String option = allArgs.next();
496             if (("-source".equals(option) || "--release".equals(option)) && allArgs.hasNext()) {
497                 return true;
498             }
499         }
500         return false;
501     }
502 
503     private JavaCompiler getEcj() {
504         ServiceLoader<JavaCompiler> javaCompilerLoader =
505                 ServiceLoader.load(JavaCompiler.class, BatchCompiler.class.getClassLoader());
506         Class<?> c = null;
507         try {
508             c = Class.forName(
509                     "org.eclipse.jdt.internal.compiler.tool.EclipseCompiler",
510                     false,
511                     BatchCompiler.class.getClassLoader());
512         } catch (ClassNotFoundException e) {
513             // Ignore
514         }
515         if (c != null) {
516             for (JavaCompiler javaCompiler : javaCompilerLoader) {
517                 if (c.isInstance(javaCompiler)) {
518                     return javaCompiler;
519                 }
520             }
521         }
522         getLog().debug("Cannot find org.eclipse.jdt.internal.compiler.tool.EclipseCompiler");
523         return null;
524     }
525 
526     private CompilerMessage.Kind convert(Diagnostic.Kind kind) {
527         if (kind == null) {
528             return CompilerMessage.Kind.OTHER;
529         }
530         switch (kind) {
531             case ERROR:
532                 return errorsAsWarnings ? CompilerMessage.Kind.WARNING : CompilerMessage.Kind.ERROR;
533             case WARNING:
534                 return CompilerMessage.Kind.WARNING;
535             case MANDATORY_WARNING:
536                 return CompilerMessage.Kind.MANDATORY_WARNING;
537             case NOTE:
538                 return CompilerMessage.Kind.NOTE;
539             case OTHER:
540             default:
541                 return CompilerMessage.Kind.OTHER;
542         }
543     }
544 
545     private String getLastLines(String text, int lines) {
546         List<String> lineList = new ArrayList<>();
547         text = text.replace("\r\n", "\n");
548         text = text.replace("\r", "\n"); // make sure eoln is \n
549 
550         int index = text.length();
551         while (index > 0) {
552             int before = text.lastIndexOf('\n', index - 1);
553 
554             if (before + 1 < index) { // Non empty line?
555                 lineList.add(text.substring(before + 1, index));
556                 lines--;
557                 if (lines <= 0) {
558                     break;
559                 }
560             }
561 
562             index = before;
563         }
564 
565         StringBuilder sb = new StringBuilder();
566         for (int i = lineList.size() - 1; i >= 0; i--) {
567             String s = lineList.get(i);
568             sb.append(s);
569             sb.append(System.getProperty("line.separator")); // 8-/
570         }
571         return sb.toString();
572     }
573 
574     private static void append(StringBuilder warns, String s) {
575         if (warns.length() > 0) {
576             warns.append(',');
577         }
578         warns.append(s);
579     }
580 
581     private boolean isPreJava1_6(CompilerConfiguration config) {
582         String s = config.getSourceVersion();
583         if (s == null) {
584             // now return true, as the 1.6 version is not the default - 1.4 is.
585             return true;
586         }
587         return s.startsWith("1.5")
588                 || s.startsWith("1.4")
589                 || s.startsWith("1.3")
590                 || s.startsWith("1.2")
591                 || s.startsWith("1.1")
592                 || s.startsWith("1.0");
593     }
594 
595     @Override
596     public String[] createCommandLine(CompilerConfiguration config) throws CompilerException {
597         return null;
598     }
599 
600     @Override
601     public boolean supportsIncrementalCompilation() {
602         return true;
603     }
604 
605     /**
606      * Change any Maven Java version number to ECJ's version number. Do not check the validity
607      * of the version: the compiler does that nicely, and this allows compiler updates without
608      * changing the compiler plugin. This is important with the half year release cycle for Java.
609      */
610     private String decodeVersion(String versionSpec) {
611         if (StringUtils.isEmpty(versionSpec)) {
612             return null;
613         }
614 
615         if (versionSpec.equals("1.9")) {
616             getLog().warn("Version 9 should be specified as 9, not 1.9");
617             return "9";
618         }
619         return versionSpec;
620     }
621 }