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.XMLRenderer;
55  import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
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                 generateMavenSiteReport( locale );
172             }
173             finally
174             {
175                 Thread.currentThread().setContextClassLoader( origLoader );
176             }
177 
178         }
179     }
180 
181     @Override
182     public boolean canGenerateReport()
183     {
184         if ( skip )
185         {
186             return false;
187         }
188 
189         boolean result = super.canGenerateReport();
190         if ( result )
191         {
192             try
193             {
194                 executeCpdWithClassloader();
195                 if ( skipEmptyReport )
196                 {
197                     result = cpd.getMatches().hasNext();
198                     if ( result )
199                     {
200                         getLog().debug( "Skipping report since skipEmptyReport is true and there are no CPD issues." );
201                     }
202                 }
203             }
204             catch ( MavenReportException e )
205             {
206                 throw new RuntimeException( e );
207             }
208         }
209         return result;
210     }
211 
212     private void executeCpdWithClassloader()
213         throws MavenReportException
214     {
215         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
216         try
217         {
218             Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
219             executeCpd();
220         }
221         finally
222         {
223             Thread.currentThread().setContextClassLoader( origLoader );
224         }
225     }
226 
227     private void executeCpd()
228         throws MavenReportException
229     {
230         if ( cpd != null )
231         {
232             // CPD has already been run
233             getLog().debug( "CPD has already been run - skipping redundant execution." );
234             return;
235         }
236 
237         setupPmdLogging();
238 
239         Properties p = new Properties();
240         if ( ignoreLiterals )
241         {
242             p.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
243         }
244         if ( ignoreIdentifiers )
245         {
246             p.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
247         }
248         if ( ignoreAnnotations )
249         {
250             p.setProperty( JavaTokenizer.IGNORE_ANNOTATIONS, "true" );
251         }
252         try
253         {
254             if ( filesToProcess == null )
255             {
256                 filesToProcess = getFilesToProcess();
257             }
258 
259             try
260             {
261                 excludeDuplicationsFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
262             }
263             catch ( MojoExecutionException e )
264             {
265                 throw new MavenReportException( "Error loading exclusions", e );
266             }
267 
268             String encoding = determineEncoding( !filesToProcess.isEmpty() );
269             Language cpdLanguage;
270             if ( "java".equals ( language ) || null == language )
271             {
272                 cpdLanguage = new JavaLanguage( p );
273             }
274             else if ( "javascript".equals( language ) )
275             {
276                 cpdLanguage = new EcmascriptLanguage();
277             }
278             else if ( "jsp".equals( language ) )
279             {
280                 cpdLanguage = new JSPLanguage();
281             }
282             else
283             {
284                 cpdLanguage = LanguageFactory.createLanguage( language, p );
285             }
286 
287             CPDConfiguration cpdConfiguration = new CPDConfiguration();
288             cpdConfiguration.setMinimumTileSize( minimumTokens );
289             cpdConfiguration.setLanguage( cpdLanguage );
290             cpdConfiguration.setSourceEncoding( encoding );
291 
292             cpd = new CPD( cpdConfiguration );
293 
294             for ( File file : filesToProcess.keySet() )
295             {
296                 cpd.add( file );
297             }
298         }
299         catch ( UnsupportedEncodingException e )
300         {
301             throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e );
302         }
303         catch ( IOException e )
304         {
305             throw new MavenReportException( e.getMessage(), e );
306         }
307         getLog().debug( "Executing CPD..." );
308         cpd.go();
309         getLog().debug( "CPD finished." );
310 
311         // always create XML format. we need to output it even if the file list is empty or we have no duplications
312         // so the "check" goals can check for violations
313         writeXmlReport( cpd );
314 
315         // html format is handled by maven site report, xml format as already bean rendered
316         if ( !isHtml() && !isXml() )
317         {
318             writeFormattedReport( cpd );
319         }
320     }
321 
322     private Iterator<Match> filterMatches( Iterator<Match> matches )
323     {
324         getLog().debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
325             + " configured exclusions." );
326 
327         List<Match> filteredMatches = new ArrayList<>();
328         int excludedDuplications = 0;
329         while ( matches.hasNext() )
330         {
331             Match match = matches.next();
332             if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) )
333             {
334                 excludedDuplications++;
335             }
336             else
337             {
338                 filteredMatches.add( match );
339             }
340         }
341 
342         getLog().debug( "Excluded " + excludedDuplications + " duplications." );
343         return filteredMatches.iterator();
344     }
345 
346     private void generateMavenSiteReport( Locale locale )
347     {
348         CpdReportGenerator gen = new CpdReportGenerator( getSink(), filesToProcess, getBundle( locale ), aggregate );
349         Iterator<Match> matches = cpd.getMatches();
350         gen.generate( filterMatches( matches ) );
351     }
352 
353     private String determineEncoding( boolean showWarn )
354         throws UnsupportedEncodingException
355     {
356         String encoding = WriterFactory.FILE_ENCODING;
357         if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
358         {
359 
360             encoding = getSourceEncoding();
361             // test encoding as CPD will convert exception into a RuntimeException
362             WriterFactory.newWriter( new ByteArrayOutputStream(), encoding );
363 
364         }
365         else if ( showWarn )
366         {
367             getLog().warn( "File encoding has not been set, using platform encoding " + WriterFactory.FILE_ENCODING
368                                + ", i.e. build is platform dependent!" );
369             encoding = WriterFactory.FILE_ENCODING;
370         }
371         return encoding;
372     }
373 
374     private void writeFormattedReport( CPD cpd )
375         throws MavenReportException
376     {
377         CPDRenderer r = createRenderer();
378         writeReport( cpd, r, format );
379 
380     }
381 
382     void writeXmlReport( CPD cpd ) throws MavenReportException
383     {
384         File targetFile = writeReport( cpd, new XMLRenderer( getOutputEncoding() ), "xml" );
385         if ( includeXmlInSite )
386         {
387             File siteDir = getReportOutputDirectory();
388             siteDir.mkdirs();
389             try
390             {
391                 FileUtils.copyFile( targetFile, new File( siteDir, "cpd.xml" ) );
392             }
393             catch ( IOException e )
394             {
395                 throw new MavenReportException( e.getMessage(), e );
396             }
397         }
398     }
399 
400     private File writeReport( CPD cpd, CPDRenderer r, String extension ) throws MavenReportException
401     {
402         if ( r == null )
403         {
404             return null;
405         }
406 
407         File targetFile = new File( targetDirectory, "cpd." + extension );
408         targetDirectory.mkdirs();
409         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
410         {
411             r.render( filterMatches( cpd.getMatches() ), writer );
412             writer.flush();
413         }
414         catch ( IOException ioe )
415         {
416             throw new MavenReportException( ioe.getMessage(), ioe );
417         }
418         return targetFile;
419     }
420 
421     /**
422      * {@inheritDoc}
423      */
424     public String getOutputName()
425     {
426         return "cpd";
427     }
428 
429     private static ResourceBundle getBundle( Locale locale )
430     {
431         return ResourceBundle.getBundle( "cpd-report", locale, CpdReport.class.getClassLoader() );
432     }
433 
434     /**
435      * Create and return the correct renderer for the output type.
436      *
437      * @return the renderer based on the configured output
438      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
439      */
440     public CPDRenderer createRenderer()
441         throws MavenReportException
442     {
443         CPDRenderer renderer = null;
444         if ( "xml".equals( format ) )
445         {
446             renderer = new XMLRenderer( getOutputEncoding() );
447         }
448         else if ( "csv".equals( format ) )
449         {
450             renderer = new CSVRenderer();
451         }
452         else if ( !"".equals( format ) && !"none".equals( format ) )
453         {
454             try
455             {
456                 renderer = (CPDRenderer) Class.forName( format ).getConstructor().newInstance();
457             }
458             catch ( Exception e )
459             {
460                 throw new MavenReportException( "Can't find CPD custom format " + format + ": "
461                     + e.getClass().getName(), e );
462             }
463         }
464 
465         return renderer;
466     }
467 }