[MPMD-375] Replace *ReportGenerators with a new *ReportRenderers

This closes #130
diff --git a/pom.xml b/pom.xml
index 2e28f68..62a8990 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,7 @@
     <pmdVersion>6.55.0</pmdVersion>
     <slf4jVersion>1.7.36</slf4jVersion>
     <aetherVersion>1.0.0.v20140518</aetherVersion>
+    <doxiaVersion>1.12.0</doxiaVersion>
     <compilerPluginVersion>3.11.0</compilerPluginVersion>
     <sitePluginVersion>3.12.1</sitePluginVersion>
     <projectInfoReportsPluginVersion>3.4.3</projectInfoReportsPluginVersion>
@@ -139,6 +140,12 @@
       <groupId>org.apache.maven.shared</groupId>
       <artifactId>maven-common-artifact-filters</artifactId>
       <version>3.3.2</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.sonatype.sisu</groupId>
+          <artifactId>sisu-inject-plexus</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
     <dependency>
       <groupId>org.apache.maven</groupId>
@@ -190,7 +197,24 @@
     <dependency>
       <groupId>org.apache.maven.doxia</groupId>
       <artifactId>doxia-sink-api</artifactId>
-      <version>1.12.0</version>
+      <version>${doxiaVersion}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-container-default</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.doxia</groupId>
+      <artifactId>doxia-core</artifactId>
+      <version>${doxiaVersion}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-container-default</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
     <dependency>
       <groupId>org.apache.maven.doxia</groupId>
@@ -207,6 +231,14 @@
           <groupId>org.apache.httpcomponents</groupId>
           <artifactId>httpclient</artifactId>
         </exclusion>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-container-default</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-component-api</artifactId>
+        </exclusion>
       </exclusions>
     </dependency>
 
@@ -227,6 +259,17 @@
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-utils</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.plexus</groupId>
+      <artifactId>plexus-i18n</artifactId>
+      <version>1.0-beta-10</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-component-api</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
 
     <!-- test -->
     <dependency>
@@ -240,6 +283,12 @@
       <artifactId>maven-plugin-testing-harness</artifactId>
       <version>3.3.0</version>
       <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-container-default</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
     <dependency>
       <groupId>com.github.tomakehurst</groupId>
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 e5c4249..c01fdbc 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.
@@ -97,24 +98,35 @@
     private boolean ignoreAnnotations;
 
     /**
+     * Internationalization component
+     */
+    @Component
+    private I18N i18n;
+
+    /**
      * Contains the result of the last CPD execution.
      * It might be <code>null</code> which means, that CPD
      * has not been executed yet.
      */
     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 @@
         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 generateMavenSiteReport(Locale locale) {
-        CpdReportGenerator gen = new CpdReportGenerator(getSink(), filesToProcess, getBundle(locale), isAggregator());
-        gen.generate(cpdResult.getDuplications());
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -221,10 +230,6 @@
         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/CpdReportGenerator.java
deleted file mode 100644
index dd13594..0000000
--- a/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java
+++ /dev/null
@@ -1,200 +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.util.List;
-import java.util.Map;
-import java.util.ResourceBundle;
-
-import org.apache.maven.doxia.sink.Sink;
-import org.apache.maven.plugins.pmd.model.CpdFile;
-import org.apache.maven.plugins.pmd.model.Duplication;
-import org.apache.maven.project.MavenProject;
-import org.codehaus.plexus.util.StringUtils;
-
-/**
- * Class that generated the CPD report.
- *
- * @author mperham
- * @version $Id$
- */
-public class CpdReportGenerator {
-    private Sink sink;
-
-    private Map<File, PmdFileInfo> fileMap;
-
-    private ResourceBundle bundle;
-
-    private boolean aggregate;
-
-    public CpdReportGenerator(Sink sink, Map<File, PmdFileInfo> fileMap, ResourceBundle bundle, boolean aggregate) {
-        this.sink = sink;
-        this.fileMap = fileMap;
-        this.bundle = bundle;
-        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");
-    }
-
-    /**
-     * Method that generates the start of the CPD report.
-     */
-    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.cpd.cpdlink") + " ");
-        sink.link("https://pmd.github.io/latest/pmd_userdocs_cpd.html");
-        sink.text("CPD");
-        sink.link_();
-        sink.text(" " + AbstractPmdReport.getPmdVersion() + ".");
-        sink.paragraph_();
-
-        sink.section1_();
-
-        // TODO overall summary
-
-        sink.section1();
-        sink.sectionTitle1();
-        sink.text(bundle.getString("report.cpd.dupes"));
-        sink.sectionTitle1_();
-
-        // TODO files summary
-    }
-
-    /**
-     * Method that generates a line of CPD report according to a TokenEntry.
-     */
-    private void generateFileLine(CpdFile duplicationMark) {
-        // Get information for report generation
-        String filename = duplicationMark.getPath();
-        File file = new File(filename);
-        PmdFileInfo fileInfo = fileMap.get(file);
-        File sourceDirectory = fileInfo.getSourceDirectory();
-        filename = StringUtils.substring(
-                filename, sourceDirectory.getAbsolutePath().length() + 1);
-        String xrefLocation = fileInfo.getXrefLocation();
-        MavenProject projectFile = fileInfo.getProject();
-        int line = duplicationMark.getLine();
-
-        sink.tableRow();
-        sink.tableCell();
-        sink.text(filename);
-        sink.tableCell_();
-        if (aggregate) {
-            sink.tableCell();
-            sink.text(projectFile.getName());
-            sink.tableCell_();
-        }
-        sink.tableCell();
-
-        if (xrefLocation != null) {
-            sink.link(xrefLocation + "/"
-                    + filename.replaceAll("\\.java$", ".html").replace('\\', '/') + "#L" + line);
-        }
-        sink.text(String.valueOf(line));
-        if (xrefLocation != null) {
-            sink.link_();
-        }
-
-        sink.tableCell_();
-        sink.tableRow_();
-    }
-
-    /**
-     * Method that generates the contents of the CPD report
-     *
-     * @param duplications the found duplications
-     */
-    public void generate(List<Duplication> duplications) {
-        beginDocument();
-
-        if (duplications.isEmpty()) {
-            sink.paragraph();
-            sink.text(bundle.getString("report.cpd.noProblems"));
-            sink.paragraph_();
-        }
-
-        for (Duplication duplication : duplications) {
-            String code = duplication.getCodefragment();
-
-            sink.table();
-            sink.tableRows(null, false);
-            sink.tableRow();
-            sink.tableHeaderCell();
-            sink.text(bundle.getString("report.cpd.column.file"));
-            sink.tableHeaderCell_();
-            if (aggregate) {
-                sink.tableHeaderCell();
-                sink.text(bundle.getString("report.cpd.column.project"));
-                sink.tableHeaderCell_();
-            }
-            sink.tableHeaderCell();
-            sink.text(bundle.getString("report.cpd.column.line"));
-            sink.tableHeaderCell_();
-            sink.tableRow_();
-
-            // Iterating on every token entry
-            for (CpdFile mark : duplication.getFiles()) {
-                generateFileLine(mark);
-            }
-
-            // Source snippet
-            sink.tableRow();
-
-            int colspan = 2;
-            if (aggregate) {
-                ++colspan;
-            }
-            // TODO Cleaner way to do this?
-            sink.rawText("<td colspan='" + colspan + "'>");
-            sink.verbatim(null);
-            sink.text(code);
-            sink.verbatim_();
-            sink.rawText("</td>");
-            sink.tableRow_();
-            sink.tableRows_();
-            sink.table_();
-        }
-
-        sink.section1_();
-        sink.body_();
-        sink.flush();
-        sink.close();
-    }
-}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/CpdReportRenderer.java b/src/main/java/org/apache/maven/plugins/pmd/CpdReportRenderer.java
new file mode 100644
index 0000000..58f6076
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/CpdReportRenderer.java
@@ -0,0 +1,179 @@
+/*
+ * 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 javax.swing.text.html.HTML.Attribute;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+
+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;
+
+/**
+ * Class that generated the CPD report.
+ *
+ * @author mperham
+ * @version $Id$
+ */
+public class CpdReportRenderer extends AbstractMavenReportRenderer {
+    private final I18N i18n;
+
+    private final Locale locale;
+
+    private final Map<File, PmdFileInfo> files;
+
+    private final Collection<Duplication> duplications;
+
+    private final boolean aggregate;
+
+    public CpdReportRenderer(
+            Sink sink,
+            I18N i18n,
+            Locale locale,
+            Map<File, PmdFileInfo> files,
+            Collection<Duplication> duplications,
+            boolean aggregate) {
+        super(sink);
+        this.i18n = i18n;
+        this.locale = locale;
+        this.files = files;
+        this.duplications = duplications;
+        this.aggregate = aggregate;
+    }
+
+    @Override
+    public String getTitle() {
+        return getI18nString("title");
+    }
+
+    /**
+     * @param key The key.
+     * @return The translated string.
+     */
+    private String getI18nString(String key) {
+        return i18n.getString("cpd-report", locale, "report.cpd." + key);
+    }
+
+    @Override
+    protected void renderBody() {
+        startSection(getTitle());
+
+        sink.paragraph();
+        sink.text(getI18nString("cpdlink") + " ");
+        link("https://pmd.github.io/latest/pmd_userdocs_cpd.html", "CPD");
+        sink.text(" " + AbstractPmdReport.getPmdVersion() + ".");
+        sink.paragraph_();
+
+        // TODO overall summary
+
+        if (!duplications.isEmpty()) {
+            renderDuplications();
+        } else {
+            paragraph(getI18nString("noProblems"));
+        }
+
+        // TODO files summary
+
+        endSection();
+    }
+
+    /**
+     * Method that generates a line of CPD report according to a TokenEntry.
+     */
+    private void generateFileLine(CpdFile duplicationMark) {
+        // Get information for report generation
+        String filename = duplicationMark.getPath();
+        File file = new File(filename);
+        PmdFileInfo fileInfo = files.get(file);
+        File sourceDirectory = fileInfo.getSourceDirectory();
+        filename = StringUtils.substring(
+                filename, sourceDirectory.getAbsolutePath().length() + 1);
+        String xrefLocation = fileInfo.getXrefLocation();
+        MavenProject projectFile = fileInfo.getProject();
+        int line = duplicationMark.getLine();
+
+        sink.tableRow();
+        tableCell(filename);
+        if (aggregate) {
+            tableCell(projectFile.getName());
+        }
+        sink.tableCell();
+
+        if (xrefLocation != null) {
+            sink.link(xrefLocation + "/"
+                    + filename.replaceAll("\\.java$", ".html").replace('\\', '/') + "#L" + line);
+        }
+        sink.text(String.valueOf(line));
+        if (xrefLocation != null) {
+            sink.link_();
+        }
+
+        sink.tableCell_();
+        sink.tableRow_();
+    }
+
+    private void renderDuplications() {
+        startSection(getI18nString("dupes"));
+
+        for (Duplication duplication : duplications) {
+            String code = duplication.getCodefragment();
+
+            startTable();
+            sink.tableRow();
+            tableHeaderCell(getI18nString("column.file"));
+            if (aggregate) {
+                tableHeaderCell(getI18nString("column.project"));
+            }
+            tableHeaderCell(getI18nString("column.line"));
+            sink.tableRow_();
+
+            // Iterating on every token entry
+            for (CpdFile mark : duplication.getFiles()) {
+                generateFileLine(mark);
+            }
+
+            // Source snippet
+            sink.tableRow();
+
+            int colspan = 2;
+            if (aggregate) {
+                colspan = 3;
+            }
+            SinkEventAttributes att = new SinkEventAttributeSet();
+            att.addAttribute(Attribute.COLSPAN, colspan);
+            sink.tableCell(att);
+            verbatimText(code);
+            sink.tableCell_();
+            sink.tableRow_();
+            endTable();
+        }
+
+        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 c3f1065..dab00bb 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;
@@ -238,26 +237,35 @@
     private DependencyResolver dependencyResolver;
 
     /**
+     * Internationalization component
+     */
+    @Component
+    private I18N i18n;
+
+    /**
      * Contains the result of the last PMD execution.
      * It might be <code>null</code> which means, that PMD
      * has not been executed yet.
      */
     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 @@
         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 @@
         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 @@
         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 bb836c9..0000000
--- 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<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;
-    }
-}
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 0000000..3de9146
--- /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<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);
+    }
+
+    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("&#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() {
+        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);
+
+            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<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);
+        }
+    }
+}
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 eed633a..7bde404 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 @@
         assertTrue(str.contains("pmd_rules_java_bestpractices.html#unusedprivatefield\">UnusedPrivateField</a>"));
 
         // there should be the section Violations By Priority
-        assertTrue(str.contains("Violations By Priority</h2>"));
-        assertTrue(str.contains("Priority 3</h3>"));
-        assertTrue(str.contains("Priority 4</h3>"));
+        assertTrue(str.contains("Violations By Priority</h3>"));
+        assertTrue(str.contains("Priority 3</h4>"));
+        assertTrue(str.contains("Priority 4</h4>"));
         // 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 @@
         String str = readFile(generatedReport);
 
         // custom rule without link
-        assertEquals(2, StringUtils.countMatches(str, "<td>CustomRule</td>"));
+        assertEquals(2, StringUtils.countMatches(str, "<td align=\"left\">CustomRule</td>"));
         // standard rule with link
         assertEquals(4, StringUtils.countMatches(str, "\">UnusedPrivateField</a></td>"));
     }