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.lang.rule.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     protected 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         // May contain content not legit for #tableCell()
181         sink.tableCell();
182         sink.text(ruleViolation.getText());
183         sink.tableCell_();
184 
185         if (this.renderRuleViolationPriority) {
186             tableCell(String.valueOf(
187                     RulePriority.valueOf(ruleViolation.getPriority()).getPriority()));
188         }
189 
190         sink.tableCell();
191 
192         int beginLine = ruleViolation.getBeginline();
193         outputLineLink(beginLine, fileInfo);
194         int endLine = ruleViolation.getEndline();
195         if (endLine != beginLine) {
196             sink.text("&#x2013;"); // \u2013 is a medium long dash character
197             outputLineLink(endLine, fileInfo);
198         }
199 
200         sink.tableCell_();
201         sink.tableRow_();
202     }
203 
204     // PMD might run the analysis multi-threaded, so the violations might be reported
205     // out of order. We sort them here by filename and line number before writing them to
206     // the report.
207     private void renderViolations() {
208         startSection(getI18nString("files"));
209 
210         // TODO files summary
211         renderViolationsTable(violations);
212 
213         endSection();
214     }
215 
216     private void renderViolationsByPriority() {
217         if (!renderViolationsByPriority) {
218             return;
219         }
220 
221         boolean oldPriorityColumn = this.renderRuleViolationPriority;
222         this.renderRuleViolationPriority = false;
223 
224         startSection(getI18nString("violationsByPriority"));
225 
226         Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
227         for (Violation violation : violations) {
228             RulePriority priority = RulePriority.valueOf(violation.getPriority());
229             List<Violation> violationSegment = violationsByPriority.get(priority);
230             if (violationSegment == null) {
231                 violationSegment = new ArrayList<>();
232                 violationsByPriority.put(priority, violationSegment);
233             }
234             violationSegment.add(violation);
235         }
236 
237         for (RulePriority priority : RulePriority.values()) {
238             List<Violation> violationsWithPriority = violationsByPriority.get(priority);
239             if (violationsWithPriority == null || violationsWithPriority.isEmpty()) {
240                 continue;
241             }
242 
243             startSection(getI18nString("priority") + " " + priority.getPriority());
244 
245             renderViolationsTable(violationsWithPriority);
246 
247             endSection();
248         }
249 
250         if (violations.isEmpty()) {
251             paragraph(getI18nString("noProblems"));
252         }
253 
254         endSection();
255 
256         this.renderRuleViolationPriority = oldPriorityColumn;
257     }
258 
259     private void renderViolationsTable(Collection<Violation> violationSegment) {
260         List<Violation> violationSegmentCopy = new ArrayList<>(violationSegment);
261         Collections.sort(violationSegmentCopy, new Comparator<Violation>() {
262             /** {@inheritDoc} */
263             public int compare(Violation o1, Violation o2) {
264                 int filenames = o1.getFileName().compareTo(o2.getFileName());
265                 if (filenames == 0) {
266                     return o1.getBeginline() - o2.getBeginline();
267                 } else {
268                     return filenames;
269                 }
270             }
271         });
272 
273         boolean fileSectionStarted = false;
274         String previousFilename = null;
275         for (Violation ruleViolation : violationSegmentCopy) {
276             String currentFn = ruleViolation.getFileName();
277             PmdFileInfo fileInfo = determineFileInfo(currentFn);
278 
279             if (!currentFn.equalsIgnoreCase(previousFilename) && fileSectionStarted) {
280                 endFileSection();
281                 fileSectionStarted = false;
282             }
283             if (!fileSectionStarted) {
284                 startFileSection(currentFn, fileInfo);
285                 fileSectionStarted = true;
286             }
287 
288             renderSingleRuleViolation(ruleViolation, fileInfo);
289 
290             previousFilename = currentFn;
291         }
292 
293         if (fileSectionStarted) {
294             endFileSection();
295         }
296     }
297 
298     private void outputLineLink(int line, PmdFileInfo fileInfo) {
299         String xrefLocation = null;
300         if (fileInfo != null) {
301             xrefLocation = fileInfo.getXrefLocation();
302         }
303 
304         if (xrefLocation != null) {
305             sink.link(xrefLocation + "/" + currentFilename.replaceAll("\\.java$", ".html") + "#L" + line);
306         }
307         sink.text(String.valueOf(line));
308         if (xrefLocation != null) {
309             sink.link_();
310         }
311     }
312 
313     // PMD might run the analysis multi-threaded, so the suppressed violations might be reported
314     // out of order. We sort them here by filename before writing them to
315     // the report.
316     private void renderSuppressedViolations() {
317         if (suppressedViolations.isEmpty()) {
318             return;
319         }
320 
321         startSection(getI18nString("suppressedViolations.title"));
322 
323         List<SuppressedViolation> suppressedViolationsCopy = new ArrayList<>(suppressedViolations);
324         Collections.sort(suppressedViolationsCopy, new Comparator<SuppressedViolation>() {
325             @Override
326             public int compare(SuppressedViolation o1, SuppressedViolation o2) {
327                 return o1.getFilename().compareTo(o2.getFilename());
328             }
329         });
330 
331         startTable();
332         tableHeader(new String[] {
333             getI18nString("suppressedViolations.column.filename"),
334             getI18nString("suppressedViolations.column.ruleMessage"),
335             getI18nString("suppressedViolations.column.suppressionType"),
336             getI18nString("suppressedViolations.column.userMessage")
337         });
338 
339         for (SuppressedViolation suppressedViolation : suppressedViolationsCopy) {
340             String filename = suppressedViolation.getFilename();
341             PmdFileInfo fileInfo = determineFileInfo(filename);
342             filename = shortenFilename(filename, fileInfo);
343 
344             // May contain content not legit for #tableCell()
345             sink.tableRow();
346             tableCell(filename);
347             sink.tableCell();
348             sink.text(suppressedViolation.getRuleMessage());
349             sink.tableCell_();
350             tableCell(suppressedViolation.getSuppressionType());
351             sink.tableCell();
352             sink.text(suppressedViolation.getUserMessage());
353             sink.tableCell_();
354             sink.tableRow_();
355         }
356 
357         endTable();
358         endSection();
359     }
360 
361     private void renderProcessingErrors() {
362         if (processingErrors.isEmpty()) {
363             return;
364         }
365 
366         // sort the problem by filename first, since PMD is executed multi-threaded
367         // and might reports the results unsorted
368         List<ProcessingError> processingErrorsCopy = new ArrayList<>(processingErrors);
369         Collections.sort(processingErrorsCopy, new Comparator<ProcessingError>() {
370             @Override
371             public int compare(ProcessingError e1, ProcessingError e2) {
372                 return e1.getFilename().compareTo(e2.getFilename());
373             }
374         });
375 
376         startSection(getI18nString("processingErrors.title"));
377 
378         startTable();
379         tableHeader(new String[] {
380             getI18nString("processingErrors.column.filename"), getI18nString("processingErrors.column.problem")
381         });
382 
383         for (ProcessingError error : processingErrorsCopy) {
384             renderSingleProcessingError(error);
385         }
386 
387         endTable();
388         endSection();
389     }
390 
391     private void renderSingleProcessingError(ProcessingError error) {
392         String filename = error.getFilename();
393         PmdFileInfo fileInfo = determineFileInfo(filename);
394         filename = makeFileSectionName(shortenFilename(filename, fileInfo), fileInfo);
395 
396         sink.tableRow();
397         tableCell(filename);
398         sink.tableCell();
399         sink.text(error.getMsg());
400         sink.verbatim(null);
401         sink.rawText(error.getDetail());
402         sink.verbatim_();
403         sink.tableCell_();
404         sink.tableRow_();
405     }
406 
407     private String shortenFilename(String filename, PmdFileInfo fileInfo) {
408         String result = filename;
409         if (fileInfo != null && fileInfo.getSourceDirectory() != null) {
410             result = StringUtils.substring(
411                     result, fileInfo.getSourceDirectory().getAbsolutePath().length() + 1);
412         }
413         return StringUtils.replace(result, "\\", "/");
414     }
415 
416     private String makeFileSectionName(String filename, PmdFileInfo fileInfo) {
417         if (aggregate && fileInfo != null && fileInfo.getProject() != null) {
418             return fileInfo.getProject().getName() + " - " + filename;
419         }
420         return filename;
421     }
422 
423     private PmdFileInfo determineFileInfo(String filename) {
424         try {
425             File canonicalFilename = new File(filename).getCanonicalFile();
426             PmdFileInfo fileInfo = files.get(canonicalFilename);
427             if (fileInfo == null) {
428                 log.warn("Couldn't determine PmdFileInfo for file " + filename + " (canonical: " + canonicalFilename
429                         + "). XRef links won't be available.");
430             }
431             return fileInfo;
432         } catch (IOException e) {
433             throw new UncheckedIOException(e);
434         }
435     }
436 }