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;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.UncheckedIOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.Comparator;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  
33  import net.sourceforge.pmd.RulePriority;
34  import org.apache.maven.doxia.sink.Sink;
35  import org.apache.maven.plugin.logging.Log;
36  import org.apache.maven.plugins.pmd.model.ProcessingError;
37  import org.apache.maven.plugins.pmd.model.SuppressedViolation;
38  import org.apache.maven.plugins.pmd.model.Violation;
39  import org.apache.maven.reporting.AbstractMavenReportRenderer;
40  import org.codehaus.plexus.i18n.I18N;
41  import org.codehaus.plexus.util.StringUtils;
42  
43  /**
44   * Render the PMD violations into Doxia events.
45   *
46   * @author Brett Porter
47   * @version $Id$
48   */
49  public class PmdReportRenderer extends AbstractMavenReportRenderer {
50      private final Log log;
51  
52      private final I18N i18n;
53  
54      private final Locale locale;
55  
56      private final Map<File, PmdFileInfo> files;
57  
58      // TODO Should not share state
59      private String currentFilename;
60  
61      private final Collection<Violation> violations;
62  
63      private boolean renderRuleViolationPriority;
64  
65      private final boolean renderViolationsByPriority;
66  
67      private final boolean aggregate;
68  
69      private Collection<SuppressedViolation> suppressedViolations = new ArrayList<>();
70  
71      private Collection<ProcessingError> processingErrors = new ArrayList<>();
72  
73      public PmdReportRenderer(
74              Log log,
75              Sink sink,
76              I18N i18n,
77              Locale locale,
78              Map<File, PmdFileInfo> files,
79              Collection<Violation> violations,
80              boolean renderRuleViolationPriority,
81              boolean renderViolationsByPriority,
82              boolean aggregate) {
83          super(sink);
84          this.log = log;
85          this.i18n = i18n;
86          this.locale = locale;
87          this.files = files;
88          this.violations = violations;
89          this.renderRuleViolationPriority = renderRuleViolationPriority;
90          this.renderViolationsByPriority = renderViolationsByPriority;
91          this.aggregate = aggregate;
92      }
93  
94      public void setSuppressedViolations(Collection<SuppressedViolation> suppressedViolations) {
95          this.suppressedViolations = suppressedViolations;
96      }
97  
98      public void setProcessingErrors(Collection<ProcessingError> processingErrors) {
99          this.processingErrors = processingErrors;
100     }
101 
102     @Override
103     public String getTitle() {
104         return getI18nString("title");
105     }
106 
107     /**
108      * @param key The key.
109      * @return The translated string.
110      */
111     private String getI18nString(String key) {
112         return i18n.getString("pmd-report", locale, "report.pmd." + key);
113     }
114 
115     public void renderBody() {
116         startSection(getTitle());
117 
118         sink.paragraph();
119         sink.text(getI18nString("pmdlink") + " ");
120         link("https://pmd.github.io", "PMD");
121         sink.text(" " + AbstractPmdReport.getPmdVersion() + ".");
122         sink.paragraph_();
123 
124         if (!violations.isEmpty()) {
125             renderViolationsByPriority();
126 
127             renderViolations();
128         } else {
129             paragraph(getI18nString("noProblems"));
130         }
131 
132         renderSuppressedViolations();
133 
134         renderProcessingErrors();
135 
136         endSection();
137     }
138 
139     private void startFileSection(String currentFilename, PmdFileInfo fileInfo) {
140         // prepare the filename
141         this.currentFilename = shortenFilename(currentFilename, fileInfo);
142 
143         startSection(makeFileSectionName(this.currentFilename, fileInfo));
144 
145         startTable();
146         sink.tableRow();
147         tableHeaderCell(getI18nString("column.rule"));
148         tableHeaderCell(getI18nString("column.violation"));
149         if (this.renderRuleViolationPriority) {
150             tableHeaderCell(getI18nString("column.priority"));
151         }
152         tableHeaderCell(getI18nString("column.line"));
153         sink.tableRow_();
154     }
155 
156     private void endFileSection() {
157         endTable();
158         endSection();
159     }
160 
161     private void addRuleName(Violation ruleViolation) {
162         boolean hasUrl = StringUtils.isNotBlank(ruleViolation.getExternalInfoUrl());
163 
164         if (hasUrl) {
165             sink.link(ruleViolation.getExternalInfoUrl());
166         }
167 
168         sink.text(ruleViolation.getRule());
169 
170         if (hasUrl) {
171             sink.link_();
172         }
173     }
174 
175     private void renderSingleRuleViolation(Violation ruleViolation, PmdFileInfo fileInfo) {
176         sink.tableRow();
177         sink.tableCell();
178         addRuleName(ruleViolation);
179         sink.tableCell_();
180         tableCell(ruleViolation.getText());
181 
182         if (this.renderRuleViolationPriority) {
183             tableCell(String.valueOf(
184                     RulePriority.valueOf(ruleViolation.getPriority()).getPriority()));
185         }
186 
187         sink.tableCell();
188 
189         int beginLine = ruleViolation.getBeginline();
190         outputLineLink(beginLine, fileInfo);
191         int endLine = ruleViolation.getEndline();
192         if (endLine != beginLine) {
193             sink.text("&#x2013;"); // \u2013 is a medium long dash character
194             outputLineLink(endLine, fileInfo);
195         }
196 
197         sink.tableCell_();
198         sink.tableRow_();
199     }
200 
201     // PMD might run the analysis multi-threaded, so the violations might be reported
202     // out of order. We sort them here by filename and line number before writing them to
203     // the report.
204     private void renderViolations() {
205         startSection(getI18nString("files"));
206 
207         // TODO files summary
208         renderViolationsTable(violations);
209 
210         endSection();
211     }
212 
213     private void renderViolationsByPriority() {
214         if (!renderViolationsByPriority) {
215             return;
216         }
217 
218         boolean oldPriorityColumn = this.renderRuleViolationPriority;
219         this.renderRuleViolationPriority = false;
220 
221         startSection(getI18nString("violationsByPriority"));
222 
223         Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
224         for (Violation violation : violations) {
225             RulePriority priority = RulePriority.valueOf(violation.getPriority());
226             List<Violation> violationSegment = violationsByPriority.get(priority);
227             if (violationSegment == null) {
228                 violationSegment = new ArrayList<>();
229                 violationsByPriority.put(priority, violationSegment);
230             }
231             violationSegment.add(violation);
232         }
233 
234         for (RulePriority priority : RulePriority.values()) {
235             List<Violation> violationsWithPriority = violationsByPriority.get(priority);
236             if (violationsWithPriority == null || violationsWithPriority.isEmpty()) {
237                 continue;
238             }
239 
240             startSection(getI18nString("priority") + " " + priority.getPriority());
241 
242             renderViolationsTable(violationsWithPriority);
243 
244             endSection();
245         }
246 
247         if (violations.isEmpty()) {
248             paragraph(getI18nString("noProblems"));
249         }
250 
251         endSection();
252 
253         this.renderRuleViolationPriority = oldPriorityColumn;
254     }
255 
256     private void renderViolationsTable(Collection<Violation> violationSegment) {
257         List<Violation> violationSegmentCopy = new ArrayList<>(violationSegment);
258         Collections.sort(violationSegmentCopy, new Comparator<Violation>() {
259             /** {@inheritDoc} */
260             public int compare(Violation o1, Violation o2) {
261                 int filenames = o1.getFileName().compareTo(o2.getFileName());
262                 if (filenames == 0) {
263                     return o1.getBeginline() - o2.getBeginline();
264                 } else {
265                     return filenames;
266                 }
267             }
268         });
269 
270         boolean fileSectionStarted = false;
271         String previousFilename = null;
272         for (Violation ruleViolation : violationSegmentCopy) {
273             String currentFn = ruleViolation.getFileName();
274             PmdFileInfo fileInfo = determineFileInfo(currentFn);
275 
276             if (!currentFn.equalsIgnoreCase(previousFilename) && fileSectionStarted) {
277                 endFileSection();
278                 fileSectionStarted = false;
279             }
280             if (!fileSectionStarted) {
281                 startFileSection(currentFn, fileInfo);
282                 fileSectionStarted = true;
283             }
284 
285             renderSingleRuleViolation(ruleViolation, fileInfo);
286 
287             previousFilename = currentFn;
288         }
289 
290         if (fileSectionStarted) {
291             endFileSection();
292         }
293     }
294 
295     private void outputLineLink(int line, PmdFileInfo fileInfo) {
296         String xrefLocation = null;
297         if (fileInfo != null) {
298             xrefLocation = fileInfo.getXrefLocation();
299         }
300 
301         if (xrefLocation != null) {
302             sink.link(xrefLocation + "/" + currentFilename.replaceAll("\\.java$", ".html") + "#L" + line);
303         }
304         sink.text(String.valueOf(line));
305         if (xrefLocation != null) {
306             sink.link_();
307         }
308     }
309 
310     // PMD might run the analysis multi-threaded, so the suppressed violations might be reported
311     // out of order. We sort them here by filename before writing them to
312     // the report.
313     private void renderSuppressedViolations() {
314         if (suppressedViolations.isEmpty()) {
315             return;
316         }
317 
318         startSection(getI18nString("suppressedViolations.title"));
319 
320         List<SuppressedViolation> suppressedViolationsCopy = new ArrayList<>(suppressedViolations);
321         Collections.sort(suppressedViolationsCopy, new Comparator<SuppressedViolation>() {
322             @Override
323             public int compare(SuppressedViolation o1, SuppressedViolation o2) {
324                 return o1.getFilename().compareTo(o2.getFilename());
325             }
326         });
327 
328         startTable();
329         tableHeader(new String[] {
330             getI18nString("suppressedViolations.column.filename"),
331             getI18nString("suppressedViolations.column.ruleMessage"),
332             getI18nString("suppressedViolations.column.suppressionType"),
333             getI18nString("suppressedViolations.column.userMessage")
334         });
335 
336         for (SuppressedViolation suppressedViolation : suppressedViolationsCopy) {
337             String filename = suppressedViolation.getFilename();
338             PmdFileInfo fileInfo = determineFileInfo(filename);
339             filename = shortenFilename(filename, fileInfo);
340 
341             tableRow(new String[] {
342                 filename,
343                 suppressedViolation.getRuleMessage(),
344                 suppressedViolation.getSuppressionType(),
345                 suppressedViolation.getUserMessage()
346             });
347         }
348 
349         endTable();
350         endSection();
351     }
352 
353     private void renderProcessingErrors() {
354         if (processingErrors.isEmpty()) {
355             return;
356         }
357 
358         // sort the problem by filename first, since PMD is executed multi-threaded
359         // and might reports the results unsorted
360         List<ProcessingError> processingErrorsCopy = new ArrayList<>(processingErrors);
361         Collections.sort(processingErrorsCopy, new Comparator<ProcessingError>() {
362             @Override
363             public int compare(ProcessingError e1, ProcessingError e2) {
364                 return e1.getFilename().compareTo(e2.getFilename());
365             }
366         });
367 
368         startSection(getI18nString("processingErrors.title"));
369 
370         startTable();
371         tableHeader(new String[] {
372             getI18nString("processingErrors.column.filename"), getI18nString("processingErrors.column.problem")
373         });
374 
375         for (ProcessingError error : processingErrorsCopy) {
376             renderSingleProcessingError(error);
377         }
378 
379         endTable();
380         endSection();
381     }
382 
383     private void renderSingleProcessingError(ProcessingError error) {
384         String filename = error.getFilename();
385         PmdFileInfo fileInfo = determineFileInfo(filename);
386         filename = makeFileSectionName(shortenFilename(filename, fileInfo), fileInfo);
387 
388         sink.tableRow();
389         tableCell(filename);
390         sink.tableCell();
391         sink.text(error.getMsg());
392         sink.verbatim(null);
393         sink.rawText(error.getDetail());
394         sink.verbatim_();
395         sink.tableCell_();
396         sink.tableRow_();
397     }
398 
399     private String shortenFilename(String filename, PmdFileInfo fileInfo) {
400         String result = filename;
401         if (fileInfo != null && fileInfo.getSourceDirectory() != null) {
402             result = StringUtils.substring(
403                     result, fileInfo.getSourceDirectory().getAbsolutePath().length() + 1);
404         }
405         return StringUtils.replace(result, "\\", "/");
406     }
407 
408     private String makeFileSectionName(String filename, PmdFileInfo fileInfo) {
409         if (aggregate && fileInfo != null && fileInfo.getProject() != null) {
410             return fileInfo.getProject().getName() + " - " + filename;
411         }
412         return filename;
413     }
414 
415     private PmdFileInfo determineFileInfo(String filename) {
416         try {
417             File canonicalFilename = new File(filename).getCanonicalFile();
418             PmdFileInfo fileInfo = files.get(canonicalFilename);
419             if (fileInfo == null) {
420                 log.warn("Couldn't determine PmdFileInfo for file " + filename + " (canonical: " + canonicalFilename
421                         + "). XRef links won't be available.");
422             }
423             return fileInfo;
424         } catch (IOException e) {
425             throw new UncheckedIOException(e);
426         }
427     }
428 }