View Javadoc
1   package org.apache.maven.plugins.pmd;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStreamWriter;
27  import java.io.UnsupportedEncodingException;
28  import java.io.Writer;
29  import java.util.ArrayList;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Properties;
34  import java.util.ResourceBundle;
35  
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.reporting.MavenReportException;
40  import org.codehaus.plexus.util.FileUtils;
41  import org.codehaus.plexus.util.StringUtils;
42  import org.codehaus.plexus.util.WriterFactory;
43  
44  import net.sourceforge.pmd.cpd.CPD;
45  import net.sourceforge.pmd.cpd.CPDConfiguration;
46  import net.sourceforge.pmd.cpd.CSVRenderer;
47  import net.sourceforge.pmd.cpd.EcmascriptLanguage;
48  import net.sourceforge.pmd.cpd.JSPLanguage;
49  import net.sourceforge.pmd.cpd.JavaLanguage;
50  import net.sourceforge.pmd.cpd.JavaTokenizer;
51  import net.sourceforge.pmd.cpd.Language;
52  import net.sourceforge.pmd.cpd.LanguageFactory;
53  import net.sourceforge.pmd.cpd.Match;
54  import net.sourceforge.pmd.cpd.Renderer;
55  import net.sourceforge.pmd.cpd.XMLRenderer;
56  
57  /**
58   * Creates a report for PMD's CPD tool. See
59   * <a href="https://pmd.github.io/latest/pmd_userdocs_cpd.html">Finding duplicated code</a>
60   * for more details.
61   *
62   * @author Mike Perham
63   * @version $Id$
64   * @since 2.0
65   */
66  @Mojo( name = "cpd", threadSafe = true )
67  public class CpdReport
68      extends AbstractPmdReport
69  {
70      /**
71       * The programming language to be analyzed by CPD. Valid values are currently <code>java</code>,
72       * <code>javascript</code> or <code>jsp</code>.
73       *
74       * @since 3.5
75       */
76      @Parameter( defaultValue = "java" )
77      private String language;
78  
79      /**
80       * The minimum number of tokens that need to be duplicated before it causes a violation.
81       */
82      @Parameter( property = "minimumTokens", defaultValue = "100" )
83      private int minimumTokens;
84  
85      /**
86       * Skip the CPD report generation. Most useful on the command line via "-Dcpd.skip=true".
87       *
88       * @since 2.1
89       */
90      @Parameter( property = "cpd.skip", defaultValue = "false" )
91      private boolean skip;
92  
93      /**
94       * If true, CPD ignores literal value differences when evaluating a duplicate block. This means that
95       * <code>foo=42;</code> and <code>foo=43;</code> will be seen as equivalent. You may want to run PMD with this
96       * option off to start with and then switch it on to see what it turns up.
97       *
98       * @since 2.5
99       */
100     @Parameter( property = "cpd.ignoreLiterals", defaultValue = "false" )
101     private boolean ignoreLiterals;
102 
103     /**
104      * Similar to <code>ignoreLiterals</code> but for identifiers; i.e., variable names, methods names, and so forth.
105      *
106      * @since 2.5
107      */
108     @Parameter( property = "cpd.ignoreIdentifiers", defaultValue = "false" )
109     private boolean ignoreIdentifiers;
110 
111     /**
112      * If true, CPD ignores annotations.
113      *
114      * @since 3.11.0
115      */
116     @Parameter( property = "cpd.ignoreAnnotations", defaultValue = "false" )
117     private boolean ignoreAnnotations;
118 
119     /** The CPD instance used to analyze the files. Will itself collect the duplicated code matches. */
120     private CPD cpd;
121 
122     /** Helper to exclude duplications from the result. */
123     private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
124 
125     /**
126      * {@inheritDoc}
127      */
128     public String getName( Locale locale )
129     {
130         return getBundle( locale ).getString( "report.cpd.name" );
131     }
132 
133     /**
134      * {@inheritDoc}
135      */
136     public String getDescription( Locale locale )
137     {
138         return getBundle( locale ).getString( "report.cpd.description" );
139     }
140 
141     /**
142      * {@inheritDoc}
143      */
144     @Override
145     public void executeReport( Locale locale )
146         throws MavenReportException
147     {
148         try
149         {
150             execute( locale );
151         }
152         finally
153         {
154             if ( getSink() != null )
155             {
156                 getSink().close();
157             }
158         }
159     }
160 
161     private void execute( Locale locale )
162         throws MavenReportException
163     {
164         if ( !skip && canGenerateReport() )
165         {
166             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
167             try
168             {
169                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
170 
171                 generateReport( locale );
172 
173                 if ( !isHtml() && !isXml() )
174                 {
175                     writeNonHtml( cpd );
176                 }
177             }
178             finally
179             {
180                 Thread.currentThread().setContextClassLoader( origLoader );
181             }
182 
183         }
184     }
185 
186     @Override
187     public boolean canGenerateReport()
188     {
189         if ( skip )
190         {
191             return false;
192         }
193 
194         boolean result = super.canGenerateReport();
195         if ( result )
196         {
197             try
198             {
199                 executeCpdWithClassloader();
200                 if ( skipEmptyReport )
201                 {
202                     result = cpd.getMatches().hasNext();
203                     if ( result )
204                     {
205                         getLog().debug( "Skipping report since skipEmptyReport is true and there are no CPD issues." );
206                     }
207                 }
208             }
209             catch ( MavenReportException e )
210             {
211                 throw new RuntimeException( e );
212             }
213         }
214         return result;
215     }
216 
217     private void executeCpdWithClassloader()
218         throws MavenReportException
219     {
220         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
221         try
222         {
223             Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
224             executeCpd();
225         }
226         finally
227         {
228             Thread.currentThread().setContextClassLoader( origLoader );
229         }
230     }
231 
232     private void executeCpd()
233         throws MavenReportException
234     {
235         if ( cpd != null )
236         {
237             // CPD has already been run
238             getLog().debug( "CPD has already been run - skipping redundant execution." );
239             return;
240         }
241 
242         setupPmdLogging();
243 
244         Properties p = new Properties();
245         if ( ignoreLiterals )
246         {
247             p.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
248         }
249         if ( ignoreIdentifiers )
250         {
251             p.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
252         }
253         if ( ignoreAnnotations )
254         {
255             p.setProperty( JavaTokenizer.IGNORE_ANNOTATIONS, "true" );
256         }
257         try
258         {
259             if ( filesToProcess == null )
260             {
261                 filesToProcess = getFilesToProcess();
262             }
263 
264             try
265             {
266                 excludeDuplicationsFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
267             }
268             catch ( MojoExecutionException e )
269             {
270                 throw new MavenReportException( "Error loading exclusions", e );
271             }
272 
273             String encoding = determineEncoding( !filesToProcess.isEmpty() );
274             Language cpdLanguage;
275             if ( "java".equals ( language ) || null == language )
276             {
277                 cpdLanguage = new JavaLanguage( p );
278             }
279             else if ( "javascript".equals( language ) )
280             {
281                 cpdLanguage = new EcmascriptLanguage();
282             }
283             else if ( "jsp".equals( language ) )
284             {
285                 cpdLanguage = new JSPLanguage();
286             }
287             else
288             {
289                 cpdLanguage = LanguageFactory.createLanguage( language, p );
290             }
291 
292             CPDConfiguration cpdConfiguration = new CPDConfiguration();
293             cpdConfiguration.setMinimumTileSize( minimumTokens );
294             cpdConfiguration.setLanguage( cpdLanguage );
295             cpdConfiguration.setSourceEncoding( encoding );
296 
297             cpd = new CPD( cpdConfiguration );
298 
299             for ( File file : filesToProcess.keySet() )
300             {
301                 cpd.add( file );
302             }
303         }
304         catch ( UnsupportedEncodingException e )
305         {
306             throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e );
307         }
308         catch ( IOException e )
309         {
310             throw new MavenReportException( e.getMessage(), e );
311         }
312         getLog().debug( "Executing CPD..." );
313         cpd.go();
314         getLog().debug( "CPD finished." );
315 
316         // if format is XML, we need to output it even if the file list is empty or we have no duplications
317         // so the "check" goals can check for violations
318         if ( isXml() )
319         {
320             writeNonHtml( cpd );
321         }
322     }
323 
324     private Iterator<Match> filterMatches( Iterator<Match> matches )
325     {
326         getLog().debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
327             + " configured exclusions." );
328 
329         List<Match> filteredMatches = new ArrayList<>();
330         int excludedDuplications = 0;
331         while ( matches.hasNext() )
332         {
333             Match match = matches.next();
334             if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) )
335             {
336                 excludedDuplications++;
337             }
338             else
339             {
340                 filteredMatches.add( match );
341             }
342         }
343 
344         getLog().debug( "Excluded " + excludedDuplications + " duplications." );
345         return filteredMatches.iterator();
346     }
347 
348     private void generateReport( Locale locale )
349     {
350         CpdReportGenerator gen = new CpdReportGenerator( getSink(), filesToProcess, getBundle( locale ), aggregate );
351         Iterator<Match> matches = cpd.getMatches();
352         gen.generate( filterMatches( matches ) );
353     }
354 
355     private String determineEncoding( boolean showWarn )
356         throws UnsupportedEncodingException
357     {
358         String encoding = WriterFactory.FILE_ENCODING;
359         if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
360         {
361 
362             encoding = getSourceEncoding();
363             // test encoding as CPD will convert exception into a RuntimeException
364             WriterFactory.newWriter( new ByteArrayOutputStream(), encoding );
365 
366         }
367         else if ( showWarn )
368         {
369             getLog().warn( "File encoding has not been set, using platform encoding " + WriterFactory.FILE_ENCODING
370                                + ", i.e. build is platform dependent!" );
371             encoding = WriterFactory.FILE_ENCODING;
372         }
373         return encoding;
374     }
375 
376     void writeNonHtml( CPD cpd )
377         throws MavenReportException
378     {
379         Renderer r = createRenderer();
380 
381         if ( r == null )
382         {
383             return;
384         }
385 
386         String buffer = r.render( filterMatches( cpd.getMatches() ) );
387         File targetFile = new File( targetDirectory, "cpd." + format );
388         targetDirectory.mkdirs();
389         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
390         {
391             writer.write( buffer );
392             writer.flush();
393 
394             if ( includeXmlInSite )
395             {
396                 File siteDir = getReportOutputDirectory();
397                 siteDir.mkdirs();
398                 FileUtils.copyFile( targetFile, new File( siteDir, "cpd." + format ) );
399             }
400         }
401         catch ( IOException ioe )
402         {
403             throw new MavenReportException( ioe.getMessage(), ioe );
404         }
405     }
406 
407     /**
408      * {@inheritDoc}
409      */
410     public String getOutputName()
411     {
412         return "cpd";
413     }
414 
415     private static ResourceBundle getBundle( Locale locale )
416     {
417         return ResourceBundle.getBundle( "cpd-report", locale, CpdReport.class.getClassLoader() );
418     }
419 
420     /**
421      * Create and return the correct renderer for the output type.
422      *
423      * @return the renderer based on the configured output
424      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
425      */
426     public Renderer createRenderer()
427         throws MavenReportException
428     {
429         Renderer renderer = null;
430         if ( "xml".equals( format ) )
431         {
432             renderer = new XMLRenderer( getOutputEncoding() );
433         }
434         else if ( "csv".equals( format ) )
435         {
436             renderer = new CSVRenderer();
437         }
438         else if ( !"".equals( format ) && !"none".equals( format ) )
439         {
440             try
441             {
442                 renderer = (Renderer) Class.forName( format ).newInstance();
443             }
444             catch ( Exception e )
445             {
446                 throw new MavenReportException( "Can't find CPD custom format " + format + ": "
447                     + e.getClass().getName(), e );
448             }
449         }
450 
451         return renderer;
452     }
453 }