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