View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.pmd;
20  
21  import java.io.IOException;
22  import java.io.UnsupportedEncodingException;
23  import java.util.Locale;
24  import java.util.Properties;
25  
26  import net.sourceforge.pmd.cpd.JavaTokenizer;
27  import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
28  import org.apache.maven.plugins.annotations.Component;
29  import org.apache.maven.plugins.annotations.Mojo;
30  import org.apache.maven.plugins.annotations.Parameter;
31  import org.apache.maven.plugins.pmd.exec.CpdExecutor;
32  import org.apache.maven.plugins.pmd.exec.CpdRequest;
33  import org.apache.maven.plugins.pmd.exec.CpdResult;
34  import org.apache.maven.reporting.MavenReportException;
35  import org.apache.maven.toolchain.Toolchain;
36  import org.codehaus.plexus.i18n.I18N;
37  
38  /**
39   * Creates a report for PMD's Copy/Paste Detector (CPD) tool.
40   * It can also generate a cpd results file in any of these formats: xml, csv or txt.
41   *
42   * <p>See <a href="https://pmd.github.io/latest/pmd_userdocs_cpd.html">Finding duplicated code</a>
43   * for more details.
44   *
45   * @author Mike Perham
46   * @version $Id$
47   * @since 2.0
48   */
49  @Mojo(name = "cpd", threadSafe = true)
50  public class CpdReport extends AbstractPmdReport {
51      /**
52       * The programming language to be analyzed by CPD. Valid values are currently <code>java</code>,
53       * <code>javascript</code> or <code>jsp</code>.
54       *
55       * @since 3.5
56       */
57      @Parameter(defaultValue = "java")
58      private String language;
59  
60      /**
61       * The minimum number of tokens that need to be duplicated before it causes a violation.
62       */
63      @Parameter(property = "minimumTokens", defaultValue = "100")
64      private int minimumTokens;
65  
66      /**
67       * Skip the CPD report generation. Most useful on the command line via "-Dcpd.skip=true".
68       *
69       * @since 2.1
70       */
71      @Parameter(property = "cpd.skip", defaultValue = "false")
72      private boolean skip;
73  
74      /**
75       * If true, CPD ignores literal value differences when evaluating a duplicate block. This means that
76       * <code>foo=42;</code> and <code>foo=43;</code> will be seen as equivalent. You may want to run PMD with this
77       * option off to start with and then switch it on to see what it turns up.
78       *
79       * @since 2.5
80       */
81      @Parameter(property = "cpd.ignoreLiterals", defaultValue = "false")
82      private boolean ignoreLiterals;
83  
84      /**
85       * Similar to <code>ignoreLiterals</code> but for identifiers; i.e., variable names, methods names, and so forth.
86       *
87       * @since 2.5
88       */
89      @Parameter(property = "cpd.ignoreIdentifiers", defaultValue = "false")
90      private boolean ignoreIdentifiers;
91  
92      /**
93       * If true, CPD ignores annotations.
94       *
95       * @since 3.11.0
96       */
97      @Parameter(property = "cpd.ignoreAnnotations", defaultValue = "false")
98      private boolean ignoreAnnotations;
99  
100     /**
101      * Internationalization component
102      */
103     @Component
104     private I18N i18n;
105 
106     /**
107      * Contains the result of the last CPD execution.
108      * It might be <code>null</code> which means, that CPD
109      * has not been executed yet.
110      */
111     private CpdResult cpdResult;
112 
113     /** {@inheritDoc} */
114     public String getName(Locale locale) {
115         return getI18nString(locale, "name");
116     }
117 
118     /** {@inheritDoc} */
119     public String getDescription(Locale locale) {
120         return getI18nString(locale, "description");
121     }
122 
123     /**
124      * @param locale The locale
125      * @param key The key to search for
126      * @return The text appropriate for the locale.
127      */
128     protected String getI18nString(Locale locale, String key) {
129         return i18n.getString("cpd-report", locale, "report.cpd." + key);
130     }
131 
132     /**
133      * {@inheritDoc}
134      */
135     @Override
136     public void executeReport(Locale locale) throws MavenReportException {
137         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
138         try {
139             Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
140 
141             CpdReportRenderer r = new CpdReportRenderer(
142                     getSink(), i18n, locale, filesToProcess, cpdResult.getDuplications(), isAggregator());
143             r.render();
144         } finally {
145             Thread.currentThread().setContextClassLoader(origLoader);
146         }
147     }
148 
149     @Override
150     public boolean canGenerateReport() {
151         if (skip) {
152             getLog().info("Skipping CPD execution");
153             return false;
154         }
155 
156         boolean result = super.canGenerateReport();
157         if (result) {
158             try {
159                 executeCpd();
160                 if (skipEmptyReport) {
161                     result = cpdResult.hasDuplications();
162                     if (!result) {
163                         getLog().debug("Skipping report since skipEmptyReport is true and there are no CPD issues.");
164                     }
165                 }
166             } catch (MavenReportException e) {
167                 throw new RuntimeException(e);
168             }
169         }
170         return result;
171     }
172 
173     private void executeCpd() throws MavenReportException {
174         if (cpdResult != null) {
175             // CPD has already been run
176             getLog().debug("CPD has already been run - skipping redundant execution.");
177             return;
178         }
179 
180         Properties languageProperties = new Properties();
181         if (ignoreLiterals) {
182             languageProperties.setProperty(JavaTokenizer.IGNORE_LITERALS, "true");
183         }
184         if (ignoreIdentifiers) {
185             languageProperties.setProperty(JavaTokenizer.IGNORE_IDENTIFIERS, "true");
186         }
187         if (ignoreAnnotations) {
188             languageProperties.setProperty(JavaTokenizer.IGNORE_ANNOTATIONS, "true");
189         }
190         try {
191             filesToProcess = getFilesToProcess();
192 
193             CpdRequest request = new CpdRequest();
194             request.setMinimumTokens(minimumTokens);
195             request.setLanguage(language);
196             request.setLanguageProperties(languageProperties);
197             request.setSourceEncoding(getInputEncoding());
198             request.addFiles(filesToProcess.keySet());
199 
200             request.setShowPmdLog(showPmdLog);
201             request.setLogLevel(determineCurrentRootLogLevel());
202 
203             request.setExcludeFromFailureFile(excludeFromFailureFile);
204             request.setTargetDirectory(targetDirectory.getAbsolutePath());
205             request.setOutputEncoding(getOutputEncoding());
206             request.setFormat(format);
207             request.setIncludeXmlInSite(includeXmlInSite);
208             request.setReportOutputDirectory(getReportOutputDirectory().getAbsolutePath());
209 
210             Toolchain tc = getToolchain();
211             if (tc != null) {
212                 getLog().info("Toolchain in maven-pmd-plugin: " + tc);
213                 String javaExecutable = tc.findTool("java"); // NOI18N
214                 request.setJavaExecutable(javaExecutable);
215             }
216 
217             getLog().info("PMD version: " + AbstractPmdReport.getPmdVersion());
218             cpdResult = CpdExecutor.execute(request);
219         } catch (UnsupportedEncodingException e) {
220             throw new MavenReportException("Encoding '" + getInputEncoding() + "' is not supported.", e);
221         } catch (IOException e) {
222             throw new MavenReportException(e.getMessage(), e);
223         }
224     }
225 
226     /**
227      * {@inheritDoc}
228      */
229     public String getOutputName() {
230         return "cpd";
231     }
232 
233     /**
234      * Create and return the correct renderer for the output type.
235      *
236      * @return the renderer based on the configured output
237      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
238      * @deprecated Use {@link CpdExecutor#createRenderer(String, String)} instead.
239      */
240     @Deprecated
241     public CPDRenderer createRenderer() throws MavenReportException {
242         return CpdExecutor.createRenderer(format, getOutputEncoding());
243     }
244 }