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     /** The CPD instance used to analyze the files. Will itself collect the duplicated code matches. */
112     private CPD cpd;
113 
114     /** Helper to exclude duplications from the result. */
115     private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
116 
117     /**
118      * {@inheritDoc}
119      */
120     public String getName( Locale locale )
121     {
122         return getBundle( locale ).getString( "report.cpd.name" );
123     }
124 
125     /**
126      * {@inheritDoc}
127      */
128     public String getDescription( Locale locale )
129     {
130         return getBundle( locale ).getString( "report.cpd.description" );
131     }
132 
133     /**
134      * {@inheritDoc}
135      */
136     @Override
137     public void executeReport( Locale locale )
138         throws MavenReportException
139     {
140         try
141         {
142             execute( locale );
143         }
144         finally
145         {
146             if ( getSink() != null )
147             {
148                 getSink().close();
149             }
150         }
151     }
152 
153     private void execute( Locale locale )
154         throws MavenReportException
155     {
156         if ( !skip && canGenerateReport() )
157         {
158             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
159             try
160             {
161                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
162 
163                 generateReport( locale );
164 
165                 if ( !isHtml() && !isXml() )
166                 {
167                     writeNonHtml( cpd );
168                 }
169             }
170             finally
171             {
172                 Thread.currentThread().setContextClassLoader( origLoader );
173             }
174 
175         }
176     }
177 
178     @Override
179     public boolean canGenerateReport()
180     {
181         if ( skip )
182         {
183             return false;
184         }
185 
186         boolean result = super.canGenerateReport();
187         if ( result )
188         {
189             try
190             {
191                 executeCpdWithClassloader();
192                 if ( skipEmptyReport )
193                 {
194                     result = cpd.getMatches().hasNext();
195                     if ( result )
196                     {
197                         getLog().debug( "Skipping report since skipEmptyReport is true and there are no CPD issues." );
198                     }
199                 }
200             }
201             catch ( MavenReportException e )
202             {
203                 throw new RuntimeException( e );
204             }
205         }
206         return result;
207     }
208 
209     private void executeCpdWithClassloader()
210         throws MavenReportException
211     {
212         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
213         try
214         {
215             Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
216             executeCpd();
217         }
218         finally
219         {
220             Thread.currentThread().setContextClassLoader( origLoader );
221         }
222     }
223 
224     private void executeCpd()
225         throws MavenReportException
226     {
227         if ( cpd != null )
228         {
229             // CPD has already been run
230             getLog().debug( "CPD has already been run - skipping redundant execution." );
231             return;
232         }
233 
234         setupPmdLogging();
235 
236         Properties p = new Properties();
237         if ( ignoreLiterals )
238         {
239             p.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
240         }
241         if ( ignoreIdentifiers )
242         {
243             p.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
244         }
245 
246         try
247         {
248             if ( filesToProcess == null )
249             {
250                 filesToProcess = getFilesToProcess();
251             }
252 
253             try
254             {
255                 excludeDuplicationsFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
256             }
257             catch ( MojoExecutionException e )
258             {
259                 throw new MavenReportException( "Error loading exclusions", e );
260             }
261 
262             String encoding = determineEncoding( !filesToProcess.isEmpty() );
263             Language cpdLanguage;
264             if ( "java".equals ( language ) || null == language )
265             {
266                 cpdLanguage = new JavaLanguage( p );
267             }
268             else if ( "javascript".equals( language ) )
269             {
270                 cpdLanguage = new EcmascriptLanguage();
271             }
272             else if ( "jsp".equals( language ) )
273             {
274                 cpdLanguage = new JSPLanguage();
275             }
276             else
277             {
278                 cpdLanguage = LanguageFactory.createLanguage( language, p );
279             }
280 
281             CPDConfiguration cpdConfiguration = new CPDConfiguration();
282             cpdConfiguration.setMinimumTileSize( minimumTokens );
283             cpdConfiguration.setLanguage( cpdLanguage );
284             cpdConfiguration.setSourceEncoding( encoding );
285 
286             cpd = new CPD( cpdConfiguration );
287 
288             for ( File file : filesToProcess.keySet() )
289             {
290                 cpd.add( file );
291             }
292         }
293         catch ( UnsupportedEncodingException e )
294         {
295             throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e );
296         }
297         catch ( IOException e )
298         {
299             throw new MavenReportException( e.getMessage(), e );
300         }
301         getLog().debug( "Executing CPD..." );
302         cpd.go();
303         getLog().debug( "CPD finished." );
304 
305         // if format is XML, we need to output it even if the file list is empty or we have no duplications
306         // so the "check" goals can check for violations
307         if ( isXml() )
308         {
309             writeNonHtml( cpd );
310         }
311     }
312 
313     private Iterator<Match> filterMatches( Iterator<Match> matches )
314     {
315         getLog().debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
316             + " configured exclusions." );
317 
318         List<Match> filteredMatches = new ArrayList<>();
319         int excludedDuplications = 0;
320         while ( matches.hasNext() )
321         {
322             Match match = matches.next();
323             if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) )
324             {
325                 excludedDuplications++;
326             }
327             else
328             {
329                 filteredMatches.add( match );
330             }
331         }
332 
333         getLog().debug( "Excluded " + excludedDuplications + " duplications." );
334         return filteredMatches.iterator();
335     }
336 
337     private void generateReport( Locale locale )
338     {
339         CpdReportGenerator gen = new CpdReportGenerator( getSink(), filesToProcess, getBundle( locale ), aggregate );
340         Iterator<Match> matches = cpd.getMatches();
341         gen.generate( filterMatches( matches ) );
342     }
343 
344     private String determineEncoding( boolean showWarn )
345         throws UnsupportedEncodingException
346     {
347         String encoding = WriterFactory.FILE_ENCODING;
348         if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
349         {
350 
351             encoding = getSourceEncoding();
352             // test encoding as CPD will convert exception into a RuntimeException
353             WriterFactory.newWriter( new ByteArrayOutputStream(), encoding );
354 
355         }
356         else if ( showWarn )
357         {
358             getLog().warn( "File encoding has not been set, using platform encoding " + WriterFactory.FILE_ENCODING
359                                + ", i.e. build is platform dependent!" );
360             encoding = WriterFactory.FILE_ENCODING;
361         }
362         return encoding;
363     }
364 
365     void writeNonHtml( CPD cpd )
366         throws MavenReportException
367     {
368         Renderer r = createRenderer();
369 
370         if ( r == null )
371         {
372             return;
373         }
374 
375         String buffer = r.render( filterMatches( cpd.getMatches() ) );
376         File targetFile = new File( targetDirectory, "cpd." + format );
377         targetDirectory.mkdirs();
378         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
379         {
380             writer.write( buffer );
381             writer.flush();
382 
383             if ( includeXmlInSite )
384             {
385                 File siteDir = getReportOutputDirectory();
386                 siteDir.mkdirs();
387                 FileUtils.copyFile( targetFile, new File( siteDir, "cpd." + format ) );
388             }
389         }
390         catch ( IOException ioe )
391         {
392             throw new MavenReportException( ioe.getMessage(), ioe );
393         }
394     }
395 
396     /**
397      * {@inheritDoc}
398      */
399     public String getOutputName()
400     {
401         return "cpd";
402     }
403 
404     private static ResourceBundle getBundle( Locale locale )
405     {
406         return ResourceBundle.getBundle( "cpd-report", locale, CpdReport.class.getClassLoader() );
407     }
408 
409     /**
410      * Create and return the correct renderer for the output type.
411      *
412      * @return the renderer based on the configured output
413      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
414      */
415     public Renderer createRenderer()
416         throws MavenReportException
417     {
418         Renderer renderer = null;
419         if ( "xml".equals( format ) )
420         {
421             renderer = new XMLRenderer( getOutputEncoding() );
422         }
423         else if ( "csv".equals( format ) )
424         {
425             renderer = new CSVRenderer();
426         }
427         else if ( !"".equals( format ) && !"none".equals( format ) )
428         {
429             try
430             {
431                 renderer = (Renderer) Class.forName( format ).newInstance();
432             }
433             catch ( Exception e )
434             {
435                 throw new MavenReportException( "Can't find CPD custom format " + format + ": "
436                     + e.getClass().getName(), e );
437             }
438         }
439 
440         return renderer;
441     }
442 }