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 = config.getAnnotationProcessors();
177             List<String> processorPathEntries = config.getProcessorPathEntries();
178             List<String> processorModulePathEntries = config.getProcessorModulePathEntries();
179 
180             if ((annotationProcessors != null && annotationProcessors.length > 0)
181                     || (processorPathEntries != null && processorPathEntries.size() > 0)
182                     || (processorModulePathEntries != null && processorModulePathEntries.size() > 0)) {
183                 if (annotationProcessors != null && annotationProcessors.length > 0) {
184                     args.add("-processor");
185                     StringBuilder sb = new StringBuilder();
186                     for (String ap : annotationProcessors) {
187                         if (sb.length() > 0) {
188                             sb.append(',');
189                         }
190                         sb.append(ap);
191                     }
192                     args.add(sb.toString());
193                 }
194 
195                 if (processorPathEntries != null && processorPathEntries.size() > 0) {
196                     if (isReplaceProcessorPath(config)) {
197                         args.add("--processor-module-path");
198                     } else {
199                         args.add("-processorpath");
200                     }
201                     args.add(getPathString(processorPathEntries));
202                 }
203 
204                 if (processorModulePathEntries != null && processorModulePathEntries.size() > 0) {
205                     args.add("--processor-module-path");
206                     args.add(getPathString(processorModulePathEntries));
207                 }
208 
209                 if (config.getProc() != null) {
210                     args.add("-proc:" + config.getProc());
211                 }
212             }
213         }
214 
215         // Collect sources
216         List<String> allSources = Arrays.asList(getSourceFiles(config));
217         List<CompilerMessage> messageList = new ArrayList<>();
218         if (allSources.isEmpty()) {
219             // -- Nothing to do -> bail out
220             return new CompilerResult(true, messageList);
221         }
222 
223         allSources = resortSourcesToPutModuleInfoFirst(allSources);
224 
225         logCompiling(null, config);
226 
227         // Compile
228         try {
229             StringWriter sw = new StringWriter();
230             PrintWriter devNull = new PrintWriter(sw);
231             JavaCompiler compiler = getEcj();
232             boolean success = false;
233             if (compiler != null) {
234                 getLog().debug("Using JSR-199 EclipseCompiler");
235                 // ECJ JSR-199 compiles against the latest Java version it supports if no source
236                 // version is given explicitly. BatchCompiler uses 1.3 as default. So check
237                 // whether a source version is specified, and if not supply 1.3 explicitly.
238                 if (!haveSourceOrReleaseArgument(args)) {
239                     getLog().debug("ecj: no source level nor release specified, defaulting to Java 1.3");
240                     args.add("-source");
241                     args.add("1.3");
242                 }
243 
244                 // Also check for the encoding. Could have been set via the CompilerConfig
245                 // above, or also via the arguments explicitly. We need the charset for the
246                 // StandardJavaFileManager below.
247                 String encoding = null;
248                 Iterator<String> allArgs = args.iterator();
249                 while (encoding == null && allArgs.hasNext()) {
250                     String option = allArgs.next();
251                     if ("-encoding".equals(option) && allArgs.hasNext()) {
252                         encoding = allArgs.next();
253                     }
254                 }
255                 final Locale defaultLocale = Locale.getDefault();
256                 final List<CompilerMessage> messages = messageList;
257                 DiagnosticListener<? super JavaFileObject> messageCollector = new DiagnosticListener<JavaFileObject>() {
258 
259                     @Override
260                     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
261                         // Convert to Plexus' CompilerMessage and append to messageList
262                         String fileName = "Unknown source";
263                         try {
264                             JavaFileObject file = diagnostic.getSource();
265                             if (file != null) {
266                                 fileName = file.getName();
267                             }
268                         } catch (NullPointerException e) {
269                             // ECJ bug: diagnostic.getSource() may throw an NPE if there is no source
270                         }
271                         long startColumn = diagnostic.getColumnNumber();
272                         // endColumn may be wrong if the endPosition is not on the same line.
273                         long endColumn = startColumn + (diagnostic.getEndPosition() - diagnostic.getStartPosition());
274                         CompilerMessage message = new CompilerMessage(
275                                 fileName,
276                                 convert(diagnostic.getKind()),
277                                 (int) diagnostic.getLineNumber(),
278                                 (int) startColumn,
279                                 (int) diagnostic.getLineNumber(),
280                                 (int) endColumn,
281                                 diagnostic.getMessage(defaultLocale));
282                         messages.add(message);
283                     }
284                 };
285                 Charset charset = null;
286                 if (encoding != null) {
287                     encoding = encoding.trim();
288                     try {
289                         charset = Charset.forName(encoding);
290                     } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
291                         getLog().warn("ecj: invalid or unsupported character set '" + encoding + "', using default");
292                         // charset remains null
293                     }
294                 }
295                 if (charset == null) {
296                     charset = Charset.defaultCharset();
297                 }
298                 if (getLog().isDebugEnabled()) {
299                     getLog().debug("ecj: using character set " + charset.displayName());
300                     getLog().debug("ecj command line: " + args);
301                     getLog().debug("ecj input source files: " + allSources);
302                 }
303 
304                 try (StandardJavaFileManager manager =
305                         compiler.getStandardFileManager(messageCollector, defaultLocale, charset)) {
306                     Iterable<? extends JavaFileObject> units = manager.getJavaFileObjectsFromStrings(allSources);
307                     success =
308                             Boolean.TRUE.equals(compiler.getTask(devNull, manager, messageCollector, args, null, units)
309                                     .call());
310                 } catch (RuntimeException e) {
311                     throw new EcjFailureException(e.getLocalizedMessage());
312                 }
313                 getLog().debug(sw.toString());
314             } else {
315                 // Use the BatchCompiler and send all errors to xml temp file.
316                 File errorF = null;
317                 try {
318                     errorF = File.createTempFile("ecjerr-", ".xml");
319                     getLog().debug("Using legacy BatchCompiler; error file " + errorF);
320 
321                     args.add("-log");
322                     args.add(errorF.toString());
323                     args.addAll(allSources);
324 
325                     getLog().debug("ecj command line: " + args);
326 
327                     success = BatchCompiler.compile(
328                             args.toArray(new String[args.size()]), devNull, devNull, new CompilationProgress() {
329                                 @Override
330                                 public void begin(int i) {}
331 
332                                 @Override
333                                 public void done() {}
334 
335                                 @Override
336                                 public boolean isCanceled() {
337                                     return false;
338                                 }
339 
340                                 @Override
341                                 public void setTaskName(String s) {}
342 
343                                 @Override
344                                 public void worked(int i, int i1) {}
345                             });
346                     getLog().debug(sw.toString());
347 
348                     if (errorF.length() < 80) {
349                         throw new EcjFailureException(sw.toString());
350                     }
351                     messageList = new EcjResponseParser().parse(errorF, errorsAsWarnings);
352                 } finally {
353                     if (null != errorF) {
354                         try {
355                             errorF.delete();
356                         } catch (Exception x) {
357                         }
358                     }
359                 }
360             }
361             boolean hasError = false;
362             for (CompilerMessage compilerMessage : messageList) {
363                 if (compilerMessage.isError()) {
364                     hasError = true;
365                     break;
366                 }
367             }
368             if (!hasError && !success && !errorsAsWarnings) {
369                 CompilerMessage.Kind kind =
370                         errorsAsWarnings ? CompilerMessage.Kind.WARNING : CompilerMessage.Kind.ERROR;
371 
372                 // -- Compiler reported failure but we do not seem to have one -> probable
373                 // exception
374                 CompilerMessage cm = new CompilerMessage(
375                         "[ecj] The compiler reported an error but has not written it to its logging", kind);
376                 messageList.add(cm);
377                 hasError = true;
378 
379                 // -- Try to find the actual message by reporting the last 5 lines as a message
380                 String stdout = getLastLines(sw.toString(), 5);
381                 if (stdout.length() > 0) {
382                     cm = new CompilerMessage("[ecj] The following line(s) might indicate the issue:\n" + stdout, kind);
383                     messageList.add(cm);
384                 }
385             }
386             return new CompilerResult(!hasError || errorsAsWarnings, messageList);
387         } catch (EcjFailureException x) {
388             throw x;
389         } catch (Exception x) {
390             throw new RuntimeException(x); // sigh
391         }
392     }
393 
394     private static final String OPT_REPLACE_PROCESSOR_PATH = "replaceProcessorPathWithProcessorModulePath";
395     private static final String OPT_REPLACE_PROCESSOR_PATH_ = "-" + OPT_REPLACE_PROCESSOR_PATH;
396 
397     static boolean isReplaceProcessorPath(CompilerConfiguration config) {
398         for (Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
399             String opt = entry.getKey();
400             if (opt.equals(OPT_REPLACE_PROCESSOR_PATH) || opt.equals(OPT_REPLACE_PROCESSOR_PATH_)) {
401                 return true;
402             }
403         }
404         return false;
405     }
406 
407     static List<String> resortSourcesToPutModuleInfoFirst(List<String> allSources) {
408         List<String> resorted = new ArrayList<>(allSources.size());
409 
410         for (String mi : allSources) {
411             if (mi.endsWith("module-info.java")) {
412                 resorted.add(mi);
413             }
414         }
415 
416         for (String nmi : allSources) {
417             if (!nmi.endsWith("module-info.java")) {
418                 resorted.add(nmi);
419             }
420         }
421 
422         return resorted;
423     }
424 
425     static boolean processCustomArguments(CompilerConfiguration config, List<String> args) {
426         boolean result = false;
427 
428         for (Entry<String, String> entry : config.getCustomCompilerArgumentsEntries()) {
429             String opt = entry.getKey();
430             String optionValue = entry.getValue();
431 
432             // handle errorsAsWarnings options
433             if (opt.equals("errorsAsWarnings") || opt.equals("-errorsAsWarnings")) {
434                 result = true;
435                 continue;
436             }
437 
438             if (opt.equals("-properties")) {
439                 if (null != optionValue) {
440                     File propFile = new File(optionValue);
441                     if (!propFile.exists() || !propFile.isFile()) {
442                         throw new IllegalArgumentException(
443                                 "Properties file specified by -properties " + propFile + " does not exist");
444                     }
445                 }
446             }
447 
448             // -- Write .class files even when error occur, but make sure methods with compile errors do abort when
449             // called
450             if (opt.equals("-proceedOnError")) {
451                 // Generate a class file even with errors, but make methods with errors fail when called
452                 args.add("-proceedOnError:Fatal");
453                 continue;
454             }
455 
456             if (!opt.equals(OPT_REPLACE_PROCESSOR_PATH) && !opt.equals(OPT_REPLACE_PROCESSOR_PATH_)) {
457                 /*
458                  * The compiler mojo makes quite a mess of passing arguments, depending on exactly WHICH
459                  * way is used to pass them. The method method using <compilerArguments> uses the tag names
460                  * of its contents to denote option names, and so the compiler mojo happily adds a '-' to
461                  * all of the names there and adds them to the "custom compiler arguments" map as a
462                  * name, value pair where the name always contains a single '-'. The Eclipse compiler (and
463                  * javac too, btw) has options with two dashes (like --add-modules for java 9). These cannot
464                  * be passed using a <compilerArguments> tag.
465                  *
466                  * The other method is to use <compilerArgs>, where each SINGLE argument needs to be passed
467                  * using an <arg>xxxx</arg> tag. In there the xxx is not manipulated by the compiler mojo, so
468                  * if it starts with a dash or more dashes these are perfectly preserved. But of course these
469                  * single <arg> entries are not a pair. So the compiler mojo adds them as pairs of (xxxx, null).
470                  *
471                  * We use that knowledge here: if a pair has a null value then do not mess up the key but
472                  * render it as a single value. This should ensure that something like:
473                  * <compilerArgs>
474                  *     <arg>--add-modules</arg>
475                  *     <arg>java.se.ee</arg>
476                  * </compilerArgs>
477                  *
478                  * is actually added to the command like as such.
479                  *
480                  * (btw: the above example will still give an error when using ecj <= 4.8M6:
481                  *      invalid module name: java.se.ee
482                  * but that seems to be a bug in ecj).
483                  */
484                 if (null == optionValue) {
485                     // -- We have an option from compilerArgs: use the key as-is as a single option value
486                     args.add(opt);
487                 } else {
488                     if (!opt.startsWith("-")) {
489                         opt = "-" + opt;
490                     }
491                     args.add(opt);
492                     args.add(optionValue);
493                 }
494             }
495         }
496         return result;
497     }
498 
499     private static boolean haveSourceOrReleaseArgument(List<String> args) {
500         Iterator<String> allArgs = args.iterator();
501         while (allArgs.hasNext()) {
502             String option = allArgs.next();
503             if (("-source".equals(option) || "--release".equals(option)) && allArgs.hasNext()) {
504                 return true;
505             }
506         }
507         return false;
508     }
509 
510     private JavaCompiler getEcj() {
511         ServiceLoader<JavaCompiler> javaCompilerLoader =
512                 ServiceLoader.load(JavaCompiler.class, BatchCompiler.class.getClassLoader());
513         Class<?> c = null;
514         try {
515             c = Class.forName(
516                     "org.eclipse.jdt.internal.compiler.tool.EclipseCompiler",
517                     false,
518                     BatchCompiler.class.getClassLoader());
519         } catch (ClassNotFoundException e) {
520             // Ignore
521         }
522         if (c != null) {
523             for (JavaCompiler javaCompiler : javaCompilerLoader) {
524                 if (c.isInstance(javaCompiler)) {
525                     return javaCompiler;
526                 }
527             }
528         }
529         getLog().debug("Cannot find org.eclipse.jdt.internal.compiler.tool.EclipseCompiler");
530         return null;
531     }
532 
533     private CompilerMessage.Kind convert(Diagnostic.Kind kind) {
534         if (kind == null) {
535             return CompilerMessage.Kind.OTHER;
536         }
537         switch (kind) {
538             case ERROR:
539                 return errorsAsWarnings ? CompilerMessage.Kind.WARNING : CompilerMessage.Kind.ERROR;
540             case WARNING:
541                 return CompilerMessage.Kind.WARNING;
542             case MANDATORY_WARNING:
543                 return CompilerMessage.Kind.MANDATORY_WARNING;
544             case NOTE:
545                 return CompilerMessage.Kind.NOTE;
546             case OTHER:
547             default:
548                 return CompilerMessage.Kind.OTHER;
549         }
550     }
551 
552     private String getLastLines(String text, int lines) {
553         List<String> lineList = new ArrayList<>();
554         text = text.replace("\r\n", "\n");
555         text = text.replace("\r", "\n"); // make sure eoln is \n
556 
557         int index = text.length();
558         while (index > 0) {
559             int before = text.lastIndexOf('\n', index - 1);
560 
561             if (before + 1 < index) { // Non empty line?
562                 lineList.add(text.substring(before + 1, index));
563                 lines--;
564                 if (lines <= 0) {
565                     break;
566                 }
567             }
568 
569             index = before;
570         }
571 
572         StringBuilder sb = new StringBuilder();
573         for (int i = lineList.size() - 1; i >= 0; i--) {
574             String s = lineList.get(i);
575             sb.append(s);
576             sb.append(System.getProperty("line.separator")); // 8-/
577         }
578         return sb.toString();
579     }
580 
581     private static void append(StringBuilder warns, String s) {
582         if (warns.length() > 0) {
583             warns.append(',');
584         }
585         warns.append(s);
586     }
587 
588     private boolean isPreJava1_6(CompilerConfiguration config) {
589         String s = config.getSourceVersion();
590         if (s == null) {
591             // now return true, as the 1.6 version is not the default - 1.4 is.
592             return true;
593         }
594         return s.startsWith("1.5")
595                 || s.startsWith("1.4")
596                 || s.startsWith("1.3")
597                 || s.startsWith("1.2")
598                 || s.startsWith("1.1")
599                 || s.startsWith("1.0");
600     }
601 
602     @Override
603     public String[] createCommandLine(CompilerConfiguration config) throws CompilerException {
604         return null;
605     }
606 
607     @Override
608     public boolean supportsIncrementalCompilation() {
609         return true;
610     }
611 
612     /**
613      * Change any Maven Java version number to ECJ's version number. Do not check the validity
614      * of the version: the compiler does that nicely, and this allows compiler updates without
615      * changing the compiler plugin. This is important with the half year release cycle for Java.
616      */
617     private String decodeVersion(String versionSpec) {
618         if (StringUtils.isEmpty(versionSpec)) {
619             return null;
620         }
621 
622         if (versionSpec.equals("1.9")) {
623             getLog().warn("Version 9 should be specified as 9, not 1.9");
624             return "9";
625         }
626         return versionSpec;
627     }
628 }