Merge pull request #33 from apache/unused

[MPMD-298] remove httpclient dependency
diff --git a/README.md b/README.md
index 4ba5215..f930f97 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,8 @@
 [![ASF Jira](https://img.shields.io/endpoint?url=https%3A%2F%2Fmaven.apache.org%2Fbadges%2Fasf_jira-MPMD.json)][jira]
 [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/apache/maven.svg?label=License)][license]
 [![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-pmd-plugin.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-pmd-plugin)
-[![Jenkins Status](https://img.shields.io/jenkins/s/https/builds.apache.org/job/maven-box/job/maven-pmd-plugin/job/master.svg?)][build]
-[![Jenkins tests](https://img.shields.io/jenkins/t/https/builds.apache.org/job/maven-box/job/maven-pmd-plugin/job/master.svg?)][test-results]
+[![Jenkins Status](https://img.shields.io/jenkins/s/https/ci-builds.apache.org/job/Maven/job/maven-box/job/maven-pmd-plugin/job/master.svg?)][build]
+[![Jenkins tests](https://img.shields.io/jenkins/t/https/ci-builds.apache.org/job/Maven/job/maven-box/job/maven-pmd-plugin/job/master.svg?)][test-results]
 
 
 You have found a bug or you have an idea for a cool new feature? Contributing
@@ -95,5 +95,5 @@
 [code-style]: https://maven.apache.org/developers/conventions/code.html
 [cla]: https://www.apache.org/licenses/#clas
 [maven-wiki]: https://cwiki.apache.org/confluence/display/MAVEN/Index
-[test-results]: https://builds.apache.org/job/maven-box/job/maven-pmd-plugin/job/master/lastCompletedBuild/testReport/
-[build]: https://builds.apache.org/job/maven-box/job/maven-pmd-plugin/job/master/
+[test-results]: https://ci-builds.apache.org/job/Maven/job/maven-box/job/maven-pmd-plugin/job/master/lastCompletedBuild/testReport/
+[build]: https://ci-builds.apache.org/job/Maven/job/maven-box/job/maven-pmd-plugin/job/master/
diff --git a/pom.xml b/pom.xml
index 6d29fad..ce41830 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
   </parent>
 
   <artifactId>maven-pmd-plugin</artifactId>
-  <version>3.14.0-SNAPSHOT</version>
+  <version>3.15.0-SNAPSHOT</version>
   <packaging>maven-plugin</packaging>
 
   <name>Apache Maven PMD Plugin</name>
@@ -75,7 +75,7 @@
   </issueManagement>
   <ciManagement>
     <system>Jenkins</system>
-    <url>https://builds.apache.org/job/maven-box/job/maven-pmd-plugin/</url>
+    <url>https://ci-builds.apache.org/job/Maven/job/maven-box/job/maven-pmd-plugin/</url>
   </ciManagement>
   <distributionManagement>
     <site>
@@ -89,10 +89,10 @@
     <doxiaVersion>1.9.1</doxiaVersion>
     <doxiaSitetoolsVersion>1.9.2</doxiaSitetoolsVersion>
     <javaVersion>7</javaVersion><!-- Because PMD 5.4+ requires Java 7 -->
-    <pmdVersion>6.26.0</pmdVersion>
+    <pmdVersion>6.29.0</pmdVersion>
     <sitePluginVersion>3.7.1</sitePluginVersion>
     <projectInfoReportsPluginVersion>3.0.0</projectInfoReportsPluginVersion>
-    <project.build.outputTimestamp>2020-04-07T21:04:00Z</project.build.outputTimestamp>
+    <project.build.outputTimestamp>2020-10-24T17:10:38Z</project.build.outputTimestamp>
   </properties>
 
   <dependencies>
@@ -132,18 +132,18 @@
       <artifactId>maven-common-artifact-filters</artifactId>
       <version>3.1.0</version>
     </dependency>
+    <dependency>
+        <groupId>org.apache.maven</groupId>
+        <artifactId>maven-embedder</artifactId>
+        <version>3.1.0</version>
+        <scope>provided</scope>
+    </dependency>
 
     <!-- pmd -->
     <dependency>
         <groupId>org.apache.commons</groupId>
-        <artifactId>commons-text</artifactId>
-        <!-- overwrite version from pmd-core. 1.3 is still compatible with java7, 1.6 not. -->
-        <version>1.3</version>
-    </dependency>
-    <dependency>
-        <groupId>org.apache.commons</groupId>
         <artifactId>commons-lang3</artifactId>
-        <version>3.7</version>
+        <version>3.8.1</version>
     </dependency>
     <dependency>
       <groupId>net.sourceforge.pmd</groupId>
@@ -206,6 +206,11 @@
       <artifactId>maven-reporting-impl</artifactId>
       <version>3.0.0</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven.shared</groupId>
+      <artifactId>maven-shared-utils</artifactId>
+      <version>3.2.1</version>
+    </dependency>
 
     <!-- plexus -->
     <dependency>
@@ -244,6 +249,12 @@
       <version>2.5</version>
       <!-- scope>test</scope> Required by PMD transitively. -->
     </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>1.7.5</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -342,6 +353,7 @@
             <artifactId>maven-invoker-plugin</artifactId>
             <configuration>
               <localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
+              <debug>false</debug>
               <goals>
                 <goal>clean</goal>
                 <goal>site</goal>
diff --git a/src/it/MPMD-165/src/main/java/test/MyClass.java b/src/it/MPMD-165/src/main/java/test/MyClass.java
index 5e8d6f1..6c4c05c 100644
--- a/src/it/MPMD-165/src/main/java/test/MyClass.java
+++ b/src/it/MPMD-165/src/main/java/test/MyClass.java
@@ -24,7 +24,7 @@
 
     public static void main( String[] args )
     {
-        return;
+        return; // This is a UnnecessaryReturn violation
     }
 
 }
diff --git a/src/it/MPMD-244-logging/invoker.properties b/src/it/MPMD-244-logging/invoker.properties
index ea9f79e..d57dce3 100644
--- a/src/it/MPMD-244-logging/invoker.properties
+++ b/src/it/MPMD-244-logging/invoker.properties
@@ -17,3 +17,4 @@
 
 invoker.goals = clean pmd:check
 invoker.maven.version = 3.1.0+
+invoker.debug = true
diff --git a/src/it/MPMD-290-cpd-for-csharp/invoker.properties b/src/it/MPMD-290-cpd-for-csharp/invoker.properties
new file mode 100644
index 0000000..8e84538
--- /dev/null
+++ b/src/it/MPMD-290-cpd-for-csharp/invoker.properties
@@ -0,0 +1,21 @@
+# 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.
+
+invoker.maven.version = 3.3+
+invoker.goals = clean verify
+invoker.buildResult = failure
+invoker.debug = true
diff --git a/src/it/MPMD-290-cpd-for-csharp/pom.xml b/src/it/MPMD-290-cpd-for-csharp/pom.xml
new file mode 100644
index 0000000..38907b4
--- /dev/null
+++ b/src/it/MPMD-290-cpd-for-csharp/pom.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugins.pmd.its</groupId>
+  <artifactId>MPMD-290-cpd-for-csharp</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <description>
+    Use CPD via m-pmd-p to analyze duplications in c# files.
+  </description>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>@project.version@</version>
+        <configuration>
+          <language>cs</language>
+          <minimumTokens>10</minimumTokens>
+          <includes>
+            <include>**/*.cs</include>
+          </includes>
+          <compileSourceRoots>
+            <compileSourceRoot>${basedir}/src/main/cs</compileSourceRoot>
+          </compileSourceRoots>
+          <printFailingErrors>true</printFailingErrors>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>cpd-check</goal>
+            </goals>
+          </execution>
+        </executions>
+        <dependencies>
+            <dependency>
+                <groupId>net.sourceforge.pmd</groupId>
+                <artifactId>pmd-cs</artifactId>
+                <version>@pmdVersion@</version>
+            </dependency>
+        </dependencies>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/src/it/MPMD-290-cpd-for-csharp/src/main/cs/Sample1.cs b/src/it/MPMD-290-cpd-for-csharp/src/main/cs/Sample1.cs
new file mode 100644
index 0000000..de5f199
--- /dev/null
+++ b/src/it/MPMD-290-cpd-for-csharp/src/main/cs/Sample1.cs
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+class Sample1 {
+    public void bar() {
+        int x = 1;
+        int y = 2;
+        int z = x + y;
+    }
+}
\ No newline at end of file
diff --git a/src/it/MPMD-290-cpd-for-csharp/src/main/cs/Sample2.cs b/src/it/MPMD-290-cpd-for-csharp/src/main/cs/Sample2.cs
new file mode 100644
index 0000000..8730046
--- /dev/null
+++ b/src/it/MPMD-290-cpd-for-csharp/src/main/cs/Sample2.cs
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+class Sample2 {
+    public void bar() {
+        int x = 1;
+        int y = 2;
+        int z = x + y;
+    }
+}
\ No newline at end of file
diff --git a/src/it/MPMD-290-cpd-for-csharp/verify.groovy b/src/it/MPMD-290-cpd-for-csharp/verify.groovy
new file mode 100644
index 0000000..18b724a
--- /dev/null
+++ b/src/it/MPMD-290-cpd-for-csharp/verify.groovy
@@ -0,0 +1,36 @@
+
+/*
+ * 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.
+ */
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.exists()
+
+assert buildLog.text.contains( "[INFO] CPD Failure: Found 7 lines of duplicated code at locations" )
+assert buildLog.text.contains( "[DEBUG] PMD failureCount: 1, warningCount: 0" )
+
+File cpdXml = new File( basedir, 'target/cpd.xml' )
+assert cpdXml.exists()
+
+// no duplication for the license header - if this is reported, then CPD uses the wrong language/tokenizer
+assert !cpdXml.text.contains( '<duplication lines="20" tokens="148">' )
+assert !cpdXml.text.contains( 'line="1"' )
+
+// the only valid duplication
+assert cpdXml.text.contains( '<duplication lines="7" tokens="26">' )
+assert cpdXml.text.contains( 'line="20"' )
diff --git a/src/it/MPMD-302-JDK15/invoker.properties b/src/it/MPMD-302-JDK15/invoker.properties
new file mode 100644
index 0000000..c0309db
--- /dev/null
+++ b/src/it/MPMD-302-JDK15/invoker.properties
@@ -0,0 +1,19 @@
+# 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.
+
+invoker.java.version = 15+
+invoker.goals = clean verify
diff --git a/src/it/MPMD-302-JDK15/pom.xml b/src/it/MPMD-302-JDK15/pom.xml
new file mode 100644
index 0000000..37ea830
--- /dev/null
+++ b/src/it/MPMD-302-JDK15/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.maven.plugins.pmd.it</groupId>
+  <artifactId>MPMD-302-JDK15</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <java.version>15</java.version>
+  </properties>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.8.0</version>
+          <configuration>
+            <target>${java.version}</target>
+            <source>${java.version}</source>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>@project.version@</version>
+        <configuration>
+          <skipPmdError>false</skipPmdError>
+          <skip>false</skip>
+          <failOnViolation>true</failOnViolation>
+          <failurePriority>4</failurePriority>
+          <targetJdk>${java.version}</targetJdk>
+          <sourceEncoding>UTF-8</sourceEncoding>
+          <minimumTokens>100</minimumTokens>
+          <excludes>
+            <exclude>**/*Bean.java</exclude>
+            <exclude>**/generated/*.java</exclude>
+          </excludes>
+          <excludeRoots>
+            <excludeRoot>target/generated-sources/stubs</excludeRoot>
+          </excludeRoots>
+          <rulesets/>
+        </configuration>
+        <executions>
+          <execution>
+            <id>default</id>
+            <phase>verify</phase>
+            <goals>
+              <goal>check</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/src/it/MPMD-302-JDK15/src/main/java/com/mycompany/app/App.java b/src/it/MPMD-302-JDK15/src/main/java/com/mycompany/app/App.java
new file mode 100644
index 0000000..8e4972d
--- /dev/null
+++ b/src/it/MPMD-302-JDK15/src/main/java/com/mycompany/app/App.java
@@ -0,0 +1,25 @@
+package com.mycompany.app;
+
+/*
+ * 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.
+ */
+
+public class App
+{
+
+}
diff --git a/src/it/MPMD-302-JDK15/src/main/java/com/mycompany/app/Foo.java b/src/it/MPMD-302-JDK15/src/main/java/com/mycompany/app/Foo.java
new file mode 100644
index 0000000..2a5a2bc
--- /dev/null
+++ b/src/it/MPMD-302-JDK15/src/main/java/com/mycompany/app/Foo.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+
+public class Foo
+{
+    public Foo( final ArrayList<String> foo )
+    {
+    }
+
+}
diff --git a/src/it/MPMD-302-JDK15/verify.groovy b/src/it/MPMD-302-JDK15/verify.groovy
new file mode 100644
index 0000000..1cd85d4
--- /dev/null
+++ b/src/it/MPMD-302-JDK15/verify.groovy
@@ -0,0 +1,22 @@
+
+/*
+ * 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.
+ */
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.exists()
\ No newline at end of file
diff --git a/src/it/MPMD-304-toolchain-support/invoker.properties b/src/it/MPMD-304-toolchain-support/invoker.properties
new file mode 100644
index 0000000..198db8b
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/invoker.properties
@@ -0,0 +1,32 @@
+# 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.
+
+invoker.java.version = 1.7+
+
+# available toolchains under linux:
+# https://github.com/apache/infrastructure-p6/blob/production/modules/build_nodes/files/toolchains.xml
+# the jdk toolchain "11:oracle" is selected in pom.xml
+
+# since the toolchains are only configured under linux slaves
+# we don't use invoker selections here, but selector.groovy
+#invoker.toolchain.jdk.version = 11
+#invoker.toolchain.jdk.vendor = oracle
+
+# the file toolchains.xml will be created by selector.groovy
+# - for linux, ${user.home}/.m2/toolchains.xml will be copied
+# - for windows, a new file will be created using toolchains.windows.xml, see selector.groovy
+invoker.goals = clean verify --toolchains toolchains.xml
diff --git a/src/it/MPMD-304-toolchain-support/pom.xml b/src/it/MPMD-304-toolchain-support/pom.xml
new file mode 100644
index 0000000..5ea8c4c
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/pom.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.maven.plugins.pmd.it</groupId>
+  <artifactId>MPMD-304-toolchain-support</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <java.version>11</java.version>
+  </properties>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.8.0</version>
+          <configuration>
+            <target>${java.version}</target>
+            <source>${java.version}</source>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>@project.version@</version>
+        <configuration>
+          <failOnViolation>false</failOnViolation>
+          <printFailingErrors>true</printFailingErrors>
+          <targetJdk>${java.version}</targetJdk>
+          <sourceEncoding>UTF-8</sourceEncoding>
+          <minimumTokens>10</minimumTokens>
+        </configuration>
+        <executions>
+          <execution>
+            <id>default</id>
+            <phase>verify</phase>
+            <goals>
+              <goal>check</goal>
+              <goal>cpd-check</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-toolchains-plugin</artifactId>
+        <version>3.0.0</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>toolchain</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <toolchains>
+            <jdk>
+              <version>${java.version}</version>
+              <vendor>oracle</vendor>
+            </jdk>
+          </toolchains>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/src/it/MPMD-304-toolchain-support/selector.groovy b/src/it/MPMD-304-toolchain-support/selector.groovy
new file mode 100644
index 0000000..55abd8b
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/selector.groovy
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+File testToolchains = new File( basedir, 'toolchains.xml' )
+
+File userToolchains = new File( System.getProperty( 'user.home' ), '.m2/toolchains.xml' )
+if ( userToolchains.exists() )
+{
+    System.out.println( "INFO: Copying ${userToolchains.absolutePath} to ${testToolchains.absolutePath}" )
+    testToolchains.text = userToolchains.text
+}
+else
+{
+    System.out.println( "WARNING: File ${userToolchains.absolutePath} not found" )
+    if ( System.getProperty( 'os.name' ).startsWith( 'Windows' ) )
+    {
+        String jdk11Windows = 'f:\\jenkins\\tools\\java\\latest11'
+        File windowsToolchains = new File( basedir, 'toolchains.windows.xml' )
+        System.out.println( "INFO: Creating ${testToolchains.absolutePath} with jdk:11:oracle=${jdk11Windows}" )
+
+        String placeholder = '@jdk.home@'
+        String replacement = jdk11Windows
+        // extra escaping of backslashes in the path for Windows
+        replacement = replacement.replaceAll("\\\\", "\\\\\\\\")
+        testToolchains.text = windowsToolchains.text.replaceAll( placeholder, replacement )
+        System.out.println( "Replaced '${placeholder}' with '${replacement}' in '${testToolchains.absolutePath}'." )
+    }
+}
+
+if ( testToolchains.exists() )
+{
+    def toolchains = new XmlParser().parseText( testToolchains.text )
+    def result = toolchains.children().find { toolchain ->
+            toolchain.type.text() == 'jdk' &&
+            toolchain.provides.version.text() == '11' &&
+            toolchain.provides.vendor.text() == 'oracle'
+    }
+    if ( !result )
+    {
+        System.out.println( "WARNING: No jdk toolchain for 11:oracle found" )
+        return false
+    }
+
+    System.out.println( "INFO: Found toolchain: ${result}" )
+    return true
+}
+
+System.out.println( "WARNING: Skipping integration test due to missing toolchain" )
+return false
diff --git a/src/it/MPMD-304-toolchain-support/src/main/java/sample/Name.java b/src/it/MPMD-304-toolchain-support/src/main/java/sample/Name.java
new file mode 100644
index 0000000..1b68095
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/src/main/java/sample/Name.java
@@ -0,0 +1,36 @@
+package sample;
+
+/*
+ * 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.
+ */
+
+public class Name extends Thread
+{
+    private final String name;
+
+    public Name( String name )
+    {
+        this.name = name;
+    }
+
+    @Override
+    public String toString()
+    {
+        return name;
+    }
+}
\ No newline at end of file
diff --git a/src/it/MPMD-304-toolchain-support/src/main/java/sample/Name2.java b/src/it/MPMD-304-toolchain-support/src/main/java/sample/Name2.java
new file mode 100644
index 0000000..2ce276b
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/src/main/java/sample/Name2.java
@@ -0,0 +1,36 @@
+package sample;
+
+/*
+ * 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.
+ */
+
+public class Name2 extends Thread
+{
+    private final String name;
+
+    public Name2( String name )
+    {
+        this.name = name;
+    }
+
+    @Override
+    public String toString()
+    {
+        return name;
+    }
+}
\ No newline at end of file
diff --git a/src/it/MPMD-304-toolchain-support/src/main/java/sample/Sample.java b/src/it/MPMD-304-toolchain-support/src/main/java/sample/Sample.java
new file mode 100644
index 0000000..edcb5da
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/src/main/java/sample/Sample.java
@@ -0,0 +1,46 @@
+package sample;
+
+/*
+ * 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.
+ */
+import java.util.function.Function;
+
+// this class trigger PMD rule category/java/codestyle.xml/ExtendsObject
+public class Sample extends Object
+{
+    public static void main( String ... args )
+    {
+        new Sample();
+    }
+
+    public Sample()
+    {
+        // this triggers category/java/multithreading.xml/DontCallThreadRun
+        // it only works, if the auxclass path could be loaded,
+        // by using the correct jdk toolchain
+        new Name( "foo" ).run();
+        Name name = getName( new Name( "World" ) );
+        Function<Name, String> greeter = (var n) -> "Hello " + n;
+        System.out.println( greeter.apply( name ) );
+    }
+
+    private Name getName( Name name )
+    {
+        return new Name( name.toString() + "!" );
+    }
+}
diff --git a/src/it/MPMD-304-toolchain-support/toolchains.windows.xml b/src/it/MPMD-304-toolchain-support/toolchains.windows.xml
new file mode 100644
index 0000000..e3acb90
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/toolchains.windows.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF8"?>
+
+<!--
+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.
+-->
+
+<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
+    <toolchain>
+        <type>jdk</type>
+        <provides>
+            <version>11</version>
+            <vendor>oracle</vendor>
+        </provides>
+        <configuration>
+            <!-- this placeholder will be replaced by selector.groovy -->
+            <jdkHome>@jdk.home@</jdkHome>
+        </configuration>
+    </toolchain>
+</toolchains>
diff --git a/src/it/MPMD-304-toolchain-support/verify.groovy b/src/it/MPMD-304-toolchain-support/verify.groovy
new file mode 100644
index 0000000..7370225
--- /dev/null
+++ b/src/it/MPMD-304-toolchain-support/verify.groovy
@@ -0,0 +1,49 @@
+
+/*
+ * 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.
+ */
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.exists()
+
+assert buildLog.text.contains( '[INFO] Toolchain in maven-pmd-plugin: JDK[' )
+assert buildLog.text.contains( '[INFO] PMD Failure: sample.Sample:24 Rule:ExtendsObject' )
+assert buildLog.text.contains( '[INFO] PMD Failure: sample.Sample:36 Rule:DontCallThreadRun' )
+assert buildLog.text.contains( '[INFO] You have 2 PMD violations.' )
+assert buildLog.text.contains( '[INFO] You have 1 CPD duplication' )
+
+File pmdReport = new File( basedir, 'target/pmd.xml' )
+assert pmdReport.exists()
+assert pmdReport.text.contains( '<violation beginline="24" endline="24" begincolumn="29" endcolumn="34" rule="ExtendsObject"' )
+assert pmdReport.text.contains( '<violation beginline="36" endline="36" begincolumn="9" endcolumn="31" rule="DontCallThreadRun"' )
+
+File pmdSite = new File( basedir, 'target/site/pmd.html' )
+assert pmdSite.exists()
+assert pmdSite.text.contains( 'Sample.java' )
+assert pmdSite.text.contains( 'ExtendsObject' )
+assert pmdSite.text.contains( 'DontCallThreadRun' )
+
+File cpdReport = new File( basedir, 'target/cpd.xml' )
+assert cpdReport.exists()
+assert cpdReport.text.contains( 'Name.java' )
+assert cpdReport.text.contains( 'Name2.java' )
+
+File cpdSite = new File( basedir, 'target/site/cpd.html' )
+assert cpdSite.exists()
+assert cpdSite.text.contains( 'Name.java' )
+assert cpdSite.text.contains( 'Name2.java' )
diff --git a/src/main/java/org/apache/maven/plugins/pmd/AbstractPmdReport.java b/src/main/java/org/apache/maven/plugins/pmd/AbstractPmdReport.java
index 48567a9..15bb1cd 100644
--- a/src/main/java/org/apache/maven/plugins/pmd/AbstractPmdReport.java
+++ b/src/main/java/org/apache/maven/plugins/pmd/AbstractPmdReport.java
@@ -21,6 +21,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -29,24 +31,23 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.logging.SimpleFormatter;
-
-import net.sourceforge.pmd.PMD;
 
 import org.apache.maven.doxia.siterenderer.Renderer;
+import org.apache.maven.execution.MavenSession;
 import org.apache.maven.model.ReportPlugin;
+import org.apache.maven.model.Reporting;
 import org.apache.maven.plugins.annotations.Component;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.project.MavenProject;
 import org.apache.maven.reporting.AbstractMavenReport;
+import org.apache.maven.toolchain.Toolchain;
+import org.apache.maven.toolchain.ToolchainManager;
 import org.codehaus.plexus.util.FileUtils;
 import org.codehaus.plexus.util.PathTool;
 import org.codehaus.plexus.util.ReaderFactory;
 import org.codehaus.plexus.util.StringUtils;
-import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import net.sourceforge.pmd.PMDVersion;
 
 /**
  * Base class for the PMD reports.
@@ -57,6 +58,10 @@
 public abstract class AbstractPmdReport
     extends AbstractMavenReport
 {
+    // ----------------------------------------------------------------------
+    // Configurables
+    // ----------------------------------------------------------------------
+
     /**
      * The output directory for the intermediate XML report.
      */
@@ -72,21 +77,10 @@
     protected File outputDirectory;
 
     /**
-     * Site rendering component for generating the HTML report.
-     */
-    @Component
-    private Renderer siteRenderer;
-
-    /**
-     * The project to analyse.
-     */
-    @Parameter( defaultValue = "${project}", readonly = true, required = true )
-    protected MavenProject project;
-
-    /**
      * Set the output format type, in addition to the HTML report. Must be one of: "none", "csv", "xml", "txt" or the
      * full class name of the PMD renderer to use. See the net.sourceforge.pmd.renderers package javadoc for available
-     * renderers. XML is required if the pmd:check goal is being used.
+     * renderers. XML is produced in any case, since this format is needed
+     * for the check goals (pmd:check, pmd:cpd-check).
      */
     @Parameter( property = "format", defaultValue = "xml" )
     protected String format = "xml";
@@ -187,12 +181,6 @@
     private String outputEncoding;
 
     /**
-     * The projects in the reactor for aggregation report.
-     */
-    @Parameter( property = "reactorProjects", readonly = true )
-    protected List<MavenProject> reactorProjects;
-
-    /**
      * Whether to include the xml files generated by PMD/CPD in the site.
      *
      * @since 3.0
@@ -235,11 +223,48 @@
     protected boolean showPmdLog = true;
 
     /**
-     * This holds a strong reference in case we configured the logger to
-     * redirect to slf4j. See {@link #showPmdLog}. Without a strong reference,
-     * the logger might be garbage collected and the redirect to slf4j is gone.
+     * <p>
+     * Specify the requirements for this jdk toolchain.
+     * This overrules the toolchain selected by the maven-toolchain-plugin.
+     * </p>
+     * <strong>note:</strong> requires at least Maven 3.3.1
+     *
+     * @since 3.14.0
      */
-    private Logger julLogger;
+    @Parameter
+    private Map<String, String> jdkToolchain;
+
+    // ----------------------------------------------------------------------
+    // Read-only parameters
+    // ----------------------------------------------------------------------
+
+    /**
+     * The project to analyse.
+     */
+    @Parameter( defaultValue = "${project}", readonly = true, required = true )
+    protected MavenProject project;
+
+    /**
+     * The projects in the reactor for aggregation report.
+     */
+    @Parameter( property = "reactorProjects", readonly = true )
+    protected List<MavenProject> reactorProjects;
+
+    /**
+     * The current build session instance. This is used for
+     * toolchain manager API calls and for dependency resolver API calls.
+     */
+    @Parameter( defaultValue = "${session}", required = true, readonly = true )
+    protected MavenSession session;
+
+    /**
+     * Site rendering component for generating the HTML report.
+     */
+    @Component
+    private Renderer siteRenderer;
+
+    @Component
+    private ToolchainManager toolchainManager;
 
     /** The files that are being analyzed. */
     protected Map<File, PmdFileInfo> filesToProcess;
@@ -284,7 +309,10 @@
             else
             {
                 // Not yet generated - check if the report is on its way
-                List<ReportPlugin> reportPlugins = project.getReportPlugins();
+                Reporting reporting = project.getModel().getReporting();
+                List<ReportPlugin> reportPlugins = reporting != null
+                        ? reporting.getPlugins()
+                        : Collections.<ReportPlugin>emptyList();
                 for ( ReportPlugin plugin : reportPlugins )
                 {
                     String artifactId = plugin.getArtifactId();
@@ -482,11 +510,6 @@
         return StringUtils.join( patterns.iterator(), "," );
     }
 
-    protected boolean isHtml()
-    {
-        return "html".equals( format );
-    }
-
     protected boolean isXml()
     {
         return "xml".equals( format );
@@ -555,54 +578,63 @@
         return ( outputEncoding != null ) ? outputEncoding : ReaderFactory.UTF_8;
     }
 
-    protected void setupPmdLogging()
+    protected String determineCurrentRootLogLevel()
     {
-        if ( !showPmdLog )
+        String logLevel = System.getProperty( "org.slf4j.simpleLogger.defaultLogLevel" );
+        if ( logLevel == null )
         {
-            return;
+            logLevel = System.getProperty( "maven.logging.root.level" );
         }
-
-        Logger logger = Logger.getLogger( "net.sourceforge.pmd" );
-
-        boolean slf4jBridgeAlreadyAdded = false;
-        for ( Handler handler : logger.getHandlers() )
+        if ( logLevel == null )
         {
-            if ( handler instanceof SLF4JBridgeHandler )
-            {
-                slf4jBridgeAlreadyAdded = true;
-                break;
-            }
+            // TODO: logback level
+            logLevel = "info";
         }
-
-        if ( slf4jBridgeAlreadyAdded )
-        {
-            return;
-        }
-
-        SLF4JBridgeHandler handler = new SLF4JBridgeHandler();
-        SimpleFormatter formatter = new SimpleFormatter();
-        handler.setFormatter( formatter );
-        logger.setUseParentHandlers( false );
-        logger.addHandler( handler );
-        handler.setLevel( Level.ALL );
-        logger.setLevel( Level.ALL );
-        julLogger = logger;
-        julLogger.fine(  "Configured jul-to-slf4j bridge for " + logger.getName() );
+        return logLevel;
     }
 
     static String getPmdVersion()
     {
-        try
+        return PMDVersion.VERSION;
+    }
+
+    //TODO remove the part with ToolchainManager lookup once we depend on
+    //3.0.9 (have it as prerequisite). Define as regular component field then.
+    protected final Toolchain getToolchain()
+    {
+        Toolchain tc = null;
+
+        if ( jdkToolchain != null )
         {
-            return (String) PMD.class.getField( "VERSION" ).get( null );
+            // Maven 3.3.1 has plugin execution scoped Toolchain Support
+            try
+            {
+                Method getToolchainsMethod =
+                    toolchainManager.getClass().getMethod( "getToolchains", MavenSession.class, String.class,
+                                                           Map.class );
+
+                @SuppressWarnings( "unchecked" )
+                List<Toolchain> tcs =
+                    (List<Toolchain>) getToolchainsMethod.invoke( toolchainManager, session, "jdk",
+                                                                  jdkToolchain );
+
+                if ( tcs != null && !tcs.isEmpty() )
+                {
+                    tc = tcs.get( 0 );
+                }
+            }
+            catch ( NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
+                | InvocationTargetException e )
+            {
+                // ignore
+            }
         }
-        catch ( IllegalAccessException e )
+
+        if ( tc == null )
         {
-            throw new RuntimeException( "PMD VERSION field not accessible", e );
+            tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
         }
-        catch ( NoSuchFieldException e )
-        {
-            throw new RuntimeException( "PMD VERSION field not found", e );
-        }
+
+        return tc;
     }
 }
\ No newline at end of file
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 d9fb606..4b3ad5b 100644
--- a/src/main/java/org/apache/maven/plugins/pmd/CpdReport.java
+++ b/src/main/java/org/apache/maven/plugins/pmd/CpdReport.java
@@ -20,38 +20,24 @@
  */
 
 import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Locale;
 import java.util.Properties;
 import java.util.ResourceBundle;
 
-import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.pmd.exec.CpdExecutor;
+import org.apache.maven.plugins.pmd.exec.CpdRequest;
+import org.apache.maven.plugins.pmd.exec.CpdResult;
 import org.apache.maven.reporting.MavenReportException;
-import org.codehaus.plexus.util.FileUtils;
+import org.apache.maven.shared.utils.logging.MessageUtils;
+import org.apache.maven.toolchain.Toolchain;
 import org.codehaus.plexus.util.StringUtils;
 import org.codehaus.plexus.util.WriterFactory;
 
-import net.sourceforge.pmd.cpd.CPD;
-import net.sourceforge.pmd.cpd.CPDConfiguration;
-import net.sourceforge.pmd.cpd.CSVRenderer;
-import net.sourceforge.pmd.cpd.EcmascriptLanguage;
-import net.sourceforge.pmd.cpd.JSPLanguage;
-import net.sourceforge.pmd.cpd.JavaLanguage;
 import net.sourceforge.pmd.cpd.JavaTokenizer;
-import net.sourceforge.pmd.cpd.Language;
-import net.sourceforge.pmd.cpd.LanguageFactory;
-import net.sourceforge.pmd.cpd.Match;
-import net.sourceforge.pmd.cpd.XMLRenderer;
 import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
 
 /**
@@ -116,11 +102,12 @@
     @Parameter( property = "cpd.ignoreAnnotations", defaultValue = "false" )
     private boolean ignoreAnnotations;
 
-    /** The CPD instance used to analyze the files. Will itself collect the duplicated code matches. */
-    private CPD cpd;
-
-    /** Helper to exclude duplications from the result. */
-    private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
+    /**
+     * 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}
@@ -191,10 +178,10 @@
         {
             try
             {
-                executeCpdWithClassloader();
+                executeCpd();
                 if ( skipEmptyReport )
                 {
-                    result = cpd.getMatches().hasNext();
+                    result = cpdResult.hasDuplications();
                     if ( result )
                     {
                         getLog().debug( "Skipping report since skipEmptyReport is true and there are no CPD issues." );
@@ -209,92 +196,60 @@
         return result;
     }
 
-    private void executeCpdWithClassloader()
-        throws MavenReportException
-    {
-        ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
-        try
-        {
-            Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
-            executeCpd();
-        }
-        finally
-        {
-            Thread.currentThread().setContextClassLoader( origLoader );
-        }
-    }
-
     private void executeCpd()
         throws MavenReportException
     {
-        if ( cpd != null )
+        if ( cpdResult != null )
         {
             // CPD has already been run
             getLog().debug( "CPD has already been run - skipping redundant execution." );
             return;
         }
 
-        setupPmdLogging();
-
-        Properties p = new Properties();
+        Properties languageProperties = new Properties();
         if ( ignoreLiterals )
         {
-            p.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
+            languageProperties.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
         }
         if ( ignoreIdentifiers )
         {
-            p.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
+            languageProperties.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
         }
         if ( ignoreAnnotations )
         {
-            p.setProperty( JavaTokenizer.IGNORE_ANNOTATIONS, "true" );
+            languageProperties.setProperty( JavaTokenizer.IGNORE_ANNOTATIONS, "true" );
         }
         try
         {
-            if ( filesToProcess == null )
+            filesToProcess = getFilesToProcess();
+
+            CpdRequest request = new CpdRequest();
+            request.setMinimumTokens( minimumTokens );
+            request.setLanguage( language );
+            request.setLanguageProperties( languageProperties );
+            request.setSourceEncoding( determineEncoding( !filesToProcess.isEmpty() ) );
+            request.addFiles( filesToProcess.keySet() );
+            
+            request.setShowPmdLog( showPmdLog );
+            request.setColorizedLog( MessageUtils.isColorEnabled() );
+            request.setLogLevel( determineCurrentRootLogLevel() );
+            
+            request.setExcludeFromFailureFile( excludeFromFailureFile );
+            request.setTargetDirectory( targetDirectory.getAbsolutePath() );
+            request.setOutputEncoding( getOutputEncoding() );
+            request.setFormat( format );
+            request.setIncludeXmlInSite( includeXmlInSite );
+            request.setReportOutputDirectory( getReportOutputDirectory().getAbsolutePath() );
+
+            Toolchain tc = getToolchain();
+            if ( tc != null )
             {
-                filesToProcess = getFilesToProcess();
+                getLog().info( "Toolchain in maven-pmd-plugin: " + tc );
+                String javaExecutable = tc.findTool( "java" ); //NOI18N
+                request.setJavaExecutable( javaExecutable );
             }
 
-            try
-            {
-                excludeDuplicationsFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
-            }
-            catch ( MojoExecutionException e )
-            {
-                throw new MavenReportException( "Error loading exclusions", e );
-            }
-
-            String encoding = determineEncoding( !filesToProcess.isEmpty() );
-            Language cpdLanguage;
-            if ( "java".equals ( language ) || null == language )
-            {
-                cpdLanguage = new JavaLanguage( p );
-            }
-            else if ( "javascript".equals( language ) )
-            {
-                cpdLanguage = new EcmascriptLanguage();
-            }
-            else if ( "jsp".equals( language ) )
-            {
-                cpdLanguage = new JSPLanguage();
-            }
-            else
-            {
-                cpdLanguage = LanguageFactory.createLanguage( language, p );
-            }
-
-            CPDConfiguration cpdConfiguration = new CPDConfiguration();
-            cpdConfiguration.setMinimumTileSize( minimumTokens );
-            cpdConfiguration.setLanguage( cpdLanguage );
-            cpdConfiguration.setSourceEncoding( encoding );
-
-            cpd = new CPD( cpdConfiguration );
-
-            for ( File file : filesToProcess.keySet() )
-            {
-                cpd.add( file );
-            }
+            cpdResult = CpdExecutor.execute( request );
         }
         catch ( UnsupportedEncodingException e )
         {
@@ -304,50 +259,12 @@
         {
             throw new MavenReportException( e.getMessage(), e );
         }
-        getLog().debug( "Executing CPD..." );
-        cpd.go();
-        getLog().debug( "CPD finished." );
-
-        // always create XML format. we need to output it even if the file list is empty or we have no duplications
-        // so the "check" goals can check for violations
-        writeXmlReport( cpd );
-
-        // html format is handled by maven site report, xml format as already bean rendered
-        if ( !isHtml() && !isXml() )
-        {
-            writeFormattedReport( cpd );
-        }
-    }
-
-    private Iterator<Match> filterMatches( Iterator<Match> matches )
-    {
-        getLog().debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
-            + " configured exclusions." );
-
-        List<Match> filteredMatches = new ArrayList<>();
-        int excludedDuplications = 0;
-        while ( matches.hasNext() )
-        {
-            Match match = matches.next();
-            if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) )
-            {
-                excludedDuplications++;
-            }
-            else
-            {
-                filteredMatches.add( match );
-            }
-        }
-
-        getLog().debug( "Excluded " + excludedDuplications + " duplications." );
-        return filteredMatches.iterator();
     }
 
     private void generateMavenSiteReport( Locale locale )
     {
         CpdReportGenerator gen = new CpdReportGenerator( getSink(), filesToProcess, getBundle( locale ), aggregate );
-        Iterator<Match> matches = cpd.getMatches();
-        gen.generate( filterMatches( matches ) );
+        gen.generate( cpdResult.getDuplications() );
     }
 
     private String determineEncoding( boolean showWarn )
@@ -371,53 +288,6 @@
         return encoding;
     }
 
-    private void writeFormattedReport( CPD cpd )
-        throws MavenReportException
-    {
-        CPDRenderer r = createRenderer();
-        writeReport( cpd, r, format );
-
-    }
-
-    void writeXmlReport( CPD cpd ) throws MavenReportException
-    {
-        File targetFile = writeReport( cpd, new XMLRenderer( getOutputEncoding() ), "xml" );
-        if ( includeXmlInSite )
-        {
-            File siteDir = getReportOutputDirectory();
-            siteDir.mkdirs();
-            try
-            {
-                FileUtils.copyFile( targetFile, new File( siteDir, "cpd.xml" ) );
-            }
-            catch ( IOException e )
-            {
-                throw new MavenReportException( e.getMessage(), e );
-            }
-        }
-    }
-
-    private File writeReport( CPD cpd, CPDRenderer r, String extension ) throws MavenReportException
-    {
-        if ( r == null )
-        {
-            return null;
-        }
-
-        File targetFile = new File( targetDirectory, "cpd." + extension );
-        targetDirectory.mkdirs();
-        try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
-        {
-            r.render( filterMatches( cpd.getMatches() ), writer );
-            writer.flush();
-        }
-        catch ( IOException ioe )
-        {
-            throw new MavenReportException( ioe.getMessage(), ioe );
-        }
-        return targetFile;
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -436,32 +306,11 @@
      *
      * @return the renderer based on the configured output
      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
+     * @deprecated Use {@link CpdExecutor#createRenderer(String, String)} instead.
      */
-    public CPDRenderer createRenderer()
-        throws MavenReportException
+    @Deprecated
+    public CPDRenderer createRenderer() throws MavenReportException
     {
-        CPDRenderer renderer = null;
-        if ( "xml".equals( format ) )
-        {
-            renderer = new XMLRenderer( getOutputEncoding() );
-        }
-        else if ( "csv".equals( format ) )
-        {
-            renderer = new CSVRenderer();
-        }
-        else if ( !"".equals( format ) && !"none".equals( format ) )
-        {
-            try
-            {
-                renderer = (CPDRenderer) Class.forName( format ).getConstructor().newInstance();
-            }
-            catch ( Exception e )
-            {
-                throw new MavenReportException( "Can't find CPD custom format " + format + ": "
-                    + e.getClass().getName(), e );
-            }
-        }
-
-        return renderer;
+        return CpdExecutor.createRenderer( format, getOutputEncoding() );
     }
 }
diff --git a/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java b/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java
index 389d6ca..34a0702 100644
--- a/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java
+++ b/src/main/java/org/apache/maven/plugins/pmd/CpdReportGenerator.java
@@ -20,15 +20,13 @@
  */
 
 import java.io.File;
-import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
 
-import net.sourceforge.pmd.cpd.Mark;
-import net.sourceforge.pmd.cpd.Match;
-import net.sourceforge.pmd.cpd.TokenEntry;
-
 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;
 
@@ -107,17 +105,17 @@
     /**
      * Method that generates a line of CPD report according to a TokenEntry.
      */
-    private void generateFileLine( TokenEntry tokenEntry )
+    private void generateFileLine( CpdFile duplicationMark )
     {
         // Get information for report generation
-        String filename = tokenEntry.getTokenSrcID();
+        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 = tokenEntry.getBeginLine();
+        int line = duplicationMark.getLine();
 
         sink.tableRow();
         sink.tableCell();
@@ -149,24 +147,22 @@
     /**
      * Method that generates the contents of the CPD report
      *
-     * @param matches the found duplications
+     * @param duplications the found duplications
      */
-    public void generate( Iterator<Match> matches )
+    public void generate( List<Duplication> duplications )
     {
         beginDocument();
 
-        if ( !matches.hasNext() )
+        if ( duplications.isEmpty() )
         {
             sink.paragraph();
             sink.text( bundle.getString( "report.cpd.noProblems" ) );
             sink.paragraph_();
         }
 
-        while ( matches.hasNext() )
+        for ( Duplication duplication : duplications )
         {
-            Match match = matches.next();
-
-            String code = match.getSourceCodeSlice();
+            String code = duplication.getCodefragment();
 
             sink.table();
             sink.tableRow();
@@ -185,10 +181,8 @@
             sink.tableRow_();
 
             // Iterating on every token entry
-            for ( Iterator<Mark> occurrences = match.iterator(); occurrences.hasNext(); )
+            for ( CpdFile mark : duplication.getFiles() )
             {
-
-                TokenEntry mark = occurrences.next().getToken();
                 generateFileLine( mark );
             }
 
diff --git a/src/main/java/org/apache/maven/plugins/pmd/PmdCollectingRenderer.java b/src/main/java/org/apache/maven/plugins/pmd/PmdCollectingRenderer.java
index e55b779..2370ead 100644
--- a/src/main/java/org/apache/maven/plugins/pmd/PmdCollectingRenderer.java
+++ b/src/main/java/org/apache/maven/plugins/pmd/PmdCollectingRenderer.java
@@ -22,17 +22,16 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 
+import org.codehaus.plexus.util.StringUtils;
+
 import net.sourceforge.pmd.Report;
 import net.sourceforge.pmd.Report.ProcessingError;
 import net.sourceforge.pmd.RuleViolation;
 import net.sourceforge.pmd.renderers.AbstractRenderer;
 import net.sourceforge.pmd.util.datasource.DataSource;
 
-import org.codehaus.plexus.util.StringUtils;
-
 
 /**
  * A PMD renderer, that collects all violations and processing errors
@@ -56,14 +55,8 @@
     @Override
     public void renderFileReport( Report report ) throws IOException
     {
-        for ( RuleViolation v : report )
-        {
-            violations.add( v );
-        }
-        for ( Iterator<ProcessingError> it = report.errors(); it.hasNext(); )
-        {
-            errors.add( it.next() );
-        }
+        violations.addAll( report.getViolations() );
+        errors.addAll( report.getProcessingErrors() );
     }
 
     /**
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 9f008c3..ba06cf8 100644
--- a/src/main/java/org/apache/maven/plugins/pmd/PmdReport.java
+++ b/src/main/java/org/apache/maven/plugins/pmd/PmdReport.java
@@ -20,26 +20,21 @@
  */
 
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.PrintStream;
-import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.ResourceBundle;
 
 import org.apache.maven.doxia.sink.Sink;
-import org.apache.maven.execution.MavenSession;
-import org.apache.maven.plugin.MojoExecutionException;
 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.annotations.ResolutionScope;
+import org.apache.maven.plugins.pmd.exec.PmdExecutor;
+import org.apache.maven.plugins.pmd.exec.PmdRequest;
+import org.apache.maven.plugins.pmd.exec.PmdResult;
 import org.apache.maven.project.DefaultProjectBuildingRequest;
 import org.apache.maven.project.MavenProject;
 import org.apache.maven.project.ProjectBuildingRequest;
@@ -50,37 +45,17 @@
 import org.apache.maven.shared.artifact.filter.resolve.TransformableFilter;
 import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
 import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
+import org.apache.maven.shared.utils.logging.MessageUtils;
+import org.apache.maven.toolchain.Toolchain;
 import org.codehaus.plexus.resource.ResourceManager;
 import org.codehaus.plexus.resource.loader.FileResourceCreationException;
 import org.codehaus.plexus.resource.loader.FileResourceLoader;
 import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
-import org.codehaus.plexus.util.FileUtils;
 import org.codehaus.plexus.util.ReaderFactory;
 import org.codehaus.plexus.util.StringUtils;
 
-import net.sourceforge.pmd.PMD;
-import net.sourceforge.pmd.PMDConfiguration;
-import net.sourceforge.pmd.Report;
-import net.sourceforge.pmd.RuleContext;
-import net.sourceforge.pmd.RulePriority;
-import net.sourceforge.pmd.RuleSetFactory;
-import net.sourceforge.pmd.RuleSetNotFoundException;
 import net.sourceforge.pmd.RuleSetReferenceId;
-import net.sourceforge.pmd.RuleViolation;
-import net.sourceforge.pmd.benchmark.Benchmarker;
-import net.sourceforge.pmd.benchmark.TextReport;
-import net.sourceforge.pmd.lang.LanguageRegistry;
-import net.sourceforge.pmd.lang.LanguageVersion;
-import net.sourceforge.pmd.renderers.CSVRenderer;
-import net.sourceforge.pmd.renderers.HTMLRenderer;
 import net.sourceforge.pmd.renderers.Renderer;
-import net.sourceforge.pmd.renderers.TextRenderer;
-import net.sourceforge.pmd.renderers.XMLRenderer;
-import net.sourceforge.pmd.util.ClasspathClassLoader;
-import net.sourceforge.pmd.util.IOUtil;
-import net.sourceforge.pmd.util.ResourceLoader;
-import net.sourceforge.pmd.util.datasource.DataSource;
-import net.sourceforge.pmd.util.datasource.FileDataSource;
 
 /**
  * Creates a PMD report.
@@ -97,7 +72,8 @@
      * The target JDK to analyze based on. Should match the source used in the compiler plugin. Valid values
      * with the default PMD version are
      * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code>, <code>1.6</code>, <code>1.7</code>,
-     * <code>1.8</code>, <code>9</code>, <code>10</code>, <code>11</code>, <code>12</code>, and <code>13</code>.
+     * <code>1.8</code>, <code>9</code>, <code>10</code>, <code>11</code>, <code>12</code>, <code>13</code>,
+     * <code>14</code>, and <code>15</code>.
      *
      * <p> You can override the default PMD version by specifying PMD as a dependency,
      * see <a href="examples/upgrading-PMD-at-runtime.html">Upgrading PMD at Runtime</a>.</p>
@@ -181,17 +157,6 @@
     private String suppressMarker;
 
     /**
-     */
-    @Component
-    private ResourceManager locator;
-
-    /** The PMD renderer for collecting violations. */
-    private PmdCollectingRenderer renderer;
-
-    /** Helper to exclude violations given as a properties file. */
-    private final ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
-
-    /**
      * per default pmd executions error are ignored to not break the whole
      *
      * @since 3.1
@@ -260,11 +225,22 @@
     @Parameter( property = "pmd.rulesetsTargetDirectory", defaultValue = "${project.build.directory}/pmd/rulesets" )
     private File rulesetsTargetDirectory;
 
+    /**
+     * Used to locate configured rulesets. The rulesets could be on the plugin
+     * classpath or in the local project file system.
+     */
+    @Component
+    private ResourceManager locator;
+
     @Component
     private DependencyResolver dependencyResolver;
 
-    @Parameter( defaultValue = "${session}", required = true, readonly = true )
-    private MavenSession session;
+    /**
+     * 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}
@@ -348,10 +324,10 @@
         {
             try
             {
-                executePmdWithClassloader();
+                executePmd();
                 if ( skipEmptyReport )
                 {
-                    result = renderer.hasViolations();
+                    result = pmdResult.hasViolations();
                     if ( result )
                     {
                         getLog().debug( "Skipping report since skipEmptyReport is true and "
@@ -367,27 +343,10 @@
         return result;
     }
 
-    private void executePmdWithClassloader()
-        throws MavenReportException
-    {
-        ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
-        try
-        {
-            Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
-            executePmd();
-        }
-        finally
-        {
-            Thread.currentThread().setContextClassLoader( origLoader );
-        }
-    }
-
     private void executePmd()
         throws MavenReportException
     {
-        setupPmdLogging();
-
-        if ( renderer != null )
+        if ( pmdResult != null )
         {
             // PMD has already been run
             getLog().debug( "PMD has already been run - skipping redundant execution." );
@@ -396,21 +355,82 @@
 
         try
         {
-            excludeFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
+            filesToProcess = getFilesToProcess();
+
+            if ( filesToProcess.isEmpty() && !"java".equals( language ) )
+            {
+                getLog().warn( "No files found to process. Did you add your additional source folders like javascript?"
+                                   + " (see also build-helper-maven-plugin)" );
+            }
         }
-        catch ( MojoExecutionException e )
+        catch ( IOException e )
         {
-            throw new MavenReportException( "Unable to load exclusions", e );
+            throw new MavenReportException( "Can't get file list", e );
         }
 
+
+        PmdRequest request = new PmdRequest();
+        request.setLanguageAndVersion( language, targetJdk );
+        request.setRulesets( resolveRulesets() );
+        request.setAuxClasspath( typeResolution ? determineAuxClasspath() : null );
+        request.setSourceEncoding( getSourceEncoding() );
+        request.addFiles( filesToProcess.keySet() );
+        request.setMinimumPriority( minimumPriority );
+        request.setSuppressMarker( suppressMarker );
+        request.setBenchmarkOutputLocation( benchmark ? benchmarkOutputFilename : null );
+        request.setAnalysisCacheLocation( analysisCache ? analysisCacheLocation : null );
+        request.setExcludeFromFailureFile( excludeFromFailureFile );
+
+        request.setTargetDirectory( targetDirectory.getAbsolutePath() );
+        request.setOutputEncoding( getOutputEncoding() );
+        request.setFormat( format );
+        request.setShowPmdLog( showPmdLog );
+        request.setColorizedLog( MessageUtils.isColorEnabled() );
+        request.setSkipPmdError( skipPmdError );
+        request.setIncludeXmlInSite( includeXmlInSite );
+        request.setReportOutputDirectory( getReportOutputDirectory().getAbsolutePath() );
+        request.setLogLevel( determineCurrentRootLogLevel() );
+
+        Toolchain tc = getToolchain();
+        if ( tc != null )
+        {
+            getLog().info( "Toolchain in maven-pmd-plugin: " + tc );
+            String javaExecutable = tc.findTool( "java" ); //NOI18N
+            request.setJavaExecutable( javaExecutable );
+        }
+
+        pmdResult = PmdExecutor.execute( request );
+    }
+
+    protected String getSourceEncoding()
+    {
+        String encoding = super.getSourceEncoding();
+        if ( StringUtils.isEmpty( encoding ) )
+        {
+            encoding = ReaderFactory.FILE_ENCODING;
+            if ( !filesToProcess.isEmpty() )
+            {
+                getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
+                               + ", i.e. build is platform dependent!" );
+            }
+        }
+        return encoding;
+    }
+
+
+    /**
+     * Resolves the configured rulesets and copies them as files into the {@link #rulesetsTargetDirectory}.
+     *
+     * @return comma separated list of absolute file paths of ruleset files
+     * @throws MavenReportException if a ruleset could not be found
+     */
+    private String resolveRulesets() throws MavenReportException
+    {
         // configure ResourceManager
         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
         locator.addSearchPath( "url", "" );
         locator.setOutputDirectory( rulesetsTargetDirectory );
 
-        renderer = new PmdCollectingRenderer();
-        PMDConfiguration pmdConfiguration = getPMDConfiguration();
-
         String[] sets = new String[rulesets.length];
         try
         {
@@ -431,159 +451,7 @@
         {
             throw new MavenReportException( e.getMessage(), e );
         }
-        pmdConfiguration.setRuleSets( StringUtils.join( sets, "," ) );
-
-        try
-        {
-            if ( filesToProcess == null )
-            {
-                filesToProcess = getFilesToProcess();
-            }
-
-            if ( filesToProcess.isEmpty() && !"java".equals( language ) )
-            {
-                getLog().warn( "No files found to process. Did you add your additional source folders like javascript?"
-                                   + " (see also build-helper-maven-plugin)" );
-            }
-        }
-        catch ( IOException e )
-        {
-            throw new MavenReportException( "Can't get file list", e );
-        }
-
-        String encoding = getSourceEncoding();
-        if ( StringUtils.isEmpty( encoding ) )
-        {
-            encoding = ReaderFactory.FILE_ENCODING;
-            if ( !filesToProcess.isEmpty() )
-            {
-                getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
-                               + ", i.e. build is platform dependent!" );
-            }
-        }
-        pmdConfiguration.setSourceEncoding( encoding );
-
-        List<DataSource> dataSources = new ArrayList<>( filesToProcess.size() );
-        for ( File f : filesToProcess.keySet() )
-        {
-            dataSources.add( new FileDataSource( f ) );
-        }
-
-        if ( sets.length > 0 )
-        {
-            processFilesWithPMD( pmdConfiguration, dataSources );
-        }
-        else
-        {
-            getLog().debug( "Skipping PMD execution as no rulesets are defined." );
-        }
-
-        if ( renderer.hasErrors() )
-        {
-            if ( !skipPmdError )
-            {
-                getLog().error( "PMD processing errors:" );
-                getLog().error( renderer.getErrorsAsString( getLog().isDebugEnabled() ) );
-                throw new MavenReportException( "Found " + renderer.getErrors().size() + " PMD processing errors" );
-            }
-            getLog().warn( "There are " + renderer.getErrors().size() + " PMD processing errors:" );
-            getLog().warn( renderer.getErrorsAsString( getLog().isDebugEnabled() ) );
-        }
-
-        removeExcludedViolations( renderer.getViolations() );
-
-        // always write XML report, as this might be needed by the check mojo
-        // we need to output it even if the file list is empty or we have no violations
-        // so the "check" goals can check for violations
-        Report report = renderer.asReport();
-        writeXmlReport( report );
-
-        // write any other format except for xml and html. xml as been just produced.
-        // html format is produced by the maven site formatter. Excluding html here
-        // avoids usind PMD's own html formatter, which doesn't fit into the maven site
-        // considering the html/css styling
-        if ( !isHtml() && !isXml() )
-        {
-            writeFormattedReport( report );
-        }
-
-        if ( benchmark )
-        {
-            try ( PrintStream benchmarkFileStream = new PrintStream( benchmarkOutputFilename ) )
-            {
-                ( new TextReport() ).generate( Benchmarker.values(), benchmarkFileStream );
-            }
-            catch ( FileNotFoundException fnfe )
-            {
-                getLog().error( "Unable to generate benchmark file: " + benchmarkOutputFilename, fnfe );
-            }
-        }
-    }
-
-    private void removeExcludedViolations( List<RuleViolation> violations )
-    {
-        getLog().debug( "Removing excluded violations. Using " + excludeFromFile.countExclusions()
-            + " configured exclusions." );
-        int violationsBefore = violations.size();
-
-        Iterator<RuleViolation> iterator = violations.iterator();
-        while ( iterator.hasNext() )
-        {
-            RuleViolation rv = iterator.next();
-            if ( excludeFromFile.isExcludedFromFailure( rv ) )
-            {
-                iterator.remove();
-            }
-        }
-
-        int numberOfExcludedViolations = violationsBefore - violations.size();
-        getLog().debug( "Excluded " + numberOfExcludedViolations + " violations." );
-    }
-
-    private void processFilesWithPMD( PMDConfiguration pmdConfiguration, List<DataSource> dataSources )
-            throws MavenReportException
-    {
-        RuleSetFactory ruleSetFactory = new RuleSetFactory( new ResourceLoader(),
-                RulePriority.valueOf( this.minimumPriority ), true, true );
-        try
-        {
-            // load the ruleset once to log out any deprecated rules as warnings
-            ruleSetFactory.createRuleSets( pmdConfiguration.getRuleSets() );
-        }
-        catch ( RuleSetNotFoundException e1 )
-        {
-            throw new MavenReportException( "The ruleset could not be loaded", e1 );
-        }
-
-        try
-        {
-            getLog().debug( "Executing PMD..." );
-            RuleContext ruleContext = new RuleContext();
-            PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext,
-                              Arrays.<Renderer>asList( renderer ) );
-
-            if ( getLog().isDebugEnabled() )
-            {
-                getLog().debug( "PMD finished. Found " + renderer.getViolations().size() + " violations." );
-            }
-        }
-        catch ( Exception e )
-        {
-            String message = "Failure executing PMD: " + e.getLocalizedMessage();
-            if ( !skipPmdError )
-            {
-                throw new MavenReportException( message, e );
-            }
-            getLog().warn( message, e );
-        }
-        finally
-        {
-            ClassLoader classLoader = pmdConfiguration.getClassLoader();
-            if ( classLoader instanceof ClasspathClassLoader )
-            {
-                IOUtil.tryCloseClassLoader( classLoader );
-            }
-        }
+        return StringUtils.join( sets, "," );
     }
 
     private void generateMavenSiteReport( Locale locale )
@@ -594,10 +462,10 @@
         doxiaRenderer.setRenderRuleViolationPriority( renderRuleViolationPriority );
         doxiaRenderer.setRenderViolationsByPriority( renderViolationsByPriority );
         doxiaRenderer.setFiles( filesToProcess );
-        doxiaRenderer.setViolations( renderer.getViolations() );
+        doxiaRenderer.setViolations( pmdResult.getViolations() );
         if ( renderProcessingErrors )
         {
-            doxiaRenderer.setProcessingErrors( renderer.getErrors() );
+            doxiaRenderer.setProcessingErrors( pmdResult.getErrors() );
         }
 
         try
@@ -645,130 +513,7 @@
         return loc;
     }
 
-    private File writeReport( Report report, Renderer r, String extension ) throws MavenReportException
-    {
-        if ( r == null )
-        {
-            return null;
-        }
-
-        File targetFile = new File( targetDirectory, "pmd." + extension );
-        try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
-        {
-            targetDirectory.mkdirs();
-
-            r.setWriter( writer );
-            r.start();
-            r.renderFileReport( report );
-            r.end();
-            r.flush();
-        }
-        catch ( IOException ioe )
-        {
-            throw new MavenReportException( ioe.getMessage(), ioe );
-        }
-
-        return targetFile;
-    }
-
-    /**
-     * Use the PMD renderers to render in any format aside from HTML and XML.
-     *
-     * @param report
-     * @throws MavenReportException
-     */
-    private void writeFormattedReport( Report report )
-        throws MavenReportException
-    {
-        Renderer r = createRenderer();
-        writeReport( report, r, format );
-    }
-
-    /**
-     * Use the PMD XML renderer to create the XML report format used by the check mojo later on.
-     *
-     * @param report
-     * @throws MavenReportException
-     */
-    private void writeXmlReport( Report report ) throws MavenReportException
-    {
-        File targetFile = writeReport( report, new XMLRenderer( getOutputEncoding() ), "xml" );
-        if ( includeXmlInSite )
-        {
-            File siteDir = getReportOutputDirectory();
-            siteDir.mkdirs();
-            try
-            {
-                FileUtils.copyFile( targetFile, new File( siteDir, "pmd.xml" ) );
-            }
-            catch ( IOException e )
-            {
-                throw new MavenReportException( e.getMessage(), e );
-            }
-        }
-    }
-
-    /**
-     * Constructs the PMD configuration class, passing it an argument that configures the target JDK.
-     *
-     * @return the resulting PMD
-     * @throws org.apache.maven.reporting.MavenReportException if targetJdk is not supported
-     */
-    public PMDConfiguration getPMDConfiguration()
-        throws MavenReportException
-    {
-        PMDConfiguration configuration = new PMDConfiguration();
-        LanguageVersion languageVersion = null;
-
-        if ( ( "java".equals( language ) || null == language ) && null != targetJdk )
-        {
-            languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "java " + targetJdk );
-            if ( languageVersion == null )
-            {
-                throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
-            }
-        }
-        else if ( "javascript".equals( language ) || "ecmascript".equals( language ) )
-        {
-            languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "ecmascript" );
-        }
-        else if ( "jsp".equals( language ) )
-        {
-            languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "jsp" );
-        }
-
-        if ( languageVersion != null )
-        {
-            getLog().debug( "Using language " + languageVersion );
-            configuration.setDefaultLanguageVersion( languageVersion );
-        }
-
-        if ( typeResolution )
-        {
-            configureTypeResolution( configuration );
-        }
-
-        if ( null != suppressMarker )
-        {
-            configuration.setSuppressMarker( suppressMarker );
-        }
-
-        configuration.setBenchmark( benchmark );
-
-        if ( analysisCache )
-        {
-            configuration.setAnalysisCacheLocation( analysisCacheLocation );
-            getLog().debug( "Using analysis cache location: " + analysisCacheLocation );
-        }
-        else
-        {
-            configuration.setIgnoreIncrementalAnalysis( true );
-        }
-
-        return configuration;
-    }
-
-    private void configureTypeResolution( PMDConfiguration configuration ) throws MavenReportException
+    private String determineAuxClasspath() throws MavenReportException
     {
         try
         {
@@ -836,7 +581,7 @@
                 getLog().debug( "Using aux classpath: " + classpath );
             }
             String path = StringUtils.join( classpath.iterator(), File.pathSeparator );
-            configuration.prependClasspath( path );
+            return path;
         }
         catch ( Exception e )
         {
@@ -863,40 +608,11 @@
      *
      * @return the renderer based on the configured output
      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
+     * @deprecated Use {@link PmdExecutor#createRenderer(String, String)} instead.
      */
-    public final Renderer createRenderer()
-        throws MavenReportException
+    @Deprecated
+    public final Renderer createRenderer() throws MavenReportException
     {
-        Renderer result = null;
-        if ( "xml".equals( format ) )
-        {
-            result = new XMLRenderer( getOutputEncoding() );
-        }
-        else if ( "txt".equals( format ) )
-        {
-            result = new TextRenderer();
-        }
-        else if ( "csv".equals( format ) )
-        {
-            result = new CSVRenderer();
-        }
-        else if ( "html".equals( format ) )
-        {
-            result = new HTMLRenderer();
-        }
-        else if ( !"".equals( format ) && !"none".equals( format ) )
-        {
-            try
-            {
-                result = (Renderer) Class.forName( format ).getConstructor().newInstance();
-            }
-            catch ( Exception e )
-            {
-                throw new MavenReportException( "Can't find PMD custom format " + format + ": "
-                    + e.getClass().getName(), e );
-            }
-        }
-
-        return result;
+        return PmdExecutor.createRenderer( format, getOutputEncoding() );
     }
 }
diff --git a/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java b/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java
index 83e32ce..a094363 100644
--- a/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java
+++ b/src/main/java/org/apache/maven/plugins/pmd/PmdReportGenerator.java
@@ -34,11 +34,11 @@
 
 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.Violation;
 import org.codehaus.plexus.util.StringUtils;
 
-import net.sourceforge.pmd.Report.ProcessingError;
 import net.sourceforge.pmd.RulePriority;
-import net.sourceforge.pmd.RuleViolation;
 
 /**
  * Render the PMD violations into Doxia events.
@@ -56,7 +56,7 @@
 
     private ResourceBundle bundle;
 
-    private Set<RuleViolation> violations = new HashSet<>();
+    private Set<Violation> violations = new HashSet<>();
 
     private List<ProcessingError> processingErrors = new ArrayList<>();
 
@@ -83,12 +83,12 @@
         return bundle.getString( "report.pmd.title" );
     }
 
-    public void setViolations( Collection<RuleViolation> violations )
+    public void setViolations( Collection<Violation> violations )
     {
         this.violations = new HashSet<>( violations );
     }
 
-    public List<RuleViolation> getViolations()
+    public List<Violation> getViolations()
     {
         return new ArrayList<>( violations );
     }
@@ -183,30 +183,45 @@
         sink.section_( level );
     }
 
-    private void processSingleRuleViolation( RuleViolation ruleViolation, PmdFileInfo fileInfo )
+    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();
-        sink.link( ruleViolation.getRule().getExternalInfoUrl() );
-        sink.text( ruleViolation.getRule().getName() );
-        sink.link_();
+        addRuleName( ruleViolation );
         sink.tableCell_();
         sink.tableCell();
-        sink.text( ruleViolation.getDescription() );
+        sink.text( ruleViolation.getText() );
         sink.tableCell_();
 
         if ( this.renderRuleViolationPriority )
         {
             sink.tableCell();
-            sink.text( String.valueOf( ruleViolation.getRule().getPriority().getPriority() ) );
+            sink.text( String.valueOf( RulePriority.valueOf( ruleViolation.getPriority() ).getPriority() ) );
             sink.tableCell_();
         }
 
         sink.tableCell();
 
-        int beginLine = ruleViolation.getBeginLine();
+        int beginLine = ruleViolation.getBeginline();
         outputLineLink( beginLine, fileInfo );
-        int endLine = ruleViolation.getEndLine();
+        int endLine = ruleViolation.getEndline();
         if ( endLine != beginLine )
         {
             sink.text( "&#x2013;" ); // \u2013 is a medium long dash character
@@ -230,7 +245,7 @@
 
         // TODO files summary
 
-        List<RuleViolation> violations2 = new ArrayList<>( violations );
+        List<Violation> violations2 = new ArrayList<>( violations );
         renderViolationsTable( 2, violations2 );
 
         sink.section1_();
@@ -251,22 +266,22 @@
         sink.text( bundle.getString( "report.pmd.violationsByPriority" ) );
         sink.sectionTitle1_();
 
-        Map<RulePriority, List<RuleViolation>> violationsByPriority = new HashMap<>();
-        for ( RuleViolation violation : violations )
+        Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
+        for ( Violation violation : violations )
         {
-            RulePriority priority = violation.getRule().getPriority();
-            List<RuleViolation> violationSegment = violationsByPriority.get( priority );
+            RulePriority priority = RulePriority.valueOf( violation.getPriority() );
+            List<Violation> violationSegment = violationsByPriority.get( priority );
             if ( violationSegment == null )
             {
                 violationSegment = new ArrayList<>();
                 violationsByPriority.put( priority, violationSegment );
             }
-            violationsByPriority.get( violation.getRule().getPriority() ).add( violation );
+            violationSegment.add( violation );
         }
 
         for ( RulePriority priority : RulePriority.values() )
         {
-            List<RuleViolation> violationsWithPriority = violationsByPriority.get( priority );
+            List<Violation> violationsWithPriority = violationsByPriority.get( priority );
             if ( violationsWithPriority == null || violationsWithPriority.isEmpty() )
             {
                 continue;
@@ -294,18 +309,18 @@
         this.renderRuleViolationPriority = oldPriorityColumn;
     }
 
-    private void renderViolationsTable( int level, List<RuleViolation> violationSegment )
+    private void renderViolationsTable( int level, List<Violation> violationSegment )
     throws IOException
     {
-        Collections.sort( violationSegment, new Comparator<RuleViolation>()
+        Collections.sort( violationSegment, new Comparator<Violation>()
         {
             /** {@inheritDoc} */
-            public int compare( RuleViolation o1, RuleViolation o2 )
+            public int compare( Violation o1, Violation o2 )
             {
-                int filenames = o1.getFilename().compareTo( o2.getFilename() );
+                int filenames = o1.getFileName().compareTo( o2.getFileName() );
                 if ( filenames == 0 )
                 {
-                    return o1.getBeginLine() - o2.getBeginLine();
+                    return o1.getBeginline() - o2.getBeginline();
                 }
                 else
                 {
@@ -316,9 +331,9 @@
 
         boolean fileSectionStarted = false;
         String previousFilename = null;
-        for ( RuleViolation ruleViolation : violationSegment )
+        for ( Violation ruleViolation : violationSegment )
         {
-            String currentFn = ruleViolation.getFilename();
+            String currentFn = ruleViolation.getFileName();
             PmdFileInfo fileInfo = determineFileInfo( currentFn );
 
             if ( !currentFn.equalsIgnoreCase( previousFilename ) && fileSectionStarted )
@@ -371,7 +386,7 @@
             @Override
             public int compare( ProcessingError e1, ProcessingError e2 )
             {
-                return e1.getFile().compareTo( e2.getFile() );
+                return e1.getFilename().compareTo( e2.getFilename() );
             }
         } );
 
@@ -402,7 +417,7 @@
 
     private void processSingleProcessingError( ProcessingError error ) throws IOException
     {
-        String filename = error.getFile();
+        String filename = error.getFilename();
         PmdFileInfo fileInfo = determineFileInfo( filename );
         filename = makeFileSectionName( shortenFilename( filename, fileInfo ), fileInfo );
 
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/CpdExecutor.java b/src/main/java/org/apache/maven/plugins/pmd/exec/CpdExecutor.java
new file mode 100644
index 0000000..4905eb9
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/CpdExecutor.java
@@ -0,0 +1,347 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.pmd.ExcludeDuplicationsFromFile;
+import org.apache.maven.reporting.MavenReportException;
+import org.codehaus.plexus.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.cpd.CPD;
+import net.sourceforge.pmd.cpd.CPDConfiguration;
+import net.sourceforge.pmd.cpd.CSVRenderer;
+import net.sourceforge.pmd.cpd.EcmascriptLanguage;
+import net.sourceforge.pmd.cpd.JSPLanguage;
+import net.sourceforge.pmd.cpd.JavaLanguage;
+import net.sourceforge.pmd.cpd.Language;
+import net.sourceforge.pmd.cpd.LanguageFactory;
+import net.sourceforge.pmd.cpd.Match;
+import net.sourceforge.pmd.cpd.SimpleRenderer;
+import net.sourceforge.pmd.cpd.XMLRenderer;
+import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
+
+/**
+ * Executes CPD with the configuration provided via {@link CpdRequest}.
+ */
+public class CpdExecutor extends Executor
+{
+    private static final Logger LOG = LoggerFactory.getLogger( CpdExecutor.class );
+
+    public static CpdResult execute( CpdRequest request ) throws MavenReportException
+    {
+        if ( request.getJavaExecutable() != null )
+        {
+            return fork( request );
+        }
+
+        ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
+        try
+        {
+            Thread.currentThread().setContextClassLoader( CpdExecutor.class.getClassLoader() );
+            CpdExecutor cpdExecutor = new CpdExecutor( request );
+            return cpdExecutor.run();
+        }
+        finally
+        {
+            Thread.currentThread().setContextClassLoader( origLoader );
+        }
+    }
+
+    private static CpdResult fork( CpdRequest request )
+            throws MavenReportException
+    {
+        File basePmdDir = new File ( request.getTargetDirectory(), "pmd" );
+        basePmdDir.mkdirs();
+        File cpdRequestFile = new File( basePmdDir, "cpdrequest.bin" );
+        try ( ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( cpdRequestFile ) ) )
+        {
+            out.writeObject( request );
+        }
+        catch ( IOException e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+
+        String classpath = buildClasspath();
+        ProcessBuilder pb = new ProcessBuilder();
+        // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
+        pb.environment().put( "CLASSPATH", classpath );
+        pb.command().add( request.getJavaExecutable() );
+        pb.command().add( CpdExecutor.class.getName() );
+        pb.command().add( cpdRequestFile.getAbsolutePath() );
+
+        LOG.debug( "Executing: CLASSPATH={}, command={}", classpath, pb.command() );
+        try
+        {
+            final Process p = pb.start();
+            // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
+            // and inheritIO would only inherit file handles, not the changed streams.
+            ProcessStreamHandler.start( p.getInputStream(), System.out );
+            ProcessStreamHandler.start( p.getErrorStream(), System.err );
+            int exit = p.waitFor();
+            LOG.debug( "CpdExecutor exit code: {}", exit );
+            if ( exit != 0 )
+            {
+                throw new MavenReportException( "CpdExecutor exited with exit code " + exit );
+            }
+            return new CpdResult( new File( request.getTargetDirectory(), "cpd.xml" ), request.getOutputEncoding() );
+        }
+        catch ( IOException e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+        catch ( InterruptedException e )
+        {
+            Thread.currentThread().interrupt();
+            throw new MavenReportException( e.getMessage(), e );
+        }
+    }
+
+    /**
+     * Execute CPD analysis from CLI.
+     *
+     * <p>
+     * Single arg with the filename to the serialized {@link CpdRequest}.
+     *
+     * <p>
+     * Exit-code: 0 = success, 1 = failure in executing
+     *
+     * @param args
+     */
+    public static void main( String[] args )
+    {
+        File requestFile = new File( args[0] );
+        try ( ObjectInputStream in = new ObjectInputStream( new FileInputStream( requestFile ) ) )
+        {
+            CpdRequest request = (CpdRequest) in.readObject();
+            CpdExecutor cpdExecutor = new CpdExecutor( request );
+            cpdExecutor.setupLogLevel( request.getLogLevel() );
+            cpdExecutor.run();
+            System.exit( 0 );
+        }
+        catch ( IOException | ClassNotFoundException | MavenReportException e )
+        {
+            LOG.error( e.getMessage(), e );
+        }
+        System.exit( 1 );
+    }
+
+    private final CpdRequest request;
+
+    /** Helper to exclude duplications from the result. */
+    private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
+
+    public CpdExecutor( CpdRequest request )
+    {
+        this.request = Objects.requireNonNull( request );
+    }
+
+    private CpdResult run() throws MavenReportException
+    {
+        setupPmdLogging( request.isShowPmdLog(), request.isColorizedLog(), request.getLogLevel() );
+
+        try
+        {
+            excludeDuplicationsFromFile.loadExcludeFromFailuresData( request.getExcludeFromFailureFile() );
+        }
+        catch ( MojoExecutionException e )
+        {
+            throw new MavenReportException( "Error loading exclusions", e );
+        }
+
+        CPDConfiguration cpdConfiguration = new CPDConfiguration();
+        cpdConfiguration.setMinimumTileSize( request.getMinimumTokens() );
+        
+        Language cpdLanguage;
+        if ( "java".equals ( request.getLanguage() ) || null == request.getLanguage() )
+        {
+            cpdLanguage = new JavaLanguage( request.getLanguageProperties() );
+        }
+        else if ( "javascript".equals( request.getLanguage() ) )
+        {
+            cpdLanguage = new EcmascriptLanguage();
+        }
+        else if ( "jsp".equals( request.getLanguage() ) )
+        {
+            cpdLanguage = new JSPLanguage();
+        }
+        else
+        {
+            cpdLanguage = LanguageFactory.createLanguage( request.getLanguage(), request.getLanguageProperties() );
+        }
+        
+        cpdConfiguration.setLanguage( cpdLanguage );
+        cpdConfiguration.setSourceEncoding( request.getSourceEncoding() );
+
+        CPD cpd = new CPD( cpdConfiguration );
+        try
+        {
+            cpd.add( request.getFiles() );
+        }
+        catch ( IOException e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+
+        LOG.debug( "Executing CPD..." );
+        cpd.go();
+        LOG.debug( "CPD finished." );
+
+        // always create XML format. we need to output it even if the file list is empty or we have no duplications
+        // so the "check" goals can check for violations
+        writeXmlReport( cpd );
+
+        // html format is handled by maven site report, xml format has already been rendered
+        String format = request.getFormat();
+        if ( !"html".equals( format ) && !"xml".equals( format ) )
+        {
+            writeFormattedReport( cpd );
+        }
+
+        return new CpdResult( new File( request.getTargetDirectory(), "cpd.xml" ), request.getOutputEncoding() );
+    }
+
+    private void writeXmlReport( CPD cpd ) throws MavenReportException
+    {
+        File targetFile = writeReport( cpd, new XMLRenderer( request.getOutputEncoding() ), "xml" );
+        if ( request.isIncludeXmlInSite() )
+        {
+            File siteDir = new File( request.getReportOutputDirectory() );
+            siteDir.mkdirs();
+            try
+            {
+                FileUtils.copyFile( targetFile, new File( siteDir, "cpd.xml" ) );
+            }
+            catch ( IOException e )
+            {
+                throw new MavenReportException( e.getMessage(), e );
+            }
+        }
+    }
+
+    private File writeReport( CPD cpd, CPDRenderer r, String extension ) throws MavenReportException
+    {
+        if ( r == null )
+        {
+            return null;
+        }
+
+        File targetDir = new File( request.getTargetDirectory() );
+        targetDir.mkdirs();
+        File targetFile = new File( targetDir, "cpd." + extension );
+        try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ),
+                request.getOutputEncoding() ) )
+        {
+            r.render( filterMatches( cpd.getMatches() ), writer );
+            writer.flush();
+        }
+        catch ( IOException ioe )
+        {
+            throw new MavenReportException( ioe.getMessage(), ioe );
+        }
+        return targetFile;
+    }
+
+    private void writeFormattedReport( CPD cpd )
+            throws MavenReportException
+        {
+            CPDRenderer r = createRenderer( request.getFormat(), request.getOutputEncoding() );
+            writeReport( cpd, r, request.getFormat() );
+
+        }
+
+    /**
+     * Create and return the correct renderer for the output type.
+     *
+     * @return the renderer based on the configured output
+     * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
+     */
+    public static CPDRenderer createRenderer( String format, String outputEncoding )
+        throws MavenReportException
+    {
+        CPDRenderer renderer = null;
+        if ( "xml".equals( format ) )
+        {
+            renderer = new XMLRenderer( outputEncoding );
+        }
+        else if ( "csv".equals( format ) )
+        {
+            renderer = new CSVRenderer();
+        }
+        else if ( "txt".equals( format ) )
+        {
+            renderer = new SimpleRenderer();
+        }
+        else if ( !"".equals( format ) && !"none".equals( format ) )
+        {
+            try
+            {
+                renderer = (CPDRenderer) Class.forName( format ).getConstructor().newInstance();
+            }
+            catch ( Exception e )
+            {
+                throw new MavenReportException( "Can't find CPD custom format " + format + ": "
+                    + e.getClass().getName(), e );
+            }
+        }
+
+        return renderer;
+    }
+
+    private Iterator<Match> filterMatches( Iterator<Match> matches )
+    {
+        LOG.debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
+            + " configured exclusions." );
+
+        List<Match> filteredMatches = new ArrayList<>();
+        int excludedDuplications = 0;
+        while ( matches.hasNext() )
+        {
+            Match match = matches.next();
+            if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) )
+            {
+                excludedDuplications++;
+            }
+            else
+            {
+                filteredMatches.add( match );
+            }
+        }
+
+        LOG.debug( "Excluded " + excludedDuplications + " duplications." );
+        return filteredMatches.iterator();
+    }
+
+}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/CpdRequest.java b/src/main/java/org/apache/maven/plugins/pmd/exec/CpdRequest.java
new file mode 100644
index 0000000..694625b
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/CpdRequest.java
@@ -0,0 +1,209 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Data object to store all configuration options needed to execute CPD
+ * as a separate process.
+ * 
+ * <p>This class is intended to be serialized and read back.
+ * 
+ * <p>Some properties might be optional and can be <code>null</code>.
+ */
+public class CpdRequest implements Serializable
+{
+    private static final long serialVersionUID = -7585852992660240668L;
+
+    private String javaExecutable;
+
+    private int minimumTokens;
+    private String language;
+    private Properties languageProperties;
+    private String sourceEncoding;
+    private List<File> files = new ArrayList<>();
+
+    private boolean showPmdLog;
+    private boolean colorizedLog;
+    private String logLevel;
+
+    private String excludeFromFailureFile;
+    private String targetDirectory;
+    private String outputEncoding;
+    private String format;
+    private boolean includeXmlInSite;
+    private String reportOutputDirectory;
+
+    public void setJavaExecutable( String javaExecutable )
+    {
+        this.javaExecutable = javaExecutable;
+    }
+
+    public void setMinimumTokens( int minimumTokens )
+    {
+        this.minimumTokens = minimumTokens;
+    }
+
+    public void setLanguage( String language )
+    {
+        this.language = language;
+    }
+
+    public void setLanguageProperties( Properties languageProperties )
+    {
+        this.languageProperties = languageProperties;
+    }
+
+    public void setSourceEncoding( String sourceEncoding )
+    {
+        this.sourceEncoding = sourceEncoding;
+    }
+
+    public void addFiles( Collection<File> files )
+    {
+        this.files.addAll( files );
+    }
+
+    public void setExcludeFromFailureFile( String excludeFromFailureFile )
+    {
+        this.excludeFromFailureFile = excludeFromFailureFile;
+    }
+
+    public void setTargetDirectory( String targetDirectory )
+    {
+        this.targetDirectory = targetDirectory;
+    }
+
+    public void setOutputEncoding( String outputEncoding )
+    {
+        this.outputEncoding = outputEncoding;
+    }
+
+    public void setFormat( String format )
+    {
+        this.format = format;
+    }
+
+    public void setIncludeXmlInSite( boolean includeXmlInSite )
+    {
+        this.includeXmlInSite = includeXmlInSite;
+    }
+
+    public void setReportOutputDirectory( String reportOutputDirectory )
+    {
+        this.reportOutputDirectory = reportOutputDirectory;
+    }
+
+    public void setShowPmdLog( boolean showPmdLog )
+    {
+        this.showPmdLog = showPmdLog;
+    }
+
+    public void setColorizedLog( boolean colorizedLog )
+    {
+        this.colorizedLog = colorizedLog;
+    }
+
+    public void setLogLevel( String logLevel )
+    {
+        this.logLevel = logLevel;
+    }
+
+    public String getJavaExecutable()
+    {
+        return javaExecutable;
+    }
+
+    public int getMinimumTokens()
+    {
+        return minimumTokens;
+    }
+
+    public String getLanguage()
+    {
+        return language;
+    }
+
+    public Properties getLanguageProperties()
+    {
+        return languageProperties;
+    }
+
+    public String getSourceEncoding()
+    {
+        return sourceEncoding;
+    }
+
+    public List<File> getFiles()
+    {
+        return files;
+    }
+
+    public String getExcludeFromFailureFile()
+    {
+        return excludeFromFailureFile;
+    }
+
+    public String getTargetDirectory()
+    {
+        return targetDirectory;
+    }
+
+    public String getOutputEncoding()
+    {
+        return outputEncoding;
+    }
+
+    public String getFormat()
+    {
+        return format;
+    }
+
+    public boolean isIncludeXmlInSite()
+    {
+        return includeXmlInSite;
+    }
+
+    public String getReportOutputDirectory()
+    {
+        return reportOutputDirectory;
+    }
+
+    public boolean isShowPmdLog()
+    {
+        return showPmdLog;
+    }
+
+    public boolean isColorizedLog()
+    {
+        return colorizedLog;
+    }
+
+    public String getLogLevel()
+    {
+        return logLevel;
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/CpdResult.java b/src/main/java/org/apache/maven/plugins/pmd/exec/CpdResult.java
new file mode 100644
index 0000000..6ea18ce
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/CpdResult.java
@@ -0,0 +1,69 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.maven.plugins.pmd.model.CpdErrorDetail;
+import org.apache.maven.plugins.pmd.model.Duplication;
+import org.apache.maven.plugins.pmd.model.io.xpp3.CpdXpp3Reader;
+import org.apache.maven.reporting.MavenReportException;
+
+/**
+ * Provides access to the result of the CPD analysis.
+ */
+public class CpdResult
+{
+    private final List<Duplication> duplications = new ArrayList<>();
+
+    public CpdResult( File report, String encoding ) throws MavenReportException
+    {
+        loadResult( report, encoding );
+    }
+
+    public List<Duplication> getDuplications()
+    {
+        return duplications;
+    }
+
+    public boolean hasDuplications()
+    {
+        return !duplications.isEmpty();
+    }
+
+    private void loadResult( File report, String encoding ) throws MavenReportException
+    {
+        try ( Reader reader1 = new InputStreamReader( new FileInputStream( report ), encoding ) )
+        {
+            CpdXpp3Reader reader = new CpdXpp3Reader();
+            CpdErrorDetail details = reader.read( reader1, false );
+            duplications.addAll( details.getDuplications() );
+        }
+        catch ( Exception e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/Executor.java b/src/main/java/org/apache/maven/plugins/pmd/exec/Executor.java
new file mode 100644
index 0000000..ca17511
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/Executor.java
@@ -0,0 +1,184 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.SimpleFormatter;
+
+import org.apache.maven.cli.logging.Slf4jConfiguration;
+import org.apache.maven.cli.logging.Slf4jConfigurationFactory;
+import org.apache.maven.shared.utils.logging.MessageUtils;
+import org.codehaus.plexus.logging.console.ConsoleLogger;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+abstract class Executor
+{
+    private static final Logger LOG = LoggerFactory.getLogger( Executor.class );
+
+    /**
+     * This holds a strong reference in case we configured the logger to
+     * redirect to slf4j. See {@link #showPmdLog}. Without a strong reference,
+     * the logger might be garbage collected and the redirect to slf4j is gone.
+     */
+    private java.util.logging.Logger julLogger;
+
+    protected void setupPmdLogging( boolean showPmdLog, boolean colorizedLog, String logLevel )
+    {
+        MessageUtils.setColorEnabled( colorizedLog );
+
+        if ( !showPmdLog )
+        {
+            return;
+        }
+
+        java.util.logging.Logger logger = java.util.logging.Logger.getLogger( "net.sourceforge.pmd" );
+
+        boolean slf4jBridgeAlreadyAdded = false;
+        for ( Handler handler : logger.getHandlers() )
+        {
+            if ( handler instanceof SLF4JBridgeHandler )
+            {
+                slf4jBridgeAlreadyAdded = true;
+                break;
+            }
+        }
+
+        if ( slf4jBridgeAlreadyAdded )
+        {
+            return;
+        }
+
+        SLF4JBridgeHandler handler = new SLF4JBridgeHandler();
+        SimpleFormatter formatter = new SimpleFormatter();
+        handler.setFormatter( formatter );
+        logger.setUseParentHandlers( false );
+        logger.addHandler( handler );
+        handler.setLevel( Level.ALL );
+        logger.setLevel( Level.ALL );
+        julLogger = logger;
+        julLogger.fine( "Configured jul-to-slf4j bridge for " + logger.getName() );
+    }
+
+    protected void setupLogLevel( String logLevel )
+    {
+        ILoggerFactory slf4jLoggerFactory = LoggerFactory.getILoggerFactory();
+        Slf4jConfiguration slf4jConfiguration = Slf4jConfigurationFactory
+                .getConfiguration( slf4jLoggerFactory );
+        if ( "debug".equals( logLevel ) )
+        {
+            slf4jConfiguration
+                    .setRootLoggerLevel( Slf4jConfiguration.Level.DEBUG );
+        }
+        else if ( "info".equals( logLevel ) )
+        {
+            slf4jConfiguration
+                    .setRootLoggerLevel( Slf4jConfiguration.Level.INFO );
+        }
+        else
+        {
+            slf4jConfiguration
+                    .setRootLoggerLevel( Slf4jConfiguration.Level.ERROR );
+        }
+        slf4jConfiguration.activate();
+    }
+
+    protected static String buildClasspath()
+    {
+        StringBuilder classpath = new StringBuilder();
+
+        // plugin classpath needs to come first
+        ClassLoader pluginClassloader = Executor.class.getClassLoader();
+        buildClasspath( classpath, pluginClassloader );
+
+        ClassLoader coreClassloader = ConsoleLogger.class.getClassLoader();
+        buildClasspath( classpath, coreClassloader );
+
+        return classpath.toString();
+    }
+
+    private static void buildClasspath( StringBuilder classpath, ClassLoader cl )
+    {
+        if ( cl instanceof URLClassLoader )
+        {
+            for ( URL url : ( (URLClassLoader) cl ).getURLs() )
+            {
+                String urlString = url.toString();
+                if ( urlString.startsWith( "file:" ) )
+                {
+                    String f = urlString.substring( 5 ); //  strip "file:"
+                    classpath.append( f ).append( File.pathSeparatorChar );
+                }
+            }
+        }
+    }
+
+    protected static class ProcessStreamHandler implements Runnable
+    {
+        private static final int BUFFER_SIZE = 8192;
+
+        private final BufferedInputStream in;
+        private final BufferedOutputStream out;
+
+        public static void start( InputStream in, OutputStream out )
+        {
+            Thread t = new Thread( new ProcessStreamHandler( in, out ) );
+            t.start();
+        }
+
+        private ProcessStreamHandler( InputStream in, OutputStream out )
+        {
+            this.in = new BufferedInputStream( in );
+            this.out = new BufferedOutputStream( out );
+        }
+
+        @Override
+        public void run()
+        {
+            byte[] buffer = new byte[BUFFER_SIZE];
+            try
+            {
+                int count = in.read( buffer );
+                while ( count != -1 )
+                {
+                    out.write( buffer, 0, count );
+                    out.flush();
+                    count = in.read( buffer );
+                }
+                out.flush();
+            }
+            catch ( IOException e )
+            {
+                LOG.error( e.getMessage(), e );
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/PmdExecutor.java b/src/main/java/org/apache/maven/plugins/pmd/exec/PmdExecutor.java
new file mode 100644
index 0000000..b6f1603
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/PmdExecutor.java
@@ -0,0 +1,501 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.pmd.ExcludeViolationsFromFile;
+import org.apache.maven.plugins.pmd.PmdCollectingRenderer;
+import org.apache.maven.reporting.MavenReportException;
+import org.codehaus.plexus.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.PMD;
+import net.sourceforge.pmd.PMDConfiguration;
+import net.sourceforge.pmd.Report;
+import net.sourceforge.pmd.RuleContext;
+import net.sourceforge.pmd.RulePriority;
+import net.sourceforge.pmd.RuleSetFactory;
+import net.sourceforge.pmd.RuleSetNotFoundException;
+import net.sourceforge.pmd.RuleViolation;
+import net.sourceforge.pmd.RulesetsFactoryUtils;
+import net.sourceforge.pmd.benchmark.TextTimingReportRenderer;
+import net.sourceforge.pmd.benchmark.TimeTracker;
+import net.sourceforge.pmd.benchmark.TimingReport;
+import net.sourceforge.pmd.benchmark.TimingReportRenderer;
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.LanguageRegistry;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.renderers.CSVRenderer;
+import net.sourceforge.pmd.renderers.HTMLRenderer;
+import net.sourceforge.pmd.renderers.Renderer;
+import net.sourceforge.pmd.renderers.TextRenderer;
+import net.sourceforge.pmd.renderers.XMLRenderer;
+import net.sourceforge.pmd.util.datasource.DataSource;
+import net.sourceforge.pmd.util.datasource.FileDataSource;
+
+/**
+ * Executes PMD with the configuration provided via {@link PmdRequest}.
+ */
+public class PmdExecutor extends Executor
+{
+    private static final Logger LOG = LoggerFactory.getLogger( PmdExecutor.class );
+
+    public static PmdResult execute( PmdRequest request ) throws MavenReportException
+    {
+        if ( request.getJavaExecutable() != null )
+        {
+            return fork( request );
+        }
+
+        // make sure the class loaders are correct and call this in the same JVM
+        ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
+        try
+        {
+            Thread.currentThread().setContextClassLoader( PmdExecutor.class.getClassLoader() );
+            PmdExecutor executor = new PmdExecutor( request );
+            return executor.run();
+        }
+        finally
+        {
+            Thread.currentThread().setContextClassLoader( origLoader );
+        }
+    }
+
+    private static PmdResult fork( PmdRequest request )
+            throws MavenReportException
+    {
+        File basePmdDir = new File ( request.getTargetDirectory(), "pmd" );
+        basePmdDir.mkdirs();
+        File pmdRequestFile = new File( basePmdDir, "pmdrequest.bin" );
+        try ( ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( pmdRequestFile ) ) )
+        {
+            out.writeObject( request );
+        }
+        catch ( IOException e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+
+        String classpath = buildClasspath();
+        ProcessBuilder pb = new ProcessBuilder();
+        // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
+        pb.environment().put( "CLASSPATH", classpath );
+        pb.command().add( request.getJavaExecutable() );
+        pb.command().add( PmdExecutor.class.getName() );
+        pb.command().add( pmdRequestFile.getAbsolutePath() );
+
+        LOG.debug( "Executing: CLASSPATH={}, command={}", classpath, pb.command() );
+        try
+        {
+            final Process p = pb.start();
+            // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
+            // and inheritIO would only inherit file handles, not the changed streams.
+            ProcessStreamHandler.start( p.getInputStream(), System.out );
+            ProcessStreamHandler.start( p.getErrorStream(), System.err );
+            int exit = p.waitFor();
+            LOG.debug( "PmdExecutor exit code: {}", exit );
+            if ( exit != 0 )
+            {
+                throw new MavenReportException( "PmdExecutor exited with exit code " + exit );
+            }
+            return new PmdResult( new File( request.getTargetDirectory(), "pmd.xml" ), request.getOutputEncoding() );
+        }
+        catch ( IOException e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+        catch ( InterruptedException e )
+        {
+            Thread.currentThread().interrupt();
+            throw new MavenReportException( e.getMessage(), e );
+        }
+    }
+
+    /**
+     * Execute PMD analysis from CLI.
+     * 
+     * <p>
+     * Single arg with the filename to the serialized {@link PmdRequest}.
+     * 
+     * <p>
+     * Exit-code: 0 = success, 1 = failure in executing
+     * 
+     * @param args
+     */
+    public static void main( String[] args )
+    {
+        File requestFile = new File( args[0] );
+        try ( ObjectInputStream in = new ObjectInputStream( new FileInputStream( requestFile ) ) )
+        {
+            PmdRequest request = (PmdRequest) in.readObject();
+            PmdExecutor pmdExecutor = new PmdExecutor( request );
+            pmdExecutor.setupLogLevel( request.getLogLevel() );
+            pmdExecutor.run();
+            System.exit( 0 );
+        }
+        catch ( IOException | ClassNotFoundException | MavenReportException e )
+        {
+            LOG.error( e.getMessage(), e );
+        }
+        System.exit( 1 );
+    }
+
+    private final PmdRequest request;
+
+    public PmdExecutor( PmdRequest request )
+    {
+        this.request = Objects.requireNonNull( request );
+    }
+
+    private PmdResult run() throws MavenReportException
+    {
+        setupPmdLogging( request.isShowPmdLog(), request.isColorizedLog(), request.getLogLevel() );
+
+        PMDConfiguration configuration = new PMDConfiguration();
+        LanguageVersion languageVersion = null;
+        Language language = LanguageRegistry
+                .findLanguageByTerseName( request.getLanguage() != null ? request.getLanguage() : "java" );
+        if ( language == null )
+        {
+            throw new MavenReportException( "Unsupported language: " + request.getLanguage() );
+        }
+        if ( request.getLanguageVersion() != null )
+        {
+            languageVersion = language.getVersion( request.getLanguageVersion() );
+            if ( languageVersion == null )
+            {
+                throw new MavenReportException( "Unsupported targetJdk value '" + request.getLanguageVersion() + "'." );
+            }
+        }
+        else
+        {
+            languageVersion = language.getDefaultVersion();
+        }
+        LOG.debug( "Using language " + languageVersion );
+        configuration.setDefaultLanguageVersion( languageVersion );
+
+        try
+        {
+            configuration.prependClasspath( request.getAuxClasspath() );
+        }
+        catch ( IOException e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+        if ( request.getSuppressMarker() != null )
+        {
+            configuration.setSuppressMarker( request.getSuppressMarker() );
+        }
+        if ( request.getAnalysisCacheLocation() != null )
+        {
+            configuration.setAnalysisCacheLocation( request.getAnalysisCacheLocation() );
+            LOG.debug( "Using analysis cache location: " + request.getAnalysisCacheLocation() );
+        }
+        else
+        {
+            configuration.setIgnoreIncrementalAnalysis( true );
+        }
+
+        configuration.setRuleSets( request.getRulesets() );
+        if ( request.getBenchmarkOutputLocation() != null )
+        {
+            configuration.setBenchmark( true );
+        }
+        List<File> files = request.getFiles();
+        List<DataSource> dataSources = new ArrayList<>( files.size() );
+        for ( File f : files )
+        {
+            dataSources.add( new FileDataSource( f ) );
+        }
+
+        PmdCollectingRenderer renderer = new PmdCollectingRenderer();
+
+        if ( StringUtils.isBlank( request.getRulesets() ) )
+        {
+            LOG.debug( "Skipping PMD execution as no rulesets are defined." );
+        }
+        else
+        {
+            if ( request.getBenchmarkOutputLocation() != null )
+            {
+                TimeTracker.startGlobalTracking();
+            }
+
+            try
+            {
+                processFilesWithPMD( configuration, dataSources, renderer );
+            }
+            finally
+            {
+                if ( request.getAuxClasspath() != null )
+                {
+                    ClassLoader classLoader = configuration.getClassLoader();
+                    if ( classLoader instanceof Closeable )
+                    {
+                        IOUtils.closeQuietly( (Closeable) classLoader );
+                    }
+                }
+                if ( request.getBenchmarkOutputLocation() != null )
+                {
+                    TimingReport timingReport = TimeTracker.stopGlobalTracking();
+                    writeBenchmarkReport( timingReport, request.getBenchmarkOutputLocation(),
+                            request.getOutputEncoding() );
+                }
+            }
+        }
+
+        if ( renderer.hasErrors() )
+        {
+            if ( !request.isSkipPmdError() )
+            {
+                LOG.error( "PMD processing errors:" );
+                LOG.error( renderer.getErrorsAsString( request.isDebugEnabled() ) );
+                throw new MavenReportException( "Found " + renderer.getErrors().size() + " PMD processing errors" );
+            }
+            LOG.warn( "There are {} PMD processing errors:", renderer.getErrors().size() );
+            LOG.warn( renderer.getErrorsAsString( request.isDebugEnabled() ) );
+        }
+
+        removeExcludedViolations( renderer.getViolations() );
+
+        Report report = renderer.asReport();
+        // always write XML report, as this might be needed by the check mojo
+        // we need to output it even if the file list is empty or we have no violations
+        // so the "check" goals can check for violations
+        writeXmlReport( report );
+
+        // write any other format except for xml and html. xml has just been produced.
+        // html format is produced by the maven site formatter. Excluding html here
+        // avoids using PMD's own html formatter, which doesn't fit into the maven site
+        // considering the html/css styling
+        String format = request.getFormat();
+        if ( !"html".equals( format ) && !"xml".equals( format ) )
+        {
+            writeFormattedReport( report );
+        }
+
+        return new PmdResult( new File( request.getTargetDirectory(), "pmd.xml" ), request.getOutputEncoding() );
+    }
+
+    private void writeBenchmarkReport( TimingReport timingReport, String benchmarkOutputLocation, String encoding )
+    {
+        try ( Writer writer = new OutputStreamWriter( new FileOutputStream( benchmarkOutputLocation ), encoding ) )
+        {
+            final TimingReportRenderer renderer = new TextTimingReportRenderer();
+            renderer.render( timingReport, writer );
+        }
+        catch ( IOException e )
+        {
+            LOG.error( "Unable to generate benchmark file: {}", benchmarkOutputLocation, e );
+        }
+    }
+
+    private void processFilesWithPMD( PMDConfiguration pmdConfiguration, List<DataSource> dataSources,
+            PmdCollectingRenderer renderer ) throws MavenReportException
+    {
+        RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.createFactory(
+                RulePriority.valueOf( request.getMinimumPriority() ), true, true );
+        try
+        {
+            // load the ruleset once to log out any deprecated rules as warnings
+            ruleSetFactory.createRuleSets( pmdConfiguration.getRuleSets() );
+        }
+        catch ( RuleSetNotFoundException e1 )
+        {
+            throw new MavenReportException( "The ruleset could not be loaded", e1 );
+        }
+
+        try
+        {
+            LOG.debug( "Executing PMD..." );
+            RuleContext ruleContext = new RuleContext();
+            PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext,
+                    Arrays.<Renderer>asList( renderer ) );
+
+            LOG.debug( "PMD finished. Found {} violations.", renderer.getViolations().size() );
+        }
+        catch ( Exception e )
+        {
+            String message = "Failure executing PMD: " + e.getLocalizedMessage();
+            if ( !request.isSkipPmdError() )
+            {
+                throw new MavenReportException( message, e );
+            }
+            LOG.warn( message, e );
+        }
+    }
+
+    /**
+     * Use the PMD XML renderer to create the XML report format used by the
+     * check mojo later on.
+     *
+     * @param report
+     * @throws MavenReportException
+     */
+    private void writeXmlReport( Report report ) throws MavenReportException
+    {
+        File targetFile = writeReport( report, new XMLRenderer( request.getOutputEncoding() ), "xml" );
+        if ( request.isIncludeXmlInSite() )
+        {
+            File siteDir = new File( request.getReportOutputDirectory() );
+            siteDir.mkdirs();
+            try
+            {
+                FileUtils.copyFile( targetFile, new File( siteDir, "pmd.xml" ) );
+            }
+            catch ( IOException e )
+            {
+                throw new MavenReportException( e.getMessage(), e );
+            }
+        }
+    }
+
+    private File writeReport( Report report, Renderer r, String extension ) throws MavenReportException
+    {
+        if ( r == null )
+        {
+            return null;
+        }
+
+        File targetDir = new File( request.getTargetDirectory() );
+        targetDir.mkdirs();
+        File targetFile = new File( targetDir, "pmd." + extension );
+        try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ),
+                request.getOutputEncoding() ) )
+        {
+            r.setWriter( writer );
+            r.start();
+            r.renderFileReport( report );
+            r.end();
+            r.flush();
+        }
+        catch ( IOException ioe )
+        {
+            throw new MavenReportException( ioe.getMessage(), ioe );
+        }
+
+        return targetFile;
+    }
+
+    /**
+     * Use the PMD renderers to render in any format aside from HTML and XML.
+     *
+     * @param report
+     * @throws MavenReportException
+     */
+    private void writeFormattedReport( Report report )
+            throws MavenReportException
+    {
+        Renderer renderer = createRenderer( request.getFormat(), request.getOutputEncoding() );
+        writeReport( report, renderer, request.getFormat() );
+    }
+
+    /**
+     * Create and return the correct renderer for the output type.
+     *
+     * @return the renderer based on the configured output
+     * @throws org.apache.maven.reporting.MavenReportException
+     *             if no renderer found for the output type
+     */
+    public static Renderer createRenderer( String format, String outputEncoding ) throws MavenReportException
+    {
+        Renderer result = null;
+        if ( "xml".equals( format ) )
+        {
+            result = new XMLRenderer( outputEncoding );
+        }
+        else if ( "txt".equals( format ) )
+        {
+            result = new TextRenderer();
+        }
+        else if ( "csv".equals( format ) )
+        {
+            result = new CSVRenderer();
+        }
+        else if ( "html".equals( format ) )
+        {
+            result = new HTMLRenderer();
+        }
+        else if ( !"".equals( format ) && !"none".equals( format ) )
+        {
+            try
+            {
+                result = (Renderer) Class.forName( format ).getConstructor().newInstance();
+            }
+            catch ( Exception e )
+            {
+                throw new MavenReportException(
+                        "Can't find PMD custom format " + format + ": " + e.getClass().getName(), e );
+            }
+        }
+
+        return result;
+    }
+
+    private void removeExcludedViolations( List<RuleViolation> violations )
+            throws MavenReportException
+    {
+        ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
+
+        try
+        {
+            excludeFromFile.loadExcludeFromFailuresData( request.getExcludeFromFailureFile() );
+        }
+        catch ( MojoExecutionException e )
+        {
+            throw new MavenReportException( "Unable to load exclusions", e );
+        }
+
+        LOG.debug( "Removing excluded violations. Using {} configured exclusions.",
+                excludeFromFile.countExclusions() );
+        int violationsBefore = violations.size();
+
+        Iterator<RuleViolation> iterator = violations.iterator();
+        while ( iterator.hasNext() )
+        {
+            RuleViolation rv = iterator.next();
+            if ( excludeFromFile.isExcludedFromFailure( rv ) )
+            {
+                iterator.remove();
+            }
+        }
+
+        int numberOfExcludedViolations = violationsBefore - violations.size();
+        LOG.debug( "Excluded {} violations.", numberOfExcludedViolations );
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/PmdRequest.java b/src/main/java/org/apache/maven/plugins/pmd/exec/PmdRequest.java
new file mode 100644
index 0000000..276f495
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/PmdRequest.java
@@ -0,0 +1,300 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Data object to store all configuration options needed to execute PMD
+ * as a separate process.
+ * 
+ * <p>This class is intended to be serialized and read back.
+ * 
+ * <p>Some properties might be optional and can be <code>null</code>.
+ */
+public class PmdRequest implements Serializable
+{
+    private static final long serialVersionUID = -6324416880563476455L;
+
+    private String javaExecutable;
+
+    private String language;
+    private String languageVersion;
+    private int minimumPriority;
+    private String auxClasspath;
+    private String suppressMarker;
+    private String analysisCacheLocation;
+    private String rulesets;
+    private String sourceEncoding;
+    private List<File> files = new ArrayList<>();
+
+    private boolean showPmdLog;
+    private boolean colorizedLog;
+    private String logLevel;
+    private boolean skipPmdError;
+
+    private String excludeFromFailureFile;
+    private String targetDirectory;
+    private String outputEncoding;
+    private String format;
+    private String benchmarkOutputLocation;
+    private boolean includeXmlInSite;
+    private String reportOutputDirectory;
+
+    /**
+     * Configure language and language version.
+     *
+     * @param language the language
+     * @param targetJdk the language version, optional, can be <code>null</code>
+     */
+    public void setLanguageAndVersion( String language, String targetJdk )
+    {
+        if ( "java".equals( language ) || null == language )
+        {
+            this.language = "java";
+            this.languageVersion = targetJdk;
+        }
+        else if ( "javascript".equals( language ) || "ecmascript".equals( language ) )
+        {
+            this.language = "ecmascript";
+        }
+        else if ( "jsp".equals( language ) )
+        {
+            this.language = "jsp";
+        }
+        else
+        {
+            this.language = language;
+        }
+    }
+
+    public void setJavaExecutable( String javaExecutable )
+    {
+        this.javaExecutable = javaExecutable;
+    }
+
+    public void setMinimumPriority( int minimumPriority )
+    {
+        this.minimumPriority = minimumPriority;
+    }
+
+    public void setAuxClasspath( String auxClasspath )
+    {
+        this.auxClasspath = auxClasspath;
+    }
+
+    public void setSuppressMarker( String suppressMarker )
+    {
+        this.suppressMarker = suppressMarker;
+    }
+
+    public void setAnalysisCacheLocation( String analysisCacheLocation )
+    {
+        this.analysisCacheLocation = analysisCacheLocation;
+    }
+
+    public void setRulesets( String rulesets )
+    {
+        this.rulesets = rulesets;
+    }
+
+    public void setSourceEncoding( String sourceEncoding )
+    {
+        this.sourceEncoding = sourceEncoding;
+    }
+
+    public void addFiles( Collection<File> files )
+    {
+        this.files.addAll( files );
+    }
+
+    public void setBenchmarkOutputLocation( String benchmarkOutputLocation )
+    {
+        this.benchmarkOutputLocation = benchmarkOutputLocation;
+    }
+
+    public void setTargetDirectory( String targetDirectory )
+    {
+        this.targetDirectory = targetDirectory;
+    }
+
+    public void setOutputEncoding( String outputEncoding )
+    {
+        this.outputEncoding = outputEncoding;
+    }
+
+    public void setFormat( String format )
+    {
+        this.format = format;
+    }
+
+    public void setShowPmdLog( boolean showPmdLog )
+    {
+        this.showPmdLog = showPmdLog;
+    }
+
+    public void setColorizedLog( boolean colorizedLog )
+    {
+        this.colorizedLog = colorizedLog;
+    }
+
+    public void setLogLevel( String logLevel )
+    {
+        this.logLevel = logLevel;
+    }
+
+    public void setSkipPmdError( boolean skipPmdError )
+    {
+        this.skipPmdError = skipPmdError;
+    }
+
+    public void setIncludeXmlInSite( boolean includeXmlInSite )
+    {
+        this.includeXmlInSite = includeXmlInSite;
+    }
+
+    public void setReportOutputDirectory( String reportOutputDirectory )
+    {
+        this.reportOutputDirectory = reportOutputDirectory;
+    }
+
+    public void setExcludeFromFailureFile( String excludeFromFailureFile )
+    {
+        this.excludeFromFailureFile = excludeFromFailureFile;
+    }
+
+
+
+
+
+    public String getJavaExecutable()
+    {
+        return javaExecutable;
+    }
+
+    public String getLanguage()
+    {
+        return language;
+    }
+
+    public String getLanguageVersion()
+    {
+        return languageVersion;
+    }
+
+    public int getMinimumPriority()
+    {
+        return minimumPriority;
+    }
+
+    public String getAuxClasspath()
+    {
+        return auxClasspath;
+    }
+
+    public String getSuppressMarker()
+    {
+        return suppressMarker;
+    }
+
+    public String getAnalysisCacheLocation()
+    {
+        return analysisCacheLocation;
+    }
+
+    public String getRulesets()
+    {
+        return rulesets;
+    }
+
+    public String getSourceEncoding()
+    {
+        return sourceEncoding;
+    }
+
+    public List<File> getFiles()
+    {
+        return files;
+    }
+
+    public String getBenchmarkOutputLocation()
+    {
+        return benchmarkOutputLocation;
+    }
+
+    public String getTargetDirectory()
+    {
+        return targetDirectory;
+    }
+
+    public String getOutputEncoding()
+    {
+        return outputEncoding;
+    }
+
+    public String getFormat()
+    {
+        return format;
+    }
+
+    public boolean isShowPmdLog()
+    {
+        return showPmdLog;
+    }
+
+    public boolean isColorizedLog()
+    {
+        return colorizedLog;
+    }
+
+    public String getLogLevel()
+    {
+        return logLevel;
+    }
+
+    public boolean isDebugEnabled()
+    {
+        return "debug".equals( logLevel );
+    }
+
+    public boolean isSkipPmdError()
+    {
+        return skipPmdError;
+    }
+
+    public boolean isIncludeXmlInSite()
+    {
+        return includeXmlInSite;
+    }
+
+    public String getReportOutputDirectory()
+    {
+        return reportOutputDirectory;
+    }
+
+    public String getExcludeFromFailureFile()
+    {
+        return excludeFromFailureFile;
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/pmd/exec/PmdResult.java b/src/main/java/org/apache/maven/plugins/pmd/exec/PmdResult.java
new file mode 100644
index 0000000..93645d7
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/pmd/exec/PmdResult.java
@@ -0,0 +1,154 @@
+package org.apache.maven.plugins.pmd.exec;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilterReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.maven.plugins.pmd.model.PmdErrorDetail;
+import org.apache.maven.plugins.pmd.model.PmdFile;
+import org.apache.maven.plugins.pmd.model.ProcessingError;
+import org.apache.maven.plugins.pmd.model.Violation;
+import org.apache.maven.plugins.pmd.model.io.xpp3.PmdXpp3Reader;
+import org.apache.maven.reporting.MavenReportException;
+
+/**
+ * Provides access to the result of the pmd analysis.
+ */
+public class PmdResult
+{
+    private final List<ProcessingError> processingErrors = new ArrayList<>();
+    private final List<Violation> violations = new ArrayList<>();
+
+    public static final PmdResult EMPTY = new PmdResult();
+
+    private PmdResult()
+    {
+    }
+
+    public PmdResult( File pmdFile, String encoding ) throws MavenReportException
+    {
+        loadResult( pmdFile, encoding );
+    }
+
+    public boolean hasViolations()
+    {
+        return !violations.isEmpty();
+    }
+
+    private void loadResult( File pmdFile, String encoding ) throws MavenReportException
+    {
+        try ( Reader reader1 = new BomFilter( encoding, new InputStreamReader(
+                new FileInputStream( pmdFile ), encoding ) ) )
+        {
+            PmdXpp3Reader reader = new PmdXpp3Reader();
+            PmdErrorDetail details = reader.read( reader1, false );
+            processingErrors.addAll( details.getErrors() );
+
+            for ( PmdFile file : details.getFiles() )
+            {
+                String filename = file.getName();
+                for ( Violation violation : file.getViolations() )
+                {
+                    violation.setFileName( filename );
+                    violations.add( violation );
+                }
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new MavenReportException( e.getMessage(), e );
+        }
+    }
+
+    // Note: This seems to be a bug in PMD's XMLRenderer. The BOM is rendered multiple times.
+    // once at the beginning of the file, which is Ok, but also in the middle of the file.
+    // This filter just skips all BOMs if the encoding is not UTF-8
+    private static class BomFilter extends FilterReader
+    {
+        private static final char BOM = '\uFEFF';
+        private final boolean filter;
+
+        BomFilter( String encoding, Reader in )
+        {
+            super( in );
+            filter = !"UTF-8".equalsIgnoreCase( encoding );
+        }
+
+        @Override
+        public int read() throws IOException
+        {
+            int c = super.read();
+
+            if ( !filter )
+            {
+                return c;
+            }
+
+            while ( c != -1 && c == BOM )
+            {
+                c = super.read();
+            }
+            return c;
+        }
+
+        @Override
+        public int read( char[] cbuf, int off, int len ) throws IOException
+        {
+            int count = super.read( cbuf, off, len );
+
+            if ( !filter )
+            {
+                return count;
+            }
+
+            if ( count != -1 )
+            {
+                for ( int i = off; i < off + count; i++ )
+                {
+                    if ( cbuf[i] == BOM )
+                    {
+                        // shift the content one char to the left
+                        System.arraycopy( cbuf, i + 1, cbuf, i, off + count - 1 - i );
+                        count--;
+                    }
+                }
+            }
+            return count;
+        }
+    }
+
+    public Collection<Violation> getViolations()
+    {
+        return violations;
+    }
+
+    public Collection<ProcessingError> getErrors()
+    {
+        return processingErrors;
+    }
+}
diff --git a/src/main/mdo/pmd.mdo b/src/main/mdo/pmd.mdo
index 52bb58d..2c2a084 100644
--- a/src/main/mdo/pmd.mdo
+++ b/src/main/mdo/pmd.mdo
@@ -45,15 +45,13 @@
             <multiplicity>*</multiplicity>
           </association>
         </field>
-        <!-- 
         <field>
           <name>errors</name>
           <association xml.tagName="error" xml.itemsStyle="flat">
-            <type>PmdError</type>
+            <type>ProcessingError</type>
             <multiplicity>*</multiplicity>
           </association>
         </field>
-         -->
       </fields>
     </class>
     <class>
@@ -79,6 +77,10 @@
           <name>beginline</name>
           <type>int</type>
         </field>
+        <field xml.attribute="true">
+            <name>endline</name>
+            <type>int</type>
+        </field>
         <field xml.tagName="class" xml.attribute="true">
           <name>violationClass</name>
           <type>String</type>
@@ -95,6 +97,10 @@
           <name>priority</name>
           <type>int</type>
         </field>
+        <field xml.attribute="true">
+            <name>externalInfoUrl</name>
+            <type>String</type>
+        </field>
         <field xml.content="true">
           <name>text</name>
           <type>String</type>
@@ -120,8 +126,22 @@
         </codeSegment>
       </codeSegments>
     </class>
-  </classes>   
-    
- 
-    
+    <class>
+        <name>ProcessingError</name>
+        <fields>
+            <field xml.attribute="true">
+                <name>filename</name>
+                <type>String</type>
+            </field>
+            <field xml.attribute="true">
+                <name>msg</name>
+                <type>String</type>
+            </field>
+            <field xml.content="true">
+                <name>detail</name>
+                <type>String</type>
+            </field>
+        </fields>
+    </class>
+  </classes>
 </model>
\ No newline at end of file
diff --git a/src/site/apt/examples/cpdCsharp.apt.vm b/src/site/apt/examples/cpdCsharp.apt.vm
new file mode 100644
index 0000000..4fbf230
--- /dev/null
+++ b/src/site/apt/examples/cpdCsharp.apt.vm
@@ -0,0 +1,91 @@
+ ------
+ Finding duplicated code in C#
+ ------
+ Andreas Dangel
+ ------
+ 2020-10-02
+ ------
+
+ ~~ 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.
+
+~~ NOTE: For help with the syntax of this file, see:
+~~ http://maven.apache.org/doxia/references/apt-format.html
+
+Finding duplicated code in C#
+
+ By default, the maven-pmd-plugin only supports the languages Java, JavaScript and JSP.
+ But {{{https://pmd.github.io/latest/pmd_userdocs_cpd.html#supported-languages}CPD supports many more languages}},
+ e.g. C#. In order to enable C# in your build, you need to
+ configure several parts:
+ 
+ * Add an additional plugin dependency for c# (pmd-cs module)
+
+ * Select the language <<<cs>>>.
+
+ * Configure the includes filter to consider <<<*.cs>>> (otherwise only java files will be analyzed)
+
+ * Configure the source directory (by default, only <<<src/main/java>>> is analyzed)
+
+
++-----+
+<project>
+  ...
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>${project.version}</version>
+        <configuration>
+          <language>cs</language>
+          <minimumTokens>10</minimumTokens>
+          <includes>
+            <include>**/*.cs</include>
+          </includes>
+          <compileSourceRoots>
+            <compileSourceRoot>${basedir}/src/main/cs</compileSourceRoot>
+          </compileSourceRoots>
+          <printFailingErrors>true</printFailingErrors>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>cpd-check</goal>
+            </goals>
+          </execution>
+        </executions>
+        <dependencies>
+            <dependency>
+                <groupId>net.sourceforge.pmd</groupId>
+                <artifactId>pmd-cs</artifactId>
+                <version>${pmdVersion}</version>
+            </dependency>
+        </dependencies>
+      </plugin>
+      ...
+    </plugins>
+  </build>
+</project>
++-----+
+
+ In this example, the C# source files are located in <<<src/main/cs>>>.
+
+ <<Note:>> The version for <<<net.sourceforge.pmd:pmd-cs>>> needs to match the PMD version, that is
+ being used. If you {{{./upgrading-PMD-at-runtime.html}upgrade PMD at runtime}} you'll need to make
+ sure, to change the version here as well.
+
diff --git a/src/site/apt/examples/targetJdk.apt.vm b/src/site/apt/examples/targetJdk.apt.vm
index 6e5f367..bd4f20f 100644
--- a/src/site/apt/examples/targetJdk.apt.vm
+++ b/src/site/apt/examples/targetJdk.apt.vm
@@ -1,9 +1,10 @@
  ------
- Target JDK
+ Target JDK and Toolchains
  ------
  Dennis Lundberg
+ Andreas Dangel
  ------
- 2008-01-02
+ 2020-10-02
  ------
 
  ~~ Licensed to the Apache Software Foundation (ASF) under one
@@ -52,6 +53,17 @@
 </project>
 +--------------------+
 
+Using Maven Toolchains
 
+ Since version 3.14.0 of the PMD plugin, toolchains are supported. This helps if the build system is
+ running a different JDK than being used for compiling. PMD reads the class files for
+ {{{../pmd-mojo.html#typeResolution}type resolution}} and this fails with ClassFormatErrors
+ if the JDK version is incorrect.
 
+ To set this up, refer to the {{{/guides/mini/guide-using-toolchains.html}Guide to Using Toolchains}}, which makes use
+ of the {{{/plugins/maven-toolchains-plugin/}Maven Toolchains Plugin}}.
 
+ With the maven-toolchains-plugin you configure 1 default JDK toolchain for all related maven-plugins.
+ Since maven-pmd-plugin 3.14.0 when using with Maven 3.3.1+ it is also possible to give the plugin its own
+ toolchain, which can be useful in case of different JDK calls per execution block (e.g. the test sources require a
+ different JDK compared to the main sources).
diff --git a/src/site/apt/examples/upgrading-PMD-at-runtime.apt.vm b/src/site/apt/examples/upgrading-PMD-at-runtime.apt.vm
index 74266c1..7c343b2 100644
--- a/src/site/apt/examples/upgrading-PMD-at-runtime.apt.vm
+++ b/src/site/apt/examples/upgrading-PMD-at-runtime.apt.vm
@@ -88,7 +88,7 @@
 *--------------------------------------------------------------------------------*--------------------------------------------------*
 | <<maven-pmd-plugin>>                                                           | <<PMD>>                                          |
 *--------------------------------------------------------------------------------*--------------------------------------------------*
-| {{{https://maven.apache.org/plugins-archives/maven-pmd-plugin-3.14.0/}3.14.0}} | {{{https://pmd.github.io/pmd-6.26.0/}6.26.0}}    |
+| {{{https://maven.apache.org/plugins-archives/maven-pmd-plugin-3.14.0/}3.14.0}} | {{{https://pmd.github.io/pmd-6.29.0/}6.29.0}}    |
 *--------------------------------------------------------------------------------*--------------------------------------------------*
 | {{{https://maven.apache.org/plugins-archives/maven-pmd-plugin-3.13.0/}3.13.0}} | {{{https://pmd.github.io/pmd-6.21.0/}6.21.0}}    |
 *--------------------------------------------------------------------------------*--------------------------------------------------*
diff --git a/src/site/apt/index.apt.vm b/src/site/apt/index.apt.vm
index 38161a7..348c892 100644
--- a/src/site/apt/index.apt.vm
+++ b/src/site/apt/index.apt.vm
@@ -88,7 +88,7 @@
 
   * {{{./examples/removeReport.html}Remove Report}}
 
-  * {{{./examples/targetJdk.html}Target JDK}}
+  * {{{./examples/targetJdk.html}Target JDK and Toolchains}}
 
   * {{{./examples/usingRuleSets.html}Using Rule Sets}}
 
@@ -98,4 +98,6 @@
 
   * {{{./examples/jspReport.html}Analyzing Java Server Pages Code}}
 
+  * {{{./examples/cpdCsharp.html}Finding duplicated code in C#}}
+
   []
diff --git a/src/site/site.xml b/src/site/site.xml
index ca64ccb..c7c6561 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -36,12 +36,13 @@
       <item name="Upgrading PMD at Runtime" href="examples/upgrading-PMD-at-runtime.html"/>
       <item name="Multimodule Configuration" href="examples/multi-module-config.html"/>
       <item name="Remove Report" href="examples/removeReport.html"/>
-      <item name="Target JDK" href="examples/targetJdk.html"/>
+      <item name="Target JDK and Toolchains" href="examples/targetJdk.html"/>
       <item name="Using Rule Sets" href="examples/usingRuleSets.html"/>
       <item name="Violation Checking" href="examples/violationChecking.html"/>
       <item name="Analyzing JavaScript" href="examples/javascriptReport.html"/>
       <item name="Analyzing Java Server Pages" href="examples/jspReport.html"/>
       <item name="Violations Exclusions" href="examples/violation-exclusions.html"/>
+      <item name="Duplicated code in C#" href="examples/cpdCsharp.html"/>
     </menu>
   </body>
 </project>
diff --git a/src/test/java/org/apache/maven/plugins/pmd/AbstractPmdReportTest.java b/src/test/java/org/apache/maven/plugins/pmd/AbstractPmdReportTest.java
index 3c9af8f..64f961e 100644
--- a/src/test/java/org/apache/maven/plugins/pmd/AbstractPmdReportTest.java
+++ b/src/test/java/org/apache/maven/plugins/pmd/AbstractPmdReportTest.java
@@ -25,9 +25,9 @@
 import java.util.Locale;
 
 import org.apache.maven.doxia.site.decoration.DecorationModel;
+import org.apache.maven.doxia.siterenderer.DocumentContent;
 import org.apache.maven.doxia.siterenderer.RendererException;
 import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
-import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
 import org.apache.maven.plugin.testing.AbstractMojoTestCase;
 import org.codehaus.plexus.util.WriterFactory;
 
@@ -39,6 +39,14 @@
 public abstract class AbstractPmdReportTest
     extends AbstractMojoTestCase
 {
+    @Override
+    protected void setUp()
+        throws Exception
+    {
+        super.setUp();
+        CapturingPrintStream.init( true );
+    }
+
     /**
      * Renderer the sink from the report mojo.
      *
@@ -59,7 +67,7 @@
 
         try ( Writer writer = WriterFactory.newXmlWriter( outputHtml ) )
         {
-            mojo.getSiteRenderer().generateDocument( writer, (SiteRendererSink) mojo.getSink(), context );
+            mojo.getSiteRenderer().mergeDocumentIntoSite( writer, (DocumentContent) mojo.getSink(), context );
         }
     }
 
diff --git a/src/test/java/org/apache/maven/plugins/pmd/CapturingPrintStream.java b/src/test/java/org/apache/maven/plugins/pmd/CapturingPrintStream.java
new file mode 100644
index 0000000..09adfd5
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/pmd/CapturingPrintStream.java
@@ -0,0 +1,82 @@
+package org.apache.maven.plugins.pmd;
+
+/*
+ * 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.
+ */
+
+import java.io.PrintStream;
+
+import org.slf4j.impl.MavenSlf4jSimpleFriend;
+
+/**
+ * Captures log output from simple slf4j for asserting in unit tests.
+ */
+class CapturingPrintStream extends PrintStream {
+    private final boolean quiet;
+    private StringBuilder buffer = new StringBuilder();
+
+    private CapturingPrintStream( boolean quiet ) {
+        super( System.out, true );
+        this.quiet = quiet;
+    }
+
+    @Override
+    public void println( String x )
+    {
+        if ( !quiet )
+        {
+            super.println( x );
+        }
+        buffer.append( x ).append( System.lineSeparator() );
+    }
+
+    public static void init( boolean quiet )
+    {
+        CapturingPrintStream capture = get();
+        if ( capture != null )
+        {
+            capture.buffer.setLength( 0 );
+        }
+        else
+        {
+            capture = new CapturingPrintStream( quiet );
+            System.setOut( capture );
+            MavenSlf4jSimpleFriend.init();
+        }
+    }
+
+    public static CapturingPrintStream get()
+    {
+        if ( System.out instanceof CapturingPrintStream )
+        {
+            return (CapturingPrintStream) System.out;
+        }
+        return null;
+    }
+
+    public static String getOutput()
+    {
+        CapturingPrintStream stream = get();
+        if ( stream != null )
+        {
+            stream.flush();
+            return stream.buffer.toString();
+        }
+        return "";
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/maven/plugins/pmd/CpdReportTest.java b/src/test/java/org/apache/maven/plugins/pmd/CpdReportTest.java
index e64d532..c016283 100644
--- a/src/test/java/org/apache/maven/plugins/pmd/CpdReportTest.java
+++ b/src/test/java/org/apache/maven/plugins/pmd/CpdReportTest.java
@@ -23,23 +23,11 @@
 import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Locale;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 
-import net.sourceforge.pmd.PMD;
-import net.sourceforge.pmd.cpd.CPD;
-import net.sourceforge.pmd.cpd.CPDConfiguration;
-import net.sourceforge.pmd.cpd.JavaLanguage;
-import net.sourceforge.pmd.cpd.Mark;
-import net.sourceforge.pmd.cpd.Match;
-import net.sourceforge.pmd.cpd.SourceCode;
-import net.sourceforge.pmd.cpd.TokenEntry;
-
 import org.apache.commons.lang3.StringUtils;
 import org.codehaus.plexus.util.FileUtils;
 import org.w3c.dom.Document;
@@ -88,18 +76,40 @@
         // check the contents of cpd.html
         String str = readFile( new File( getBasedir(), "target/test/unit/default-configuration/target/site/cpd.html" ) );
         assertTrue( lowerCaseContains( str, "AppSample.java" ) );
-
-        str = readFile( new File( getBasedir(), "target/test/unit/default-configuration/target/site/cpd.html" ) );
         assertTrue( lowerCaseContains( str, "App.java" ) );
-
-        str = readFile( new File( getBasedir(), "target/test/unit/default-configuration/target/site/cpd.html" ) );
         assertTrue( lowerCaseContains( str, "public String dup( String str )" ) );
-
-        str = readFile( new File( getBasedir(), "target/test/unit/default-configuration/target/site/cpd.html" ) );
         assertTrue( lowerCaseContains( str, "tmp = tmp + str.substring( i, i + 1);" ) );
     }
 
     /**
+     * Test CPDReport with the text renderer given as "format=txt"
+     *
+     * @throws Exception
+     */
+    public void testTxtFormat()
+        throws Exception
+    {
+        File testPom =
+            new File( getBasedir(),
+                      "src/test/resources/unit/custom-configuration/cpd-txt-format-configuration-plugin-config.xml" );
+        CpdReport mojo = (CpdReport) lookupMojo( "cpd", testPom );
+        mojo.execute();
+
+        // check if the CPD files were generated
+        File generatedFile = new File( getBasedir(), "target/test/unit/custom-configuration/target/cpd.xml" );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+        generatedFile = new File( getBasedir(), "target/test/unit/custom-configuration/target/cpd.txt" );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+
+        // check the contents of cpd.txt
+        String str = readFile( generatedFile );
+        // Contents that should NOT be in the report
+        assertFalse( lowerCaseContains( str, "public static void main( String[] args )" ) );
+        // Contents that should be in the report
+        assertTrue( lowerCaseContains( str, "public void duplicateMethod( int i )" ) );
+    }
+
+    /**
      * Test CPDReport using custom configuration
      *
      * @throws Exception
@@ -188,35 +198,6 @@
         return str.toString();
     }
 
-    private CPD prepareMockCpd( String duplicatedCodeFragment )
-    {
-        TokenEntry tFirstEntry = new TokenEntry( "public java", "MyClass.java", 2 );
-        TokenEntry tSecondEntry = new TokenEntry( "public java", "MyClass3.java", 2 );
-        SourceCode sourceCodeFirst = new SourceCode(new SourceCode.StringCodeLoader(
-                PMD.EOL + duplicatedCodeFragment + PMD.EOL, "MyClass.java"));
-        SourceCode sourceCodeSecond = new SourceCode(new SourceCode.StringCodeLoader(
-                PMD.EOL + duplicatedCodeFragment + PMD.EOL, "MyClass3.java"));
-
-        List<Match> tList = new ArrayList<>();
-        Mark tFirstMark = new Mark( tFirstEntry );
-        tFirstMark.setSourceCode(sourceCodeFirst);
-        tFirstMark.setLineCount(1);
-        Mark tSecondMark = new Mark( tSecondEntry );
-        tSecondMark.setSourceCode(sourceCodeSecond);
-        tSecondMark.setLineCount(1);
-        Match tMatch = new Match( 2, tFirstMark, tSecondMark );
-        tList.add( tMatch );
-
-        CPDConfiguration cpdConfiguration = new CPDConfiguration();
-        cpdConfiguration.setMinimumTileSize( 100 );
-        cpdConfiguration.setLanguage( new JavaLanguage() );
-        cpdConfiguration.setEncoding( "UTF-8" );
-        CPD tCpd = new MockCpd( cpdConfiguration, tList.iterator() );
-
-        tCpd.go();
-        return tCpd;
-    }
-
     public void testWriteNonHtml()
         throws Exception
     {
@@ -225,10 +206,7 @@
                       "src/test/resources/unit/default-configuration/cpd-default-configuration-plugin-config.xml" );
         CpdReport mojo = (CpdReport) lookupMojo( "cpd", testPom );
         assertNotNull( mojo );
-
-        String duplicatedCodeFragment = "// ----- duplicated code example -----";
-        CPD tCpd = prepareMockCpd( duplicatedCodeFragment );
-        mojo.writeXmlReport( tCpd );
+        mojo.execute();
 
         File tReport = new File( getBasedir(), "target/test/unit/default-configuration/target/cpd.xml" );
 
@@ -237,9 +215,10 @@
         assertNotNull( pmdCpdDocument );
 
         String str = readFile( tReport );
-        assertTrue( lowerCaseContains( str, "MyClass.java" ) );
-        assertTrue( lowerCaseContains( str, "MyClass3.java" ) );
-        assertTrue( lowerCaseContains( str, duplicatedCodeFragment ) );
+        assertTrue( lowerCaseContains( str, "AppSample.java" ) );
+        assertTrue( lowerCaseContains( str, "App.java" ) );
+        assertTrue( lowerCaseContains( str, "public String dup( String str )" ) );
+        assertTrue( lowerCaseContains( str, "tmp = tmp + str.substring( i, i + 1);" ) );
     }
 
     /**
@@ -254,10 +233,7 @@
                           "src/test/resources/unit/default-configuration/cpd-report-include-xml-in-site-plugin-config.xml" );
         CpdReport mojo = (CpdReport) lookupMojo( "cpd", testPom );
         assertNotNull( mojo );
-
-        String duplicatedCodeFragment = "// ----- duplicated code example -----";
-        CPD tCpd = prepareMockCpd( duplicatedCodeFragment );
-        mojo.writeXmlReport( tCpd );
+        mojo.execute();
 
         File tReport = new File( getBasedir(), "target/test/unit/default-configuration/target/cpd.xml" );
         assertTrue( FileUtils.fileExists( tReport.getAbsolutePath() ) );
@@ -273,6 +249,7 @@
         assertTrue( FileUtils.fileExists( siteReport.getAbsolutePath() ) );
         String siteReportContent = readFile( siteReport );
         assertTrue( siteReportContent.contains( "</pmd-cpd>" ) );
+        assertEquals( str, siteReportContent );
     }
 
 
@@ -378,25 +355,4 @@
         String str = readFile( generatedFile );
         assertEquals( 0, StringUtils.countMatches( str, "<duplication" ) );
     }
-
-    public static class MockCpd
-        extends CPD
-    {
-
-        private Iterator<Match> matches;
-
-        public MockCpd( CPDConfiguration configuration, Iterator<Match> tMatch )
-        {
-            super( configuration );
-            matches = tMatch;
-        }
-
-        @Override
-        public Iterator<Match> getMatches()
-        {
-            return matches;
-        }
-
-    }
-
 }
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 3bb5b04..8a4fc0b 100644
--- a/src/test/java/org/apache/maven/plugins/pmd/PmdReportTest.java
+++ b/src/test/java/org/apache/maven/plugins/pmd/PmdReportTest.java
@@ -20,11 +20,9 @@
  */
 
 import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.net.ServerSocket;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
@@ -32,6 +30,7 @@
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.maven.plugins.pmd.exec.PmdExecutor;
 import org.apache.maven.reporting.MavenReportException;
 import org.codehaus.plexus.util.FileUtils;
 
@@ -47,6 +46,7 @@
 public class PmdReportTest
     extends AbstractPmdReportTest
 {
+
     /**
      * {@inheritDoc}
      */
@@ -578,33 +578,23 @@
                 "src/test/resources/unit/processing-error/pmd-processing-error-skip-plugin-config.xml" );
         PmdReport mojo = (PmdReport) lookupMojo( "pmd", testPom );
 
-        PrintStream originalOut = System.out;
-        ByteArrayOutputStream logging = new ByteArrayOutputStream();
-        System.setOut( new PrintStream( logging ) );
+        mojo.execute();
+        String output = CapturingPrintStream.getOutput();
+        assertTrue ( output.contains( "There are 1 PMD processing errors:" ) );
 
-        try {
-            mojo.execute();
-            String output = logging.toString();
-            assertTrue ( output.contains( "There are 1 PMD processing errors:" ) );
+        File generatedFile = new File( getBasedir(), "target/test/unit/parse-error/target/pmd.xml" );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+        String str = readFile( generatedFile );
+        assertTrue( str.contains( "Error while parsing" ) );
+        // The parse exception must be in the XML report
+        assertTrue( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
 
-            File generatedFile = new File( getBasedir(), "target/test/unit/parse-error/target/pmd.xml" );
-            assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
-            String str = readFile( generatedFile );
-            assertTrue( str.contains( "Error while parsing" ) );
-            // The parse exception must be in the XML report
-            assertTrue( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
-
-            generatedFile = new File( getBasedir(), "target/test/unit/default-configuration/target/site/pmd.html" );
-            renderer( mojo, generatedFile );
-            assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
-            str = readFile( generatedFile );
-            // The parse exception must also be in the HTML report
-            assertTrue( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
-
-        } finally {
-            System.setOut( originalOut );
-            System.out.println( logging.toString() );
-        }
+        generatedFile = new File( getBasedir(), "target/test/unit/default-configuration/target/site/pmd.html" );
+        renderer( mojo, generatedFile );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+        str = readFile( generatedFile );
+        // The parse exception must also be in the HTML report
+        assertTrue( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
     }
 
     public void testPMDProcessingErrorWithDetailsNoReport()
@@ -614,33 +604,23 @@
                 "src/test/resources/unit/processing-error/pmd-processing-error-no-report-plugin-config.xml" );
         PmdReport mojo = (PmdReport) lookupMojo( "pmd", testPom );
 
-        PrintStream originalOut = System.out;
-        ByteArrayOutputStream logging = new ByteArrayOutputStream();
-        System.setOut( new PrintStream( logging ) );
+        mojo.execute();
+        String output = CapturingPrintStream.getOutput();
+        assertTrue ( output.contains( "There are 1 PMD processing errors:" ) );
 
-        try {
-            mojo.execute();
-            String output = logging.toString();
-            assertTrue ( output.contains( "There are 1 PMD processing errors:" ) );
+        File generatedFile = new File( getBasedir(), "target/test/unit/parse-error/target/pmd.xml" );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+        String str = readFile( generatedFile );
+        assertTrue( str.contains( "Error while parsing" ) );
+        // The parse exception must be in the XML report
+        assertTrue( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
 
-            File generatedFile = new File( getBasedir(), "target/test/unit/parse-error/target/pmd.xml" );
-            assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
-            String str = readFile( generatedFile );
-            assertTrue( str.contains( "Error while parsing" ) );
-            // The parse exception must be in the XML report
-            assertTrue( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
-
-            generatedFile = new File( getBasedir(), "target/test/unit/default-configuration/target/site/pmd.html" );
-            renderer( mojo, generatedFile );
-            assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
-            str = readFile( generatedFile );
-            // The parse exception must NOT be in the HTML report, since reportProcessingErrors is false
-            assertFalse( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
-
-        } finally {
-            System.setOut( originalOut );
-            System.out.println( logging.toString() );
-        }
+        generatedFile = new File( getBasedir(), "target/test/unit/default-configuration/target/site/pmd.html" );
+        renderer( mojo, generatedFile );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+        str = readFile( generatedFile );
+        // The parse exception must NOT be in the HTML report, since reportProcessingErrors is false
+        assertFalse( str.contains( "ParseException: Encountered \"\" at line 23, column 5." ) );
     }
 
     public void testPMDExcludeRootsShouldExcludeSubdirectories() throws Exception {
@@ -675,17 +655,39 @@
 
     public void testCustomRenderer() throws MavenReportException
     {
-        PmdReport pmdReport = new PmdReport();
-        pmdReport.format = "net.sourceforge.pmd.renderers.TextRenderer";
-        final Renderer renderer = pmdReport.createRenderer();
+        final Renderer renderer = PmdExecutor.createRenderer( "net.sourceforge.pmd.renderers.TextRenderer", "UTF-8" );
         assertNotNull(renderer);
     }
 
     public void testCodeClimateRenderer() throws MavenReportException
     {
-        PmdReport pmdReport = new PmdReport();
-        pmdReport.format = "net.sourceforge.pmd.renderers.CodeClimateRenderer";
-        final Renderer renderer = pmdReport.createRenderer();
+        final Renderer renderer = PmdExecutor.createRenderer( "net.sourceforge.pmd.renderers.CodeClimateRenderer", "UTF-8" );
         assertNotNull(renderer);
     }
+
+    public void testPmdReportCustomRulesNoExternalInfoUrl()
+            throws Exception
+    {
+        FileUtils.copyDirectoryStructure( new File( getBasedir(),
+                                                    "src/test/resources/unit/default-configuration/jxr-files" ),
+                                          new File( getBasedir(), "target/test/unit/default-configuration/target/site" ) );
+
+        File testPom =
+            new File( getBasedir(),
+                      "src/test/resources/unit/default-configuration/pmd-report-custom-rules.xml" );
+        PmdReport mojo = (PmdReport) lookupMojo( "pmd", testPom );
+        mojo.execute();
+
+        File generatedFile = new File( getBasedir(), "target/test/unit/default-configuration/target/site/pmd.html" );
+        renderer( mojo, generatedFile );
+        assertTrue( FileUtils.fileExists( generatedFile.getAbsolutePath() ) );
+
+        String str = readFile( generatedFile );
+
+        // custom rule without link
+        assertEquals( 2, StringUtils.countMatches( str, "<td>CustomRule</td>" ) );
+        // standard rule with link
+        assertEquals( 4, StringUtils.countMatches( str, "\">UnusedPrivateField</a></td>" ) );
+    }
+
 }
diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties
new file mode 100644
index 0000000..64b331b
--- /dev/null
+++ b/src/test/resources/simplelogger.properties
@@ -0,0 +1,32 @@
+# 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.
+
+org.slf4j.simpleLogger.defaultLogLevel=info
+org.slf4j.simpleLogger.showDateTime=false
+org.slf4j.simpleLogger.showThreadName=false
+org.slf4j.simpleLogger.showLogName=false
+org.slf4j.simpleLogger.logFile=System.out
+org.slf4j.simpleLogger.cacheOutputStream=true
+org.slf4j.simpleLogger.levelInBrackets=true
+org.slf4j.simpleLogger.log.Sisu=info
+org.slf4j.simpleLogger.warnLevelString=WARNING
+
+# MNG-6181: mvn -X also prints all debug logging from HttpClient
+# Be aware that the shaded packages are used
+# org.apache.http -> org.apache.maven.wagon.providers.http.httpclient
+org.slf4j.simpleLogger.log.org.apache.maven.wagon.providers.http.httpclient=off
+org.slf4j.simpleLogger.log.org.apache.maven.wagon.providers.http.httpclient.wire=off
diff --git a/src/test/resources/unit/custom-configuration/cpd-txt-format-configuration-plugin-config.xml b/src/test/resources/unit/custom-configuration/cpd-txt-format-configuration-plugin-config.xml
new file mode 100644
index 0000000..5ea5415
--- /dev/null
+++ b/src/test/resources/unit/custom-configuration/cpd-txt-format-configuration-plugin-config.xml
@@ -0,0 +1,51 @@
+<!--
+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.
+-->
+
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>custom.configuration</groupId>
+  <artifactId>custom-configuration</artifactId>
+  <packaging>jar</packaging>
+  <version>1.0-SNAPSHOT</version>
+  <inceptionYear>2006</inceptionYear>
+  <name>Maven CPD Plugin Txt Format Configuration Test</name>
+  <url>http://maven.apache.org</url>
+  <build>
+    <finalName>txt-format-configuration</finalName>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <configuration>
+          <project implementation="org.apache.maven.plugins.pmd.stubs.DefaultConfigurationMavenProjectStub"/>
+          <outputDirectory>${basedir}/target/test/unit/custom-configuration/target/site</outputDirectory>
+          <targetDirectory>${basedir}/target/test/unit/custom-configuration/target</targetDirectory>
+          <format>txt</format>
+          <linkXRef>false</linkXRef>
+          <minimumTokens>30</minimumTokens>
+
+          <compileSourceRoots>
+            <compileSourceRoot>${basedir}/src/test/resources/unit/custom-configuration/</compileSourceRoot>
+          </compileSourceRoots>
+          <sourceEncoding>UTF-8</sourceEncoding>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/src/test/resources/unit/default-configuration/pmd-report-custom-rules.xml b/src/test/resources/unit/default-configuration/pmd-report-custom-rules.xml
new file mode 100644
index 0000000..04c2c55
--- /dev/null
+++ b/src/test/resources/unit/default-configuration/pmd-report-custom-rules.xml
@@ -0,0 +1,62 @@
+<!--
+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.
+-->
+
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>def.configuration</groupId>
+  <artifactId>default-configuration</artifactId>
+  <packaging>jar</packaging>
+  <version>1.0-SNAPSHOT</version>
+  <inceptionYear>2006</inceptionYear>
+  <name>Maven PMD Plugin Default Configuration Test</name>
+  <url>http://maven.apache.org</url>
+  <build>
+    <finalName>default-configuration</finalName>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <configuration>
+          <project implementation="org.apache.maven.plugins.pmd.stubs.DefaultConfigurationMavenProjectStub"/>
+          <outputDirectory>${basedir}/target/test/unit/default-configuration/target/site</outputDirectory>
+          <targetDirectory>${basedir}/target/test/unit/default-configuration/target</targetDirectory>
+          <rulesetsTargetDirectory>${basedir}/target/test/unit/default-configuration/target/pmd/rulesets</rulesetsTargetDirectory>
+          <rulesets>
+            <ruleset>${basedir}/src/test/resources/unit/default-configuration/rulesets/custom-rules.xml</ruleset>
+          </rulesets>
+          <format>xml</format>
+          <linkXRef>true</linkXRef>
+          <xrefLocation>${basedir}/target/test/unit/default-configuration/target/site/xref</xrefLocation>
+          <sourceEncoding>UTF-8</sourceEncoding>
+          <compileSourceRoots>
+            <compileSourceRoot>${basedir}/src/test/resources/unit/default-configuration/</compileSourceRoot>
+          </compileSourceRoots>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <reporting>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jxr-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </reporting>
+</project>
diff --git a/src/test/resources/unit/default-configuration/rulesets/custom-rules.xml b/src/test/resources/unit/default-configuration/rulesets/custom-rules.xml
new file mode 100644
index 0000000..4946c53
--- /dev/null
+++ b/src/test/resources/unit/default-configuration/rulesets/custom-rules.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ruleset name="Custom PMD Rule Ruleset"
+    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
+
+    <description>
+        A ruleset with a custom rule without "externalInfoUrl".
+    </description>
+
+    <rule name="CustomRule"
+          language="java"
+          message="custom rule test"
+          class="net.sourceforge.pmd.lang.rule.XPathRule">
+        <description>custom xpath rule test</description>
+        <priority>1</priority>
+        <properties>
+            <property name="xpath">
+                <value>
+<![CDATA[
+//ClassOrInterfaceDeclaration[@SimpleName = 'App']
+]]>
+                </value>
+            </property>
+        </properties>
+    </rule>
+
+    <rule ref="category/java/bestpractices.xml/UnusedPrivateField" />
+</ruleset>