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