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         writeXmlReport(report);
241 
242         // write any other format except for xml and html. xml has just been produced.
243         // html format is produced by the maven site formatter. Excluding html here
244         // avoids using PMD's own html formatter, which doesn't fit into the maven site
245         // considering the html/css styling
246         String format = request.getFormat();
247         if (!"html".equals(format) && !"xml".equals(format)) {
248             writeFormattedReport(report);
249         }
250 
251         return new PmdResult(new File(request.getTargetDirectory(), "pmd.xml"), request.getOutputEncoding());
252     }
253 
254     /**
255      * Gets the errors as a single string. Each error is in its own line.
256      * @param withDetails if <code>true</code> then add the error details additionally (contains e.g. the stacktrace)
257      * @return the errors as string
258      */
259     private String getErrorsAsString(List<Report.ProcessingError> errors, boolean withDetails) {
260         List<String> errorsAsString = new ArrayList<>(errors.size());
261         for (Report.ProcessingError error : errors) {
262             errorsAsString.add(error.getFileId().getAbsolutePath() + ": " + error.getMsg());
263             if (withDetails) {
264                 errorsAsString.add(error.getDetail());
265             }
266         }
267         return String.join(System.lineSeparator(), errorsAsString);
268     }
269 
270     private void writeBenchmarkReport(TimingReport timingReport, String benchmarkOutputLocation, String encoding) {
271         try (Writer writer = new OutputStreamWriter(new FileOutputStream(benchmarkOutputLocation), encoding)) {
272             final TimingReportRenderer renderer = new TextTimingReportRenderer();
273             renderer.render(timingReport, writer);
274         } catch (IOException e) {
275             LOG.error("Unable to generate benchmark file: {}", benchmarkOutputLocation, e);
276         }
277     }
278 
279     private Report processFilesWithPMD(PMDConfiguration pmdConfiguration, List<File> files)
280             throws MavenReportException {
281         Report report = null;
282         RuleSetLoader rulesetLoader =
283                 RuleSetLoader.fromPmdConfig(pmdConfiguration).warnDeprecated(true);
284         try {
285             // load the ruleset once to log out any deprecated rules as warnings
286             rulesetLoader.loadFromResources(pmdConfiguration.getRuleSetPaths());
287         } catch (RuleSetLoadException e1) {
288             throw new MavenReportException("The ruleset could not be loaded", e1);
289         }
290 
291         try (PmdAnalysis pmdAnalysis = PmdAnalysis.create(pmdConfiguration)) {
292             for (File file : files) {
293                 pmdAnalysis.files().addFile(file.toPath());
294             }
295             LOG.debug("Executing PMD...");
296             report = pmdAnalysis.performAnalysisAndCollectReport();
297             LOG.debug(
298                     "PMD finished. Found {} violations.", report.getViolations().size());
299         } catch (Exception e) {
300             String message = "Failure executing PMD: " + e.getLocalizedMessage();
301             if (!request.isSkipPmdError()) {
302                 throw new MavenReportException(message, e);
303             }
304             LOG.warn(message, e);
305         }
306         return report;
307     }
308 
309     /**
310      * Use the PMD XML renderer to create the XML report format used by the
311      * check mojo later on.
312      *
313      * @param report
314      * @throws MavenReportException
315      */
316     private void writeXmlReport(Report report) throws MavenReportException {
317         File targetFile = writeReport(report, new XMLRenderer(request.getOutputEncoding()));
318         if (request.isIncludeXmlInSite()) {
319             File siteDir = new File(request.getReportOutputDirectory());
320             siteDir.mkdirs();
321             try {
322                 FileUtils.copyFile(targetFile, new File(siteDir, "pmd.xml"));
323             } catch (IOException e) {
324                 throw new MavenReportException(e.getMessage(), e);
325             }
326         }
327     }
328 
329     private File writeReport(Report report, Renderer r) throws MavenReportException {
330         if (r == null) {
331             return null;
332         }
333 
334         File targetDir = new File(request.getTargetDirectory());
335         targetDir.mkdirs();
336         String extension = r.defaultFileExtension();
337         File targetFile = new File(targetDir, "pmd." + extension);
338         LOG.debug("Target PMD output file: {}", targetFile);
339         try (Writer writer = new OutputStreamWriter(new FileOutputStream(targetFile), request.getOutputEncoding())) {
340             r.setWriter(writer);
341             r.start();
342             if (report != null) {
343                 r.renderFileReport(report);
344             }
345             r.end();
346             r.flush();
347         } catch (IOException ioe) {
348             throw new MavenReportException(ioe.getMessage(), ioe);
349         }
350 
351         return targetFile;
352     }
353 
354     /**
355      * Use the PMD renderers to render in any format aside from HTML and XML.
356      *
357      * @param report
358      * @throws MavenReportException
359      */
360     private void writeFormattedReport(Report report) throws MavenReportException {
361         Renderer renderer = createRenderer(request.getFormat(), request.getOutputEncoding());
362         writeReport(report, renderer);
363     }
364 
365     /**
366      * Create and return the correct renderer for the output type.
367      *
368      * @return the renderer based on the configured output
369      * @throws org.apache.maven.reporting.MavenReportException
370      *             if no renderer found for the output type
371      */
372     public static Renderer createRenderer(String format, String outputEncoding) throws MavenReportException {
373         LOG.debug("Renderer requested: {}", format);
374         Renderer result = null;
375         if ("xml".equals(format)) {
376             result = new XMLRenderer(outputEncoding);
377         } else if ("txt".equals(format)) {
378             result = new TextRenderer();
379         } else if ("csv".equals(format)) {
380             result = new CSVRenderer();
381         } else if ("html".equals(format)) {
382             result = new HTMLRenderer();
383         } else if (!"".equals(format) && !"none".equals(format)) {
384             try {
385                 result = (Renderer) Class.forName(format).getConstructor().newInstance();
386             } catch (Exception e) {
387                 throw new MavenReportException(
388                         "Can't find PMD custom format " + format + ": "
389                                 + e.getClass().getName(),
390                         e);
391             }
392         }
393 
394         return result;
395     }
396 
397     private Report removeExcludedViolations(Report report) throws MavenReportException {
398         if (report == null) {
399             return null;
400         }
401 
402         ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
403 
404         try {
405             excludeFromFile.loadExcludeFromFailuresData(request.getExcludeFromFailureFile());
406         } catch (MojoExecutionException e) {
407             throw new MavenReportException("Unable to load exclusions", e);
408         }
409 
410         LOG.debug("Removing excluded violations. Using {} configured exclusions.", excludeFromFile.countExclusions());
411         int violationsBefore = report.getViolations().size();
412 
413         Report filtered =
414                 report.filterViolations(ruleViolation -> !excludeFromFile.isExcludedFromFailure(ruleViolation));
415 
416         int numberOfExcludedViolations =
417                 violationsBefore - filtered.getViolations().size();
418         LOG.debug("Excluded {} violations.", numberOfExcludedViolations);
419         return filtered;
420     }
421 }