| /* |
| * 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("–"); // \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; |
| } |
| } |