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