View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.pmd.exec;
20  
21  import java.io.Closeable;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.Writer;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Objects;
33  
34  import net.sourceforge.pmd.PMDConfiguration;
35  import net.sourceforge.pmd.PmdAnalysis;
36  import net.sourceforge.pmd.Report;
37  import net.sourceforge.pmd.RulePriority;
38  import net.sourceforge.pmd.RuleSetLoadException;
39  import net.sourceforge.pmd.RuleSetLoader;
40  import net.sourceforge.pmd.RuleViolation;
41  import net.sourceforge.pmd.benchmark.TextTimingReportRenderer;
42  import net.sourceforge.pmd.benchmark.TimeTracker;
43  import net.sourceforge.pmd.benchmark.TimingReport;
44  import net.sourceforge.pmd.benchmark.TimingReportRenderer;
45  import net.sourceforge.pmd.lang.Language;
46  import net.sourceforge.pmd.lang.LanguageRegistry;
47  import net.sourceforge.pmd.lang.LanguageVersion;
48  import net.sourceforge.pmd.renderers.CSVRenderer;
49  import net.sourceforge.pmd.renderers.HTMLRenderer;
50  import net.sourceforge.pmd.renderers.Renderer;
51  import net.sourceforge.pmd.renderers.TextRenderer;
52  import net.sourceforge.pmd.renderers.XMLRenderer;
53  import net.sourceforge.pmd.util.Predicate;
54  import org.apache.maven.plugin.MojoExecutionException;
55  import org.apache.maven.plugins.pmd.ExcludeViolationsFromFile;
56  import org.apache.maven.reporting.MavenReportException;
57  import org.codehaus.plexus.util.FileUtils;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Executes PMD with the configuration provided via {@link PmdRequest}.
63   */
64  public class PmdExecutor extends Executor {
65      private static final Logger LOG = LoggerFactory.getLogger(PmdExecutor.class);
66  
67      public static PmdResult execute(PmdRequest request) throws MavenReportException {
68          if (request.getJavaExecutable() != null) {
69              return fork(request);
70          }
71  
72          // make sure the class loaders are correct and call this in the same JVM
73          ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
74          try {
75              Thread.currentThread().setContextClassLoader(PmdExecutor.class.getClassLoader());
76              PmdExecutor executor = new PmdExecutor(request);
77              return executor.run();
78          } finally {
79              Thread.currentThread().setContextClassLoader(origLoader);
80          }
81      }
82  
83      private static PmdResult fork(PmdRequest request) throws MavenReportException {
84          File basePmdDir = new File(request.getTargetDirectory(), "pmd");
85          basePmdDir.mkdirs();
86          File pmdRequestFile = new File(basePmdDir, "pmdrequest.bin");
87          try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(pmdRequestFile))) {
88              out.writeObject(request);
89          } catch (IOException e) {
90              throw new MavenReportException(e.getMessage(), e);
91          }
92  
93          String classpath = buildClasspath();
94          ProcessBuilder pb = new ProcessBuilder();
95          // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
96          pb.environment().put("CLASSPATH", classpath);
97          pb.command().add(request.getJavaExecutable());
98          pb.command().add(PmdExecutor.class.getName());
99          pb.command().add(pmdRequestFile.getAbsolutePath());
100 
101         LOG.debug("Executing: CLASSPATH={}, command={}", classpath, pb.command());
102         try {
103             final Process p = pb.start();
104             // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
105             // and inheritIO would only inherit file handles, not the changed streams.
106             ProcessStreamHandler.start(p.getInputStream(), System.out);
107             ProcessStreamHandler.start(p.getErrorStream(), System.err);
108             int exit = p.waitFor();
109             LOG.debug("PmdExecutor exit code: {}", exit);
110             if (exit != 0) {
111                 throw new MavenReportException("PmdExecutor exited with exit code " + exit);
112             }
113             return new PmdResult(new File(request.getTargetDirectory(), "pmd.xml"), request.getOutputEncoding());
114         } catch (IOException e) {
115             throw new MavenReportException(e.getMessage(), e);
116         } catch (InterruptedException e) {
117             Thread.currentThread().interrupt();
118             throw new MavenReportException(e.getMessage(), e);
119         }
120     }
121 
122     /**
123      * Execute PMD analysis from CLI.
124      *
125      * <p>
126      * Single arg with the filename to the serialized {@link PmdRequest}.
127      *
128      * <p>
129      * Exit-code: 0 = success, 1 = failure in executing
130      *
131      * @param args
132      */
133     public static void main(String[] args) {
134         File requestFile = new File(args[0]);
135         try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(requestFile))) {
136             PmdRequest request = (PmdRequest) in.readObject();
137             PmdExecutor pmdExecutor = new PmdExecutor(request);
138             pmdExecutor.setupLogLevel(request.getLogLevel());
139             pmdExecutor.run();
140             System.exit(0);
141         } catch (IOException | ClassNotFoundException | MavenReportException e) {
142             LOG.error(e.getMessage(), e);
143         }
144         System.exit(1);
145     }
146 
147     private final PmdRequest request;
148 
149     public PmdExecutor(PmdRequest request) {
150         this.request = Objects.requireNonNull(request);
151     }
152 
153     private PmdResult run() throws MavenReportException {
154         setupPmdLogging(request.isShowPmdLog(), request.getLogLevel());
155 
156         PMDConfiguration configuration = new PMDConfiguration();
157         LanguageVersion languageVersion = null;
158         Language language = LanguageRegistry.findLanguageByTerseName(
159                 request.getLanguage() != null ? request.getLanguage() : "java");
160         if (language == null) {
161             throw new MavenReportException("Unsupported language: " + request.getLanguage());
162         }
163         if (request.getLanguageVersion() != null) {
164             languageVersion = language.getVersion(request.getLanguageVersion());
165             if (languageVersion == null) {
166                 throw new MavenReportException("Unsupported targetJdk value '" + request.getLanguageVersion() + "'.");
167             }
168         } else {
169             languageVersion = language.getDefaultVersion();
170         }
171         LOG.debug("Using language " + languageVersion);
172         configuration.setDefaultLanguageVersion(languageVersion);
173 
174         if (request.getSourceEncoding() != null) {
175             configuration.setSourceEncoding(request.getSourceEncoding());
176         }
177 
178         configuration.prependAuxClasspath(request.getAuxClasspath());
179 
180         if (request.getSuppressMarker() != null) {
181             configuration.setSuppressMarker(request.getSuppressMarker());
182         }
183         if (request.getAnalysisCacheLocation() != null) {
184             configuration.setAnalysisCacheLocation(request.getAnalysisCacheLocation());
185             LOG.debug("Using analysis cache location: " + request.getAnalysisCacheLocation());
186         } else {
187             configuration.setIgnoreIncrementalAnalysis(true);
188         }
189 
190         configuration.setRuleSets(request.getRulesets());
191         configuration.setMinimumPriority(RulePriority.valueOf(request.getMinimumPriority()));
192         if (request.getBenchmarkOutputLocation() != null) {
193             configuration.setBenchmark(true);
194         }
195         List<File> files = request.getFiles();
196 
197         Report report = null;
198 
199         if (request.getRulesets().isEmpty()) {
200             LOG.debug("Skipping PMD execution as no rulesets are defined.");
201         } else {
202             if (request.getBenchmarkOutputLocation() != null) {
203                 TimeTracker.startGlobalTracking();
204             }
205 
206             try {
207                 report = processFilesWithPMD(configuration, files);
208             } finally {
209                 if (request.getAuxClasspath() != null) {
210                     ClassLoader classLoader = configuration.getClassLoader();
211                     if (classLoader instanceof Closeable) {
212                         Closeable closeable = (Closeable) classLoader;
213                         try {
214                             closeable.close();
215                         } catch (IOException ex) {
216                             // ignore
217                         }
218                     }
219                 }
220                 if (request.getBenchmarkOutputLocation() != null) {
221                     TimingReport timingReport = TimeTracker.stopGlobalTracking();
222                     writeBenchmarkReport(
223                             timingReport, request.getBenchmarkOutputLocation(), request.getOutputEncoding());
224                 }
225             }
226         }
227 
228         if (report != null && !report.getProcessingErrors().isEmpty()) {
229             List<Report.ProcessingError> errors = report.getProcessingErrors();
230             if (!request.isSkipPmdError()) {
231                 LOG.error("PMD processing errors:");
232                 LOG.error(getErrorsAsString(errors, request.isDebugEnabled()));
233                 throw new MavenReportException("Found " + errors.size() + " PMD processing errors");
234             }
235             LOG.warn("There are {} PMD processing errors:", errors.size());
236             LOG.warn(getErrorsAsString(errors, request.isDebugEnabled()));
237         }
238 
239         report = removeExcludedViolations(report);
240         // always write XML report, as this might be needed by the check mojo
241         // we need to output it even if the file list is empty or we have no violations
242         // so the "check" goals can check for violations
243         writeXmlReport(report);
244 
245         // write any other format except for xml and html. xml has just been produced.
246         // html format is produced by the maven site formatter. Excluding html here
247         // avoids using PMD's own html formatter, which doesn't fit into the maven site
248         // considering the html/css styling
249         String format = request.getFormat();
250         if (!"html".equals(format) && !"xml".equals(format)) {
251             writeFormattedReport(report);
252         }
253 
254         return new PmdResult(new File(request.getTargetDirectory(), "pmd.xml"), request.getOutputEncoding());
255     }
256 
257     /**
258      * Gets the errors as a single string. Each error is in its own line.
259      * @param withDetails if <code>true</code> then add the error details additionally (contains e.g. the stacktrace)
260      * @return the errors as string
261      */
262     private String getErrorsAsString(List<Report.ProcessingError> errors, boolean withDetails) {
263         List<String> errorsAsString = new ArrayList<>(errors.size());
264         for (Report.ProcessingError error : errors) {
265             errorsAsString.add(error.getFile() + ": " + error.getMsg());
266             if (withDetails) {
267                 errorsAsString.add(error.getDetail());
268             }
269         }
270         return String.join(System.lineSeparator(), errorsAsString);
271     }
272 
273     private void writeBenchmarkReport(TimingReport timingReport, String benchmarkOutputLocation, String encoding) {
274         try (Writer writer = new OutputStreamWriter(new FileOutputStream(benchmarkOutputLocation), encoding)) {
275             final TimingReportRenderer renderer = new TextTimingReportRenderer();
276             renderer.render(timingReport, writer);
277         } catch (IOException e) {
278             LOG.error("Unable to generate benchmark file: {}", benchmarkOutputLocation, e);
279         }
280     }
281 
282     private Report processFilesWithPMD(PMDConfiguration pmdConfiguration, List<File> files)
283             throws MavenReportException {
284         Report report = null;
285         RuleSetLoader rulesetLoader =
286                 RuleSetLoader.fromPmdConfig(pmdConfiguration).warnDeprecated(true);
287         try {
288             // load the ruleset once to log out any deprecated rules as warnings
289             rulesetLoader.loadFromResources(pmdConfiguration.getRuleSetPaths());
290         } catch (RuleSetLoadException e1) {
291             throw new MavenReportException("The ruleset could not be loaded", e1);
292         }
293 
294         try (PmdAnalysis pmdAnalysis = PmdAnalysis.create(pmdConfiguration)) {
295             for (File file : files) {
296                 pmdAnalysis.files().addFile(file.toPath());
297             }
298             LOG.debug("Executing PMD...");
299             report = pmdAnalysis.performAnalysisAndCollectReport();
300             LOG.debug(
301                     "PMD finished. Found {} violations.", report.getViolations().size());
302         } catch (Exception e) {
303             String message = "Failure executing PMD: " + e.getLocalizedMessage();
304             if (!request.isSkipPmdError()) {
305                 throw new MavenReportException(message, e);
306             }
307             LOG.warn(message, e);
308         }
309         return report;
310     }
311 
312     /**
313      * Use the PMD XML renderer to create the XML report format used by the
314      * check mojo later on.
315      *
316      * @param report
317      * @throws MavenReportException
318      */
319     private void writeXmlReport(Report report) throws MavenReportException {
320         File targetFile = writeReport(report, new XMLRenderer(request.getOutputEncoding()));
321         if (request.isIncludeXmlInSite()) {
322             File siteDir = new File(request.getReportOutputDirectory());
323             siteDir.mkdirs();
324             try {
325                 FileUtils.copyFile(targetFile, new File(siteDir, "pmd.xml"));
326             } catch (IOException e) {
327                 throw new MavenReportException(e.getMessage(), e);
328             }
329         }
330     }
331 
332     private File writeReport(Report report, Renderer r) throws MavenReportException {
333         if (r == null) {
334             return null;
335         }
336 
337         File targetDir = new File(request.getTargetDirectory());
338         targetDir.mkdirs();
339         String extension = r.defaultFileExtension();
340         File targetFile = new File(targetDir, "pmd." + extension);
341         LOG.debug("Target PMD output file: {}", targetFile);
342         try (Writer writer = new OutputStreamWriter(new FileOutputStream(targetFile), request.getOutputEncoding())) {
343             r.setWriter(writer);
344             r.start();
345             if (report != null) {
346                 r.renderFileReport(report);
347             }
348             r.end();
349             r.flush();
350         } catch (IOException ioe) {
351             throw new MavenReportException(ioe.getMessage(), ioe);
352         }
353 
354         return targetFile;
355     }
356 
357     /**
358      * Use the PMD renderers to render in any format aside from HTML and XML.
359      *
360      * @param report
361      * @throws MavenReportException
362      */
363     private void writeFormattedReport(Report report) throws MavenReportException {
364         Renderer renderer = createRenderer(request.getFormat(), request.getOutputEncoding());
365         writeReport(report, renderer);
366     }
367 
368     /**
369      * Create and return the correct renderer for the output type.
370      *
371      * @return the renderer based on the configured output
372      * @throws org.apache.maven.reporting.MavenReportException
373      *             if no renderer found for the output type
374      */
375     public static Renderer createRenderer(String format, String outputEncoding) throws MavenReportException {
376         LOG.debug("Renderer requested: {}", format);
377         Renderer result = null;
378         if ("xml".equals(format)) {
379             result = new XMLRenderer(outputEncoding);
380         } else if ("txt".equals(format)) {
381             result = new TextRenderer();
382         } else if ("csv".equals(format)) {
383             result = new CSVRenderer();
384         } else if ("html".equals(format)) {
385             result = new HTMLRenderer();
386         } else if (!"".equals(format) && !"none".equals(format)) {
387             try {
388                 result = (Renderer) Class.forName(format).getConstructor().newInstance();
389             } catch (Exception e) {
390                 throw new MavenReportException(
391                         "Can't find PMD custom format " + format + ": "
392                                 + e.getClass().getName(),
393                         e);
394             }
395         }
396 
397         return result;
398     }
399 
400     private Report removeExcludedViolations(Report report) throws MavenReportException {
401         if (report == null) {
402             return null;
403         }
404 
405         ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
406 
407         try {
408             excludeFromFile.loadExcludeFromFailuresData(request.getExcludeFromFailureFile());
409         } catch (MojoExecutionException e) {
410             throw new MavenReportException("Unable to load exclusions", e);
411         }
412 
413         LOG.debug("Removing excluded violations. Using {} configured exclusions.", excludeFromFile.countExclusions());
414         int violationsBefore = report.getViolations().size();
415 
416         Report filtered = report.filterViolations(new Predicate<RuleViolation>() {
417             @Override
418             public boolean test(RuleViolation ruleViolation) {
419                 return !excludeFromFile.isExcludedFromFailure(ruleViolation);
420             }
421         });
422 
423         int numberOfExcludedViolations =
424                 violationsBefore - filtered.getViolations().size();
425         LOG.debug("Excluded {} violations.", numberOfExcludedViolations);
426         return filtered;
427     }
428 }