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.Map;
46  import java.util.Properties;
47  import java.util.ResourceBundle;
48  
49  /**
50   * Creates a report for PMD's CPD tool.  See
51   * <a href="http://pmd.sourceforge.net/cpd.html">http://pmd.sourceforge.net/cpd.html</a>
52   * for more detail.
53   *
54   * @author Mike Perham
55   * @version $Id: CpdReport.html 853015 2013-03-04 21:10:54Z olamy $
56   * @since 2.0
57   */
58  @Mojo( name = "cpd", threadSafe = true )
59  public class CpdReport
60      extends AbstractPmdReport
61  {
62      /**
63       * The minimum number of tokens that need to be duplicated before it causes a violation.
64       */
65      @Parameter( property = "minimumTokens", defaultValue = "100" )
66      private int minimumTokens;
67  
68      /**
69       * Skip the CPD report generation.  Most useful on the command line
70       * via "-Dcpd.skip=true".
71       *
72       * @since 2.1
73       */
74      @Parameter( property = "cpd.skip", defaultValue = "false" )
75      private boolean skip;
76  
77      /**
78       * If true, CPD ignores literal value differences when evaluating a duplicate block.
79       * This means that <code>foo=42;</code> and <code>foo=43;</code> will be seen as equivalent.
80       * You may want to run PMD with this option off to start with and then switch it on to see what it turns up.
81       *
82       * @since 2.5
83       */
84      @Parameter( property = "cpd.ignoreLiterals", defaultValue = "false" )
85      private boolean ignoreLiterals;
86  
87      /**
88       * Similar to <code>ignoreLiterals</code> but for identifiers; i.e., variable names, methods names, and so forth.
89       *
90       * @since 2.5
91       */
92      @Parameter( property = "cpd.ignoreIdentifiers", defaultValue = "false" )
93      private boolean ignoreIdentifiers;
94  
95      /**
96       * {@inheritDoc}
97       */
98      public String getName( Locale locale )
99      {
100         return getBundle( locale ).getString( "report.cpd.name" );
101     }
102 
103     /**
104      * {@inheritDoc}
105      */
106     public String getDescription( Locale locale )
107     {
108         return getBundle( locale ).getString( "report.cpd.description" );
109     }
110 
111     /**
112      * {@inheritDoc}
113      */
114     public void executeReport( Locale locale )
115         throws MavenReportException
116     {
117         try
118         {
119             execute( locale );
120         }
121         finally
122         {
123             if ( getSink() != null )
124             {
125                 getSink().close();
126             }
127         }
128     }
129 
130     private void execute( Locale locale )
131         throws MavenReportException
132     {
133         if ( !skip && canGenerateReport() )
134         {
135             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
136             try
137             {
138                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
139 
140                 CPD cpd = generateReport( locale );
141 
142                 if ( !isHtml() )
143                 {
144                     writeNonHtml( cpd );
145                 }
146             }
147             finally
148             {
149                 Thread.currentThread().setContextClassLoader( origLoader );
150             }
151 
152         }
153     }
154 
155     private CPD generateReport( Locale locale )
156         throws MavenReportException
157     {
158         Properties p = new Properties();
159         if ( ignoreLiterals )
160         {
161             p.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
162         }
163         if ( ignoreIdentifiers )
164         {
165             p.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
166         }
167 
168         CPD cpd;
169         Map<File, PmdFileInfo> files = null;
170 
171         try
172         {
173             files = getFilesToProcess();
174             String encoding = determineEncoding( !files.isEmpty() );
175 
176             CPDConfiguration cpdConfiguration = new CPDConfiguration( minimumTokens, new JavaLanguage( p ), encoding );
177             cpd = new CPD( cpdConfiguration );
178 
179             for ( File file : files.keySet() )
180             {
181                 cpd.add( file );
182             }
183         }
184         catch ( UnsupportedEncodingException e )
185         {
186             throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e );
187         }
188         catch ( IOException e )
189         {
190             throw new MavenReportException( e.getMessage(), e );
191         }
192         cpd.go();
193 
194         CpdReportGenerator gen = new CpdReportGenerator( getSink(), files, getBundle( locale ), aggregate );
195         gen.generate( cpd.getMatches() );
196 
197         return cpd;
198     }
199 
200     private String determineEncoding( boolean showWarn )
201         throws UnsupportedEncodingException
202     {
203         String encoding = WriterFactory.FILE_ENCODING;
204         if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
205         {
206 
207             encoding = getSourceEncoding();
208             // test encoding as CPD will convert exception into a RuntimeException
209             WriterFactory.newWriter( new ByteArrayOutputStream(), encoding );
210 
211         }
212         else if ( showWarn )
213         {
214             getLog().warn( "File encoding has not been set, using platform encoding " + WriterFactory.FILE_ENCODING
215                                + ", i.e. build is platform dependent!" );
216             encoding = WriterFactory.FILE_ENCODING;
217         }
218         return encoding;
219     }
220 
221     void writeNonHtml( CPD cpd )
222         throws MavenReportException
223     {
224         Renderer r = createRenderer();
225 
226         if ( r == null )
227         {
228             return;
229         }
230 
231         String buffer = r.render( cpd.getMatches() );
232         FileOutputStream tStream = null;
233         Writer writer = null;
234         try
235         {
236             targetDirectory.mkdirs();
237             File targetFile = new File( targetDirectory, "cpd." + format );
238             tStream = new FileOutputStream( targetFile );
239             writer = new OutputStreamWriter( tStream, getOutputEncoding() );
240             writer.write( buffer );
241             writer.close();
242 
243             if ( includeXmlInSite ) {
244                 File siteDir = getReportOutputDirectory();
245                 siteDir.mkdirs();
246                 FileUtils.copyFile( targetFile, new File( siteDir, "cpd." + format ) );
247             }
248         }
249         catch ( IOException ioe )
250         {
251             throw new MavenReportException( ioe.getMessage(), ioe );
252         }
253         finally
254         {
255             IOUtil.close( writer );
256             IOUtil.close( tStream );
257         }
258     }
259 
260     /**
261      * {@inheritDoc}
262      */
263     public String getOutputName()
264     {
265         return "cpd";
266     }
267 
268     private static ResourceBundle getBundle( Locale locale )
269     {
270         return ResourceBundle.getBundle( "cpd-report", locale, CpdReport.class.getClassLoader() );
271     }
272 
273     /**
274      * Create and return the correct renderer for the output type.
275      *
276      * @return the renderer based on the configured output
277      * @throws org.apache.maven.reporting.MavenReportException
278      *          if no renderer found for the output type
279      */
280     public Renderer createRenderer()
281         throws MavenReportException
282     {
283         Renderer renderer = null;
284         if ( "xml".equals( format ) )
285         {
286             //TODO: pmd should provide a better way to specify the output encoding (getOutputEncoding());
287             System.setProperty( "file.encoding", getOutputEncoding() );
288             renderer = new XMLRenderer();
289         }
290         else if ( "csv".equals( format ) )
291         {
292             renderer = new CSVRenderer();
293         }
294         else if ( !"".equals( format ) && !"none".equals( format ) )
295         {
296             try
297             {
298                 renderer = (Renderer) Class.forName( format ).newInstance();
299             }
300             catch ( Exception e )
301             {
302                 throw new MavenReportException(
303                     "Can't find CPD custom format " + format + ": " + e.getClass().getName(), e );
304             }
305         }
306 
307         return renderer;
308     }
309 }