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