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