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