blob: bb836c953cacc20d25ed9743929ca6aa1c0b5bea [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugins.pmd;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import net.sourceforge.pmd.RulePriority;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.pmd.model.ProcessingError;
import org.apache.maven.plugins.pmd.model.SuppressedViolation;
import org.apache.maven.plugins.pmd.model.Violation;
import org.codehaus.plexus.util.StringUtils;
/**
* Render the PMD violations into Doxia events.
*
* @author Brett Porter
* @version $Id$
*/
public class PmdReportGenerator {
private Log log;
private Sink sink;
private String currentFilename;
private ResourceBundle bundle;
private Set<Violation> violations = new HashSet<>();
private List<SuppressedViolation> suppressedViolations = new ArrayList<>();
private List<ProcessingError> processingErrors = new ArrayList<>();
private boolean aggregate;
private boolean renderRuleViolationPriority;
private boolean renderViolationsByPriority;
private Map<File, PmdFileInfo> files;
// private List<Metric> metrics = new ArrayList<Metric>();
public PmdReportGenerator(Log log, Sink sink, ResourceBundle bundle, boolean aggregate) {
this.log = log;
this.sink = sink;
this.bundle = bundle;
this.aggregate = aggregate;
}
private String getTitle() {
return bundle.getString("report.pmd.title");
}
public void setViolations(Collection<Violation> violations) {
this.violations = new HashSet<>(violations);
}
public List<Violation> getViolations() {
return new ArrayList<>(violations);
}
public void setSuppressedViolations(Collection<SuppressedViolation> suppressedViolations) {
this.suppressedViolations = new ArrayList<>(suppressedViolations);
}
public void setProcessingErrors(Collection<ProcessingError> errors) {
this.processingErrors = new ArrayList<>(errors);
}
public List<ProcessingError> getProcessingErrors() {
return processingErrors;
}
// public List<Metric> getMetrics()
// {
// return metrics;
// }
//
// public void setMetrics( List<Metric> metrics )
// {
// this.metrics = metrics;
// }
private String shortenFilename(String filename, PmdFileInfo fileInfo) {
String result = filename;
if (fileInfo != null && fileInfo.getSourceDirectory() != null) {
result = StringUtils.substring(
result, fileInfo.getSourceDirectory().getAbsolutePath().length() + 1);
}
return StringUtils.replace(result, "\\", "/");
}
private String makeFileSectionName(String filename, PmdFileInfo fileInfo) {
if (aggregate && fileInfo != null && fileInfo.getProject() != null) {
return fileInfo.getProject().getName() + " - " + filename;
}
return filename;
}
private PmdFileInfo determineFileInfo(String filename) throws IOException {
File canonicalFilename = new File(filename).getCanonicalFile();
PmdFileInfo fileInfo = files.get(canonicalFilename);
if (fileInfo == null) {
log.warn("Couldn't determine PmdFileInfo for file " + filename + " (canonical: " + canonicalFilename
+ "). XRef links won't be available.");
}
return fileInfo;
}
private void startFileSection(int level, String currentFilename, PmdFileInfo fileInfo) {
sink.section(level, null);
sink.sectionTitle(level, null);
// prepare the filename
this.currentFilename = shortenFilename(currentFilename, fileInfo);
sink.text(makeFileSectionName(this.currentFilename, fileInfo));
sink.sectionTitle_(level);
sink.table();
sink.tableRows(null, false);
sink.tableRow();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.column.rule"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.column.violation"));
sink.tableHeaderCell_();
if (this.renderRuleViolationPriority) {
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.column.priority"));
sink.tableHeaderCell_();
}
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.column.line"));
sink.tableHeaderCell_();
sink.tableRow_();
}
private void endFileSection(int level) {
sink.tableRows_();
sink.table_();
sink.section_(level);
}
private void addRuleName(Violation ruleViolation) {
boolean hasUrl = StringUtils.isNotBlank(ruleViolation.getExternalInfoUrl());
if (hasUrl) {
sink.link(ruleViolation.getExternalInfoUrl());
}
sink.text(ruleViolation.getRule());
if (hasUrl) {
sink.link_();
}
}
private void processSingleRuleViolation(Violation ruleViolation, PmdFileInfo fileInfo) {
sink.tableRow();
sink.tableCell();
addRuleName(ruleViolation);
sink.tableCell_();
sink.tableCell();
sink.text(ruleViolation.getText());
sink.tableCell_();
if (this.renderRuleViolationPriority) {
sink.tableCell();
sink.text(String.valueOf(
RulePriority.valueOf(ruleViolation.getPriority()).getPriority()));
sink.tableCell_();
}
sink.tableCell();
int beginLine = ruleViolation.getBeginline();
outputLineLink(beginLine, fileInfo);
int endLine = ruleViolation.getEndline();
if (endLine != beginLine) {
sink.text("&#x2013;"); // \u2013 is a medium long dash character
outputLineLink(endLine, fileInfo);
}
sink.tableCell_();
sink.tableRow_();
}
// PMD might run the analysis multi-threaded, so the violations might be reported
// out of order. We sort them here by filename and line number before writing them to
// the report.
private void renderViolations() throws IOException {
sink.section1();
sink.sectionTitle1();
sink.text(bundle.getString("report.pmd.files"));
sink.sectionTitle1_();
// TODO files summary
List<Violation> violations2 = new ArrayList<>(violations);
renderViolationsTable(2, violations2);
sink.section1_();
}
private void renderViolationsByPriority() throws IOException {
if (!renderViolationsByPriority) {
return;
}
boolean oldPriorityColumn = this.renderRuleViolationPriority;
this.renderRuleViolationPriority = false;
sink.section1();
sink.sectionTitle1();
sink.text(bundle.getString("report.pmd.violationsByPriority"));
sink.sectionTitle1_();
Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
for (Violation violation : violations) {
RulePriority priority = RulePriority.valueOf(violation.getPriority());
List<Violation> violationSegment = violationsByPriority.get(priority);
if (violationSegment == null) {
violationSegment = new ArrayList<>();
violationsByPriority.put(priority, violationSegment);
}
violationSegment.add(violation);
}
for (RulePriority priority : RulePriority.values()) {
List<Violation> violationsWithPriority = violationsByPriority.get(priority);
if (violationsWithPriority == null || violationsWithPriority.isEmpty()) {
continue;
}
sink.section2();
sink.sectionTitle2();
sink.text(bundle.getString("report.pmd.priority") + " " + priority.getPriority());
sink.sectionTitle2_();
renderViolationsTable(3, violationsWithPriority);
sink.section2_();
}
if (violations.isEmpty()) {
sink.paragraph();
sink.text(bundle.getString("report.pmd.noProblems"));
sink.paragraph_();
}
sink.section1_();
this.renderRuleViolationPriority = oldPriorityColumn;
}
private void renderViolationsTable(int level, List<Violation> violationSegment) throws IOException {
Collections.sort(violationSegment, new Comparator<Violation>() {
/** {@inheritDoc} */
public int compare(Violation o1, Violation o2) {
int filenames = o1.getFileName().compareTo(o2.getFileName());
if (filenames == 0) {
return o1.getBeginline() - o2.getBeginline();
} else {
return filenames;
}
}
});
boolean fileSectionStarted = false;
String previousFilename = null;
for (Violation ruleViolation : violationSegment) {
String currentFn = ruleViolation.getFileName();
PmdFileInfo fileInfo = determineFileInfo(currentFn);
if (!currentFn.equalsIgnoreCase(previousFilename) && fileSectionStarted) {
endFileSection(level);
fileSectionStarted = false;
}
if (!fileSectionStarted) {
startFileSection(level, currentFn, fileInfo);
fileSectionStarted = true;
}
processSingleRuleViolation(ruleViolation, fileInfo);
previousFilename = currentFn;
}
if (fileSectionStarted) {
endFileSection(level);
}
}
private void outputLineLink(int line, PmdFileInfo fileInfo) {
String xrefLocation = null;
if (fileInfo != null) {
xrefLocation = fileInfo.getXrefLocation();
}
if (xrefLocation != null) {
sink.link(xrefLocation + "/" + currentFilename.replaceAll("\\.java$", ".html") + "#L" + line);
}
sink.text(String.valueOf(line));
if (xrefLocation != null) {
sink.link_();
}
}
// PMD might run the analysis multi-threaded, so the suppressed violations might be reported
// out of order. We sort them here by filename before writing them to
// the report.
private void renderSuppressedViolations() throws IOException {
sink.section1();
sink.sectionTitle1();
sink.text(bundle.getString("report.pmd.suppressedViolations.title"));
sink.sectionTitle1_();
Collections.sort(suppressedViolations, new Comparator<SuppressedViolation>() {
@Override
public int compare(SuppressedViolation o1, SuppressedViolation o2) {
return o1.getFilename().compareTo(o2.getFilename());
}
});
sink.table();
sink.tableRows(null, false);
sink.tableRow();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.suppressedViolations.column.filename"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.suppressedViolations.column.ruleMessage"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.suppressedViolations.column.suppressionType"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.suppressedViolations.column.userMessage"));
sink.tableHeaderCell_();
sink.tableRow_();
for (SuppressedViolation suppressedViolation : suppressedViolations) {
String filename = suppressedViolation.getFilename();
PmdFileInfo fileInfo = determineFileInfo(filename);
filename = shortenFilename(filename, fileInfo);
sink.tableRow();
sink.tableCell();
sink.text(filename);
sink.tableCell_();
sink.tableCell();
sink.text(suppressedViolation.getRuleMessage());
sink.tableCell_();
sink.tableCell();
sink.text(suppressedViolation.getSuppressionType());
sink.tableCell_();
sink.tableCell();
sink.text(suppressedViolation.getUserMessage());
sink.tableCell_();
sink.tableRow_();
}
sink.tableRows_();
sink.table_();
sink.section1_();
}
private void processProcessingErrors() throws IOException {
// sort the problem by filename first, since PMD is executed multi-threaded
// and might reports the results unsorted
Collections.sort(processingErrors, new Comparator<ProcessingError>() {
@Override
public int compare(ProcessingError e1, ProcessingError e2) {
return e1.getFilename().compareTo(e2.getFilename());
}
});
sink.section1();
sink.sectionTitle1();
sink.text(bundle.getString("report.pmd.processingErrors.title"));
sink.sectionTitle1_();
sink.table();
sink.tableRows(null, false);
sink.tableRow();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.processingErrors.column.filename"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text(bundle.getString("report.pmd.processingErrors.column.problem"));
sink.tableHeaderCell_();
sink.tableRow_();
for (ProcessingError error : processingErrors) {
processSingleProcessingError(error);
}
sink.tableRows_();
sink.table_();
sink.section1_();
}
private void processSingleProcessingError(ProcessingError error) throws IOException {
String filename = error.getFilename();
PmdFileInfo fileInfo = determineFileInfo(filename);
filename = makeFileSectionName(shortenFilename(filename, fileInfo), fileInfo);
sink.tableRow();
sink.tableCell();
sink.text(filename);
sink.tableCell_();
sink.tableCell();
sink.text(error.getMsg());
sink.verbatim(null);
sink.rawText(error.getDetail());
sink.verbatim_();
sink.tableCell_();
sink.tableRow_();
}
public void beginDocument() {
sink.head();
sink.title();
sink.text(getTitle());
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.sectionTitle1();
sink.text(getTitle());
sink.sectionTitle1_();
sink.paragraph();
sink.text(bundle.getString("report.pmd.pmdlink") + " ");
sink.link("https://pmd.github.io");
sink.text("PMD");
sink.link_();
sink.text(" " + AbstractPmdReport.getPmdVersion() + ".");
sink.paragraph_();
sink.section1_();
// TODO overall summary
}
/*
* private void processMetrics() { if ( metrics.size() == 0 ) { return; } sink.section1(); sink.sectionTitle1();
* sink.text( "Metrics" ); sink.sectionTitle1_(); sink.table(); sink.tableRow(); sink.tableHeaderCell(); sink.text(
* "Name" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Count" ); sink.tableHeaderCell_();
* sink.tableHeaderCell(); sink.text( "High" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Low" );
* sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Average" ); sink.tableHeaderCell_();
* sink.tableRow_(); for ( Metric met : metrics ) { sink.tableRow(); sink.tableCell(); sink.text(
* met.getMetricName() ); sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getCount() ) );
* sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getHighValue() ) ); sink.tableCell_();
* sink.tableCell(); sink.text( String.valueOf( met.getLowValue() ) ); sink.tableCell_(); sink.tableCell();
* sink.text( String.valueOf( met.getAverage() ) ); sink.tableCell_(); sink.tableRow_(); } sink.table_();
* sink.section1_(); }
*/
public void render() throws IOException {
if (!violations.isEmpty()) {
renderViolationsByPriority();
renderViolations();
} else {
sink.paragraph();
sink.text(bundle.getString("report.pmd.noProblems"));
sink.paragraph_();
}
if (!suppressedViolations.isEmpty()) {
renderSuppressedViolations();
}
if (!processingErrors.isEmpty()) {
processProcessingErrors();
}
}
public void endDocument() throws IOException {
// The Metrics report useless with the current PMD metrics impl.
// For instance, run the coupling ruleset and you will get a boatload
// of excessive imports metrics, none of which is really any use.
// TODO Determine if we are going to just ignore metrics.
// processMetrics();
sink.body_();
sink.flush();
sink.close();
}
public void setFiles(Map<File, PmdFileInfo> files) {
this.files = files;
}
public void setRenderRuleViolationPriority(boolean renderRuleViolationPriority) {
this.renderRuleViolationPriority = renderRuleViolationPriority;
}
public void setRenderViolationsByPriority(boolean renderViolationsByPriority) {
this.renderViolationsByPriority = renderViolationsByPriority;
}
}