From cca3d153a691406f2450237a901529f66fa90dde Mon Sep 17 00:00:00 2001 From: Michael Osipov Date: Wed, 10 May 2023 18:33:18 +0200 Subject: [PATCH] [MPMD-375] Replace *ReportGenerators with a new *ReportRenderers This closes #129 --- pom.xml | 19 +- .../apache/maven/plugins/pmd/CpdReport.java | 41 +- ...tGenerator.java => CpdReportRenderer.java} | 149 ++--- .../apache/maven/plugins/pmd/PmdReport.java | 76 ++- .../maven/plugins/pmd/PmdReportGenerator.java | 547 ------------------ .../maven/plugins/pmd/PmdReportRenderer.java | 428 ++++++++++++++ .../maven/plugins/pmd/PmdReportTest.java | 8 +- 7 files changed, 574 insertions(+), 694 deletions(-) rename src/main/java/org/apache/maven/plugins/pmd/{CpdReportGenerator.java => CpdReportRenderer.java} (52%) delete mode 100644 src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java create mode 100644 src/main/java/org/apache/maven/plugins/pmd/PmdReportRenderer.java diff --git a/pom.xml b/pom.xml index 2e28f68d..7524abfe 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,7 @@ under the License. 6.55.0 1.7.36 1.0.0.v20140518 + 1.12.0 3.11.0 3.12.1 3.4.3 @@ -190,7 +191,12 @@ under the License. org.apache.maven.doxia doxia-sink-api - 1.12.0 + ${doxiaVersion} + + + org.apache.maven.doxia + doxia-core + ${doxiaVersion} org.apache.maven.doxia @@ -227,6 +233,17 @@ under the License. org.codehaus.plexus plexus-utils + + org.codehaus.plexus + plexus-i18n + 1.0-beta-10 + + + org.codehaus.plexus + plexus-component-api + + + diff --git a/src/main/java/org/apache/maven/plugins/pmd/CpdReport.java b/src/main/java/org/apache/maven/plugins/pmd/CpdReport.java index e5c4249b..c01fdbc4 100644 --- a/src/main/java/org/apache/maven/plugins/pmd/CpdReport.java +++ b/src/main/java/org/apache/maven/plugins/pmd/CpdReport.java @@ -22,10 +22,10 @@ import java.io.UnsupportedEncodingException; import java.util.Locale; import java.util.Properties; -import java.util.ResourceBundle; import net.sourceforge.pmd.cpd.JavaTokenizer; import net.sourceforge.pmd.cpd.renderer.CPDRenderer; +import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.pmd.exec.CpdExecutor; @@ -33,6 +33,7 @@ import org.apache.maven.plugins.pmd.exec.CpdResult; import org.apache.maven.reporting.MavenReportException; import org.apache.maven.toolchain.Toolchain; +import org.codehaus.plexus.i18n.I18N; /** * Creates a report for PMD's Copy/Paste Detector (CPD) tool. @@ -96,6 +97,12 @@ public class CpdReport extends AbstractPmdReport { @Parameter(property = "cpd.ignoreAnnotations", defaultValue = "false") private boolean ignoreAnnotations; + /** + * Internationalization component + */ + @Component + private I18N i18n; + /** * Contains the result of the last CPD execution. * It might be null which means, that CPD @@ -103,18 +110,23 @@ public class CpdReport extends AbstractPmdReport { */ private CpdResult cpdResult; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public String getName(Locale locale) { - return getBundle(locale).getString("report.cpd.name"); + return getI18nString(locale, "name"); + } + + /** {@inheritDoc} */ + public String getDescription(Locale locale) { + return getI18nString(locale, "description"); } /** - * {@inheritDoc} + * @param locale The locale + * @param key The key to search for + * @return The text appropriate for the locale. */ - public String getDescription(Locale locale) { - return getBundle(locale).getString("report.cpd.description"); + protected String getI18nString(Locale locale, String key) { + return i18n.getString("cpd-report", locale, "report.cpd." + key); } /** @@ -126,7 +138,9 @@ public void executeReport(Locale locale) throws MavenReportException { try { Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); - generateMavenSiteReport(locale); + CpdReportRenderer r = new CpdReportRenderer( + getSink(), i18n, locale, filesToProcess, cpdResult.getDuplications(), isAggregator()); + r.render(); } finally { Thread.currentThread().setContextClassLoader(origLoader); } @@ -209,11 +223,6 @@ private void executeCpd() throws MavenReportException { } } - private void generateMavenSiteReport(Locale locale) { - CpdReportGenerator gen = new CpdReportGenerator(getSink(), filesToProcess, getBundle(locale), isAggregator()); - gen.generate(cpdResult.getDuplications()); - } - /** * {@inheritDoc} */ @@ -221,10 +230,6 @@ public String getOutputName() { return "cpd"; } - private static ResourceBundle getBundle(Locale locale) { - return ResourceBundle.getBundle("cpd-report", locale, CpdReport.class.getClassLoader()); - } - /** * Create and return the correct renderer for the output type. * diff --git a/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java b/src/main/java/org/apache/maven/plugins/pmd/CpdReportRenderer.java similarity index 52% rename from src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java rename to src/main/java/org/apache/maven/plugins/pmd/CpdReportRenderer.java index dd135949..58f6076d 100644 --- a/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java +++ b/src/main/java/org/apache/maven/plugins/pmd/CpdReportRenderer.java @@ -18,15 +18,21 @@ */ package org.apache.maven.plugins.pmd; +import javax.swing.text.html.HTML.Attribute; + import java.io.File; -import java.util.List; +import java.util.Collection; +import java.util.Locale; import java.util.Map; -import java.util.ResourceBundle; import org.apache.maven.doxia.sink.Sink; +import org.apache.maven.doxia.sink.SinkEventAttributes; +import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; import org.apache.maven.plugins.pmd.model.CpdFile; import org.apache.maven.plugins.pmd.model.Duplication; import org.apache.maven.project.MavenProject; +import org.apache.maven.reporting.AbstractMavenReportRenderer; +import org.codehaus.plexus.i18n.I18N; import org.codehaus.plexus.util.StringUtils; /** @@ -35,66 +41,66 @@ * @author mperham * @version $Id$ */ -public class CpdReportGenerator { - private Sink sink; +public class CpdReportRenderer extends AbstractMavenReportRenderer { + private final I18N i18n; + + private final Locale locale; - private Map fileMap; + private final Map files; - private ResourceBundle bundle; + private final Collection duplications; - private boolean aggregate; + private final boolean aggregate; - public CpdReportGenerator(Sink sink, Map fileMap, ResourceBundle bundle, boolean aggregate) { - this.sink = sink; - this.fileMap = fileMap; - this.bundle = bundle; + public CpdReportRenderer( + Sink sink, + I18N i18n, + Locale locale, + Map files, + Collection duplications, + boolean aggregate) { + super(sink); + this.i18n = i18n; + this.locale = locale; + this.files = files; + this.duplications = duplications; this.aggregate = aggregate; } - /** - * Method that returns the title of the CPD Report - * - * @return a String that contains the title - */ - private String getTitle() { - return bundle.getString("report.cpd.title"); + @Override + public String getTitle() { + return getI18nString("title"); } /** - * Method that generates the start of the CPD report. + * @param key The key. + * @return The translated string. */ - public void beginDocument() { - sink.head(); - sink.title(); - sink.text(getTitle()); - sink.title_(); - sink.head_(); - - sink.body(); + private String getI18nString(String key) { + return i18n.getString("cpd-report", locale, "report.cpd." + key); + } - sink.section1(); - sink.sectionTitle1(); - sink.text(getTitle()); - sink.sectionTitle1_(); + @Override + protected void renderBody() { + startSection(getTitle()); sink.paragraph(); - sink.text(bundle.getString("report.cpd.cpdlink") + " "); - sink.link("https://pmd.github.io/latest/pmd_userdocs_cpd.html"); - sink.text("CPD"); - sink.link_(); + sink.text(getI18nString("cpdlink") + " "); + link("https://pmd.github.io/latest/pmd_userdocs_cpd.html", "CPD"); sink.text(" " + AbstractPmdReport.getPmdVersion() + "."); sink.paragraph_(); - sink.section1_(); - // TODO overall summary - sink.section1(); - sink.sectionTitle1(); - sink.text(bundle.getString("report.cpd.dupes")); - sink.sectionTitle1_(); + if (!duplications.isEmpty()) { + renderDuplications(); + } else { + paragraph(getI18nString("noProblems")); + } // TODO files summary + + endSection(); } /** @@ -104,7 +110,7 @@ private void generateFileLine(CpdFile duplicationMark) { // Get information for report generation String filename = duplicationMark.getPath(); File file = new File(filename); - PmdFileInfo fileInfo = fileMap.get(file); + PmdFileInfo fileInfo = files.get(file); File sourceDirectory = fileInfo.getSourceDirectory(); filename = StringUtils.substring( filename, sourceDirectory.getAbsolutePath().length() + 1); @@ -113,13 +119,9 @@ private void generateFileLine(CpdFile duplicationMark) { int line = duplicationMark.getLine(); sink.tableRow(); - sink.tableCell(); - sink.text(filename); - sink.tableCell_(); + tableCell(filename); if (aggregate) { - sink.tableCell(); - sink.text(projectFile.getName()); - sink.tableCell_(); + tableCell(projectFile.getName()); } sink.tableCell(); @@ -136,37 +138,19 @@ private void generateFileLine(CpdFile duplicationMark) { sink.tableRow_(); } - /** - * Method that generates the contents of the CPD report - * - * @param duplications the found duplications - */ - public void generate(List duplications) { - beginDocument(); - - if (duplications.isEmpty()) { - sink.paragraph(); - sink.text(bundle.getString("report.cpd.noProblems")); - sink.paragraph_(); - } + private void renderDuplications() { + startSection(getI18nString("dupes")); for (Duplication duplication : duplications) { String code = duplication.getCodefragment(); - sink.table(); - sink.tableRows(null, false); + startTable(); sink.tableRow(); - sink.tableHeaderCell(); - sink.text(bundle.getString("report.cpd.column.file")); - sink.tableHeaderCell_(); + tableHeaderCell(getI18nString("column.file")); if (aggregate) { - sink.tableHeaderCell(); - sink.text(bundle.getString("report.cpd.column.project")); - sink.tableHeaderCell_(); + tableHeaderCell(getI18nString("column.project")); } - sink.tableHeaderCell(); - sink.text(bundle.getString("report.cpd.column.line")); - sink.tableHeaderCell_(); + tableHeaderCell(getI18nString("column.line")); sink.tableRow_(); // Iterating on every token entry @@ -179,22 +163,17 @@ public void generate(List duplications) { int colspan = 2; if (aggregate) { - ++colspan; + colspan = 3; } - // TODO Cleaner way to do this? - sink.rawText(""); - sink.verbatim(null); - sink.text(code); - sink.verbatim_(); - sink.rawText(""); + SinkEventAttributes att = new SinkEventAttributeSet(); + att.addAttribute(Attribute.COLSPAN, colspan); + sink.tableCell(att); + verbatimText(code); + sink.tableCell_(); sink.tableRow_(); - sink.tableRows_(); - sink.table_(); + endTable(); } - sink.section1_(); - sink.body_(); - sink.flush(); - sink.close(); + endSection(); } } diff --git a/src/main/java/org/apache/maven/plugins/pmd/PmdReport.java b/src/main/java/org/apache/maven/plugins/pmd/PmdReport.java index c3f10652..dab00bbf 100644 --- a/src/main/java/org/apache/maven/plugins/pmd/PmdReport.java +++ b/src/main/java/org/apache/maven/plugins/pmd/PmdReport.java @@ -24,10 +24,8 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.ResourceBundle; import net.sourceforge.pmd.renderers.Renderer; -import org.apache.maven.doxia.sink.Sink; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -46,6 +44,7 @@ import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult; import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver; import org.apache.maven.toolchain.Toolchain; +import org.codehaus.plexus.i18n.I18N; import org.codehaus.plexus.resource.ResourceManager; import org.codehaus.plexus.resource.loader.FileResourceCreationException; import org.codehaus.plexus.resource.loader.FileResourceLoader; @@ -237,6 +236,12 @@ public class PmdReport extends AbstractPmdReport { @Component private DependencyResolver dependencyResolver; + /** + * Internationalization component + */ + @Component + private I18N i18n; + /** * Contains the result of the last PMD execution. * It might be null which means, that PMD @@ -244,20 +249,23 @@ public class PmdReport extends AbstractPmdReport { */ private PmdResult pmdResult; - /** - * {@inheritDoc} - */ - @Override + /** {@inheritDoc} */ public String getName(Locale locale) { - return getBundle(locale).getString("report.pmd.name"); + return getI18nString(locale, "name"); + } + + /** {@inheritDoc} */ + public String getDescription(Locale locale) { + return getI18nString(locale, "description"); } /** - * {@inheritDoc} + * @param locale The locale + * @param key The key to search for + * @return The text appropriate for the locale. */ - @Override - public String getDescription(Locale locale) { - return getBundle(locale).getString("report.pmd.description"); + protected String getI18nString(Locale locale, String key) { + return i18n.getString("pmd-report", locale, "report.pmd." + key); } /** @@ -280,7 +288,24 @@ public void executeReport(Locale locale) throws MavenReportException { try { Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); - generateMavenSiteReport(locale); + PmdReportRenderer r = new PmdReportRenderer( + getLog(), + getSink(), + i18n, + locale, + filesToProcess, + pmdResult.getViolations(), + renderRuleViolationPriority, + renderViolationsByPriority, + isAggregator()); + if (renderSuppressedViolations) { + r.setSuppressedViolations(pmdResult.getSuppressedViolations()); + } + if (renderProcessingErrors) { + r.setProcessingErrors(pmdResult.getErrors()); + } + + r.render(); } finally { Thread.currentThread().setContextClassLoader(origLoader); } @@ -422,29 +447,6 @@ private String determineRulesetFilename(String ruleset) { return result; } - private void generateMavenSiteReport(Locale locale) throws MavenReportException { - Sink sink = getSink(); - PmdReportGenerator doxiaRenderer = new PmdReportGenerator(getLog(), sink, getBundle(locale), isAggregator()); - doxiaRenderer.setRenderRuleViolationPriority(renderRuleViolationPriority); - doxiaRenderer.setRenderViolationsByPriority(renderViolationsByPriority); - doxiaRenderer.setFiles(filesToProcess); - doxiaRenderer.setViolations(pmdResult.getViolations()); - if (renderSuppressedViolations) { - doxiaRenderer.setSuppressedViolations(pmdResult.getSuppressedViolations()); - } - if (renderProcessingErrors) { - doxiaRenderer.setProcessingErrors(pmdResult.getErrors()); - } - - try { - doxiaRenderer.beginDocument(); - doxiaRenderer.render(); - doxiaRenderer.endDocument(); - } catch (IOException e) { - getLog().warn("Failure creating the report: " + e.getLocalizedMessage(), e); - } - } - /** * Convenience method to get the location of the specified file name. * @@ -551,10 +553,6 @@ public String getOutputName() { return "pmd"; } - private static ResourceBundle getBundle(Locale locale) { - return ResourceBundle.getBundle("pmd-report", locale, PmdReport.class.getClassLoader()); - } - /** * Create and return the correct renderer for the output type. * diff --git a/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java b/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java deleted file mode 100644 index bb836c95..00000000 --- a/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java +++ /dev/null @@ -1,547 +0,0 @@ -/* - * 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 violations = new HashSet<>(); - - private List suppressedViolations = new ArrayList<>(); - - private List processingErrors = new ArrayList<>(); - - private boolean aggregate; - - private boolean renderRuleViolationPriority; - - private boolean renderViolationsByPriority; - - private Map files; - - // private List metrics = new ArrayList(); - - 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 violations) { - this.violations = new HashSet<>(violations); - } - - public List getViolations() { - return new ArrayList<>(violations); - } - - public void setSuppressedViolations(Collection suppressedViolations) { - this.suppressedViolations = new ArrayList<>(suppressedViolations); - } - - public void setProcessingErrors(Collection errors) { - this.processingErrors = new ArrayList<>(errors); - } - - public List getProcessingErrors() { - return processingErrors; - } - - // public List getMetrics() - // { - // return metrics; - // } - // - // public void setMetrics( List 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 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> violationsByPriority = new HashMap<>(); - for (Violation violation : violations) { - RulePriority priority = RulePriority.valueOf(violation.getPriority()); - List violationSegment = violationsByPriority.get(priority); - if (violationSegment == null) { - violationSegment = new ArrayList<>(); - violationsByPriority.put(priority, violationSegment); - } - violationSegment.add(violation); - } - - for (RulePriority priority : RulePriority.values()) { - List 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 violationSegment) throws IOException { - Collections.sort(violationSegment, new Comparator() { - /** {@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() { - @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() { - @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 files) { - this.files = files; - } - - public void setRenderRuleViolationPriority(boolean renderRuleViolationPriority) { - this.renderRuleViolationPriority = renderRuleViolationPriority; - } - - public void setRenderViolationsByPriority(boolean renderViolationsByPriority) { - this.renderViolationsByPriority = renderViolationsByPriority; - } -} diff --git a/src/main/java/org/apache/maven/plugins/pmd/PmdReportRenderer.java b/src/main/java/org/apache/maven/plugins/pmd/PmdReportRenderer.java new file mode 100644 index 00000000..3de9146f --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/pmd/PmdReportRenderer.java @@ -0,0 +1,428 @@ +/* + * 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 files; + + // TODO Should not share state + private String currentFilename; + + private final Collection violations; + + private boolean renderRuleViolationPriority; + + private final boolean renderViolationsByPriority; + + private final boolean aggregate; + + private Collection suppressedViolations = new ArrayList<>(); + + private Collection processingErrors = new ArrayList<>(); + + public PmdReportRenderer( + Log log, + Sink sink, + I18N i18n, + Locale locale, + Map files, + Collection 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 suppressedViolations) { + this.suppressedViolations = suppressedViolations; + } + + public void setProcessingErrors(Collection 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); + } + + public 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_(); + tableCell(ruleViolation.getText()); + + 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> violationsByPriority = new HashMap<>(); + for (Violation violation : violations) { + RulePriority priority = RulePriority.valueOf(violation.getPriority()); + List violationSegment = violationsByPriority.get(priority); + if (violationSegment == null) { + violationSegment = new ArrayList<>(); + violationsByPriority.put(priority, violationSegment); + } + violationSegment.add(violation); + } + + for (RulePriority priority : RulePriority.values()) { + List 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 violationSegment) { + List violationSegmentCopy = new ArrayList<>(violationSegment); + Collections.sort(violationSegmentCopy, new Comparator() { + /** {@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 suppressedViolationsCopy = new ArrayList<>(suppressedViolations); + Collections.sort(suppressedViolationsCopy, new Comparator() { + @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); + + tableRow(new String[] { + filename, + suppressedViolation.getRuleMessage(), + suppressedViolation.getSuppressionType(), + suppressedViolation.getUserMessage() + }); + } + + 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 processingErrorsCopy = new ArrayList<>(processingErrors); + Collections.sort(processingErrorsCopy, new Comparator() { + @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); + } + } +} diff --git a/src/test/java/org/apache/maven/plugins/pmd/PmdReportTest.java b/src/test/java/org/apache/maven/plugins/pmd/PmdReportTest.java index eed633a4..7bde4041 100644 --- a/src/test/java/org/apache/maven/plugins/pmd/PmdReportTest.java +++ b/src/test/java/org/apache/maven/plugins/pmd/PmdReportTest.java @@ -86,9 +86,9 @@ public void testDefaultConfiguration() throws Exception { assertTrue(str.contains("pmd_rules_java_bestpractices.html#unusedprivatefield\">UnusedPrivateField")); // there should be the section Violations By Priority - assertTrue(str.contains("Violations By Priority")); - assertTrue(str.contains("Priority 3")); - assertTrue(str.contains("Priority 4")); + assertTrue(str.contains("Violations By Priority")); + assertTrue(str.contains("Priority 3")); + assertTrue(str.contains("Priority 4")); // the file App.java is mentioned 3 times: in prio 3, in prio 4 and in the files section assertEquals(3, StringUtils.countMatches(str, "def/configuration/App.java")); @@ -605,7 +605,7 @@ public void testPmdReportCustomRulesNoExternalInfoUrl() throws Exception { String str = readFile(generatedReport); // custom rule without link - assertEquals(2, StringUtils.countMatches(str, "CustomRule")); + assertEquals(2, StringUtils.countMatches(str, "CustomRule")); // standard rule with link assertEquals(4, StringUtils.countMatches(str, "\">UnusedPrivateField")); }