| /* |
| * 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.io.UncheckedIOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| 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.apache.maven.reporting.AbstractMavenReportRenderer; |
| import org.codehaus.plexus.i18n.I18N; |
| import org.codehaus.plexus.util.StringUtils; |
| |
| /** |
| * Render the PMD violations into Doxia events. |
| * |
| * @author Brett Porter |
| * @version $Id$ |
| */ |
| public class PmdReportRenderer extends AbstractMavenReportRenderer { |
| private final Log log; |
| |
| private final I18N i18n; |
| |
| private final Locale locale; |
| |
| private final Map<File, PmdFileInfo> files; |
| |
| // TODO Should not share state |
| private String currentFilename; |
| |
| private final Collection<Violation> violations; |
| |
| private boolean renderRuleViolationPriority; |
| |
| private final boolean renderViolationsByPriority; |
| |
| private final boolean aggregate; |
| |
| private Collection<SuppressedViolation> suppressedViolations = new ArrayList<>(); |
| |
| private Collection<ProcessingError> processingErrors = new ArrayList<>(); |
| |
| public PmdReportRenderer( |
| Log log, |
| Sink sink, |
| I18N i18n, |
| Locale locale, |
| Map<File, PmdFileInfo> files, |
| Collection<Violation> violations, |
| boolean renderRuleViolationPriority, |
| boolean renderViolationsByPriority, |
| boolean aggregate) { |
| super(sink); |
| this.log = log; |
| this.i18n = i18n; |
| this.locale = locale; |
| this.files = files; |
| this.violations = violations; |
| this.renderRuleViolationPriority = renderRuleViolationPriority; |
| this.renderViolationsByPriority = renderViolationsByPriority; |
| this.aggregate = aggregate; |
| } |
| |
| public void setSuppressedViolations(Collection<SuppressedViolation> suppressedViolations) { |
| this.suppressedViolations = suppressedViolations; |
| } |
| |
| public void setProcessingErrors(Collection<ProcessingError> processingErrors) { |
| this.processingErrors = processingErrors; |
| } |
| |
| @Override |
| public String getTitle() { |
| return getI18nString("title"); |
| } |
| |
| /** |
| * @param key The key. |
| * @return The translated string. |
| */ |
| private String getI18nString(String key) { |
| return i18n.getString("pmd-report", locale, "report.pmd." + key); |
| } |
| |
| protected void renderBody() { |
| startSection(getTitle()); |
| |
| sink.paragraph(); |
| sink.text(getI18nString("pmdlink") + " "); |
| link("https://pmd.github.io", "PMD"); |
| sink.text(" " + AbstractPmdReport.getPmdVersion() + "."); |
| sink.paragraph_(); |
| |
| if (!violations.isEmpty()) { |
| renderViolationsByPriority(); |
| |
| renderViolations(); |
| } else { |
| paragraph(getI18nString("noProblems")); |
| } |
| |
| renderSuppressedViolations(); |
| |
| renderProcessingErrors(); |
| |
| endSection(); |
| } |
| |
| private void startFileSection(String currentFilename, PmdFileInfo fileInfo) { |
| // prepare the filename |
| this.currentFilename = shortenFilename(currentFilename, fileInfo); |
| |
| startSection(makeFileSectionName(this.currentFilename, fileInfo)); |
| |
| startTable(); |
| sink.tableRow(); |
| tableHeaderCell(getI18nString("column.rule")); |
| tableHeaderCell(getI18nString("column.violation")); |
| if (this.renderRuleViolationPriority) { |
| tableHeaderCell(getI18nString("column.priority")); |
| } |
| tableHeaderCell(getI18nString("column.line")); |
| sink.tableRow_(); |
| } |
| |
| private void endFileSection() { |
| endTable(); |
| endSection(); |
| } |
| |
| 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 renderSingleRuleViolation(Violation ruleViolation, PmdFileInfo fileInfo) { |
| sink.tableRow(); |
| sink.tableCell(); |
| addRuleName(ruleViolation); |
| sink.tableCell_(); |
| // May contain content not legit for #tableCell() |
| sink.tableCell(); |
| sink.text(ruleViolation.getText()); |
| sink.tableCell_(); |
| |
| if (this.renderRuleViolationPriority) { |
| tableCell(String.valueOf( |
| RulePriority.valueOf(ruleViolation.getPriority()).getPriority())); |
| } |
| |
| sink.tableCell(); |
| |
| int beginLine = ruleViolation.getBeginline(); |
| outputLineLink(beginLine, fileInfo); |
| int endLine = ruleViolation.getEndline(); |
| if (endLine != beginLine) { |
| sink.text("–"); // \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() { |
| startSection(getI18nString("files")); |
| |
| // TODO files summary |
| renderViolationsTable(violations); |
| |
| endSection(); |
| } |
| |
| private void renderViolationsByPriority() { |
| if (!renderViolationsByPriority) { |
| return; |
| } |
| |
| boolean oldPriorityColumn = this.renderRuleViolationPriority; |
| this.renderRuleViolationPriority = false; |
| |
| startSection(getI18nString("violationsByPriority")); |
| |
| 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; |
| } |
| |
| startSection(getI18nString("priority") + " " + priority.getPriority()); |
| |
| renderViolationsTable(violationsWithPriority); |
| |
| endSection(); |
| } |
| |
| if (violations.isEmpty()) { |
| paragraph(getI18nString("noProblems")); |
| } |
| |
| endSection(); |
| |
| this.renderRuleViolationPriority = oldPriorityColumn; |
| } |
| |
| private void renderViolationsTable(Collection<Violation> violationSegment) { |
| List<Violation> violationSegmentCopy = new ArrayList<>(violationSegment); |
| Collections.sort(violationSegmentCopy, 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 : violationSegmentCopy) { |
| String currentFn = ruleViolation.getFileName(); |
| PmdFileInfo fileInfo = determineFileInfo(currentFn); |
| |
| if (!currentFn.equalsIgnoreCase(previousFilename) && fileSectionStarted) { |
| endFileSection(); |
| fileSectionStarted = false; |
| } |
| if (!fileSectionStarted) { |
| startFileSection(currentFn, fileInfo); |
| fileSectionStarted = true; |
| } |
| |
| renderSingleRuleViolation(ruleViolation, fileInfo); |
| |
| previousFilename = currentFn; |
| } |
| |
| if (fileSectionStarted) { |
| endFileSection(); |
| } |
| } |
| |
| 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() { |
| if (suppressedViolations.isEmpty()) { |
| return; |
| } |
| |
| startSection(getI18nString("suppressedViolations.title")); |
| |
| List<SuppressedViolation> suppressedViolationsCopy = new ArrayList<>(suppressedViolations); |
| Collections.sort(suppressedViolationsCopy, new Comparator<SuppressedViolation>() { |
| @Override |
| public int compare(SuppressedViolation o1, SuppressedViolation o2) { |
| return o1.getFilename().compareTo(o2.getFilename()); |
| } |
| }); |
| |
| startTable(); |
| tableHeader(new String[] { |
| getI18nString("suppressedViolations.column.filename"), |
| getI18nString("suppressedViolations.column.ruleMessage"), |
| getI18nString("suppressedViolations.column.suppressionType"), |
| getI18nString("suppressedViolations.column.userMessage") |
| }); |
| |
| for (SuppressedViolation suppressedViolation : suppressedViolationsCopy) { |
| String filename = suppressedViolation.getFilename(); |
| PmdFileInfo fileInfo = determineFileInfo(filename); |
| filename = shortenFilename(filename, fileInfo); |
| |
| // May contain content not legit for #tableCell() |
| sink.tableRow(); |
| tableCell(filename); |
| sink.tableCell(); |
| sink.text(suppressedViolation.getRuleMessage()); |
| sink.tableCell_(); |
| tableCell(suppressedViolation.getSuppressionType()); |
| sink.tableCell(); |
| sink.text(suppressedViolation.getUserMessage()); |
| sink.tableCell_(); |
| sink.tableRow_(); |
| } |
| |
| endTable(); |
| endSection(); |
| } |
| |
| private void renderProcessingErrors() { |
| if (processingErrors.isEmpty()) { |
| return; |
| } |
| |
| // sort the problem by filename first, since PMD is executed multi-threaded |
| // and might reports the results unsorted |
| List<ProcessingError> processingErrorsCopy = new ArrayList<>(processingErrors); |
| Collections.sort(processingErrorsCopy, new Comparator<ProcessingError>() { |
| @Override |
| public int compare(ProcessingError e1, ProcessingError e2) { |
| return e1.getFilename().compareTo(e2.getFilename()); |
| } |
| }); |
| |
| startSection(getI18nString("processingErrors.title")); |
| |
| startTable(); |
| tableHeader(new String[] { |
| getI18nString("processingErrors.column.filename"), getI18nString("processingErrors.column.problem") |
| }); |
| |
| for (ProcessingError error : processingErrorsCopy) { |
| renderSingleProcessingError(error); |
| } |
| |
| endTable(); |
| endSection(); |
| } |
| |
| private void renderSingleProcessingError(ProcessingError error) { |
| String filename = error.getFilename(); |
| PmdFileInfo fileInfo = determineFileInfo(filename); |
| filename = makeFileSectionName(shortenFilename(filename, fileInfo), fileInfo); |
| |
| sink.tableRow(); |
| tableCell(filename); |
| sink.tableCell(); |
| sink.text(error.getMsg()); |
| sink.verbatim(null); |
| sink.rawText(error.getDetail()); |
| sink.verbatim_(); |
| sink.tableCell_(); |
| sink.tableRow_(); |
| } |
| |
| 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) { |
| try { |
| 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; |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| } |
| } |