View Javadoc
1   package org.apache.maven.plugins.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.IOException;
24  import java.io.UnsupportedEncodingException;
25  import java.util.Locale;
26  import java.util.Properties;
27  import java.util.ResourceBundle;
28  
29  import org.apache.maven.plugins.annotations.Mojo;
30  import org.apache.maven.plugins.annotations.Parameter;
31  import org.apache.maven.plugins.pmd.exec.CpdExecutor;
32  import org.apache.maven.plugins.pmd.exec.CpdRequest;
33  import org.apache.maven.plugins.pmd.exec.CpdResult;
34  import org.apache.maven.reporting.MavenReportException;
35  import org.apache.maven.shared.utils.logging.MessageUtils;
36  import org.apache.maven.toolchain.Toolchain;
37  import org.codehaus.plexus.util.StringUtils;
38  import org.codehaus.plexus.util.WriterFactory;
39  
40  import net.sourceforge.pmd.cpd.JavaTokenizer;
41  import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
42  
43  /**
44   * Creates a report for PMD's Copy/Paste Detector (CPD) tool.
45   * It can also generate a cpd results file in any of these formats: xml, csv or txt.
46   *
47   * <p>See <a href="https://pmd.github.io/latest/pmd_userdocs_cpd.html">Finding duplicated code</a>
48   * for more details.
49   *
50   * @author Mike Perham
51   * @version $Id$
52   * @since 2.0
53   */
54  @Mojo( name = "cpd", threadSafe = true )
55  public class CpdReport
56      extends AbstractPmdReport
57  {
58      /**
59       * The programming language to be analyzed by CPD. Valid values are currently <code>java</code>,
60       * <code>javascript</code> or <code>jsp</code>.
61       *
62       * @since 3.5
63       */
64      @Parameter( defaultValue = "java" )
65      private String language;
66  
67      /**
68       * The minimum number of tokens that need to be duplicated before it causes a violation.
69       */
70      @Parameter( property = "minimumTokens", defaultValue = "100" )
71      private int minimumTokens;
72  
73      /**
74       * Skip the CPD report generation. Most useful on the command line via "-Dcpd.skip=true".
75       *
76       * @since 2.1
77       */
78      @Parameter( property = "cpd.skip", defaultValue = "false" )
79      private boolean skip;
80  
81      /**
82       * If true, CPD ignores literal value differences when evaluating a duplicate block. This means that
83       * <code>foo=42;</code> and <code>foo=43;</code> will be seen as equivalent. You may want to run PMD with this
84       * option off to start with and then switch it on to see what it turns up.
85       *
86       * @since 2.5
87       */
88      @Parameter( property = "cpd.ignoreLiterals", defaultValue = "false" )
89      private boolean ignoreLiterals;
90  
91      /**
92       * Similar to <code>ignoreLiterals</code> but for identifiers; i.e., variable names, methods names, and so forth.
93       *
94       * @since 2.5
95       */
96      @Parameter( property = "cpd.ignoreIdentifiers", defaultValue = "false" )
97      private boolean ignoreIdentifiers;
98  
99      /**
100      * If true, CPD ignores annotations.
101      *
102      * @since 3.11.0
103      */
104     @Parameter( property = "cpd.ignoreAnnotations", defaultValue = "false" )
105     private boolean ignoreAnnotations;
106 
107     /**
108      * Contains the result of the last CPD execution.
109      * It might be <code>null</code> which means, that CPD
110      * has not been executed yet.
111      */
112     private CpdResult cpdResult;
113 
114     /**
115      * {@inheritDoc}
116      */
117     public String getName( Locale locale )
118     {
119         return getBundle( locale ).getString( "report.cpd.name" );
120     }
121 
122     /**
123      * {@inheritDoc}
124      */
125     public String getDescription( Locale locale )
126     {
127         return getBundle( locale ).getString( "report.cpd.description" );
128     }
129 
130     /**
131      * {@inheritDoc}
132      */
133     @Override
134     public void executeReport( Locale locale )
135         throws MavenReportException
136     {
137         try
138         {
139             execute( locale );
140         }
141         finally
142         {
143             if ( getSink() != null )
144             {
145                 getSink().close();
146             }
147         }
148     }
149 
150     private void execute( Locale locale )
151         throws MavenReportException
152     {
153         if ( !skip && canGenerateReport() )
154         {
155             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
156             try
157             {
158                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
159 
160                 generateMavenSiteReport( locale );
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             getLog().info( "Skipping CPD execution" );
176             return false;
177         }
178 
179         boolean result = super.canGenerateReport();
180         if ( result )
181         {
182             try
183             {
184                 executeCpd();
185                 if ( skipEmptyReport )
186                 {
187                     result = cpdResult.hasDuplications();
188                     if ( !result )
189                     {
190                         getLog().debug( "Skipping report since skipEmptyReport is true and there are no CPD issues." );
191                     }
192                 }
193             }
194             catch ( MavenReportException e )
195             {
196                 throw new RuntimeException( e );
197             }
198         }
199         return result;
200     }
201 
202     private void executeCpd()
203         throws MavenReportException
204     {
205         if ( cpdResult != null )
206         {
207             // CPD has already been run
208             getLog().debug( "CPD has already been run - skipping redundant execution." );
209             return;
210         }
211 
212         Properties languageProperties = new Properties();
213         if ( ignoreLiterals )
214         {
215             languageProperties.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" );
216         }
217         if ( ignoreIdentifiers )
218         {
219             languageProperties.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" );
220         }
221         if ( ignoreAnnotations )
222         {
223             languageProperties.setProperty( JavaTokenizer.IGNORE_ANNOTATIONS, "true" );
224         }
225         try
226         {
227             filesToProcess = getFilesToProcess();
228 
229             CpdRequest request = new CpdRequest();
230             request.setMinimumTokens( minimumTokens );
231             request.setLanguage( language );
232             request.setLanguageProperties( languageProperties );
233             request.setSourceEncoding( determineEncoding( !filesToProcess.isEmpty() ) );
234             request.addFiles( filesToProcess.keySet() );
235 
236             request.setShowPmdLog( showPmdLog );
237             request.setColorizedLog( MessageUtils.isColorEnabled() );
238             request.setLogLevel( determineCurrentRootLogLevel() );
239 
240             request.setExcludeFromFailureFile( excludeFromFailureFile );
241             request.setTargetDirectory( targetDirectory.getAbsolutePath() );
242             request.setOutputEncoding( getOutputEncoding() );
243             request.setFormat( format );
244             request.setIncludeXmlInSite( includeXmlInSite );
245             request.setReportOutputDirectory( getReportOutputDirectory().getAbsolutePath() );
246 
247             Toolchain tc = getToolchain();
248             if ( tc != null )
249             {
250                 getLog().info( "Toolchain in maven-pmd-plugin: " + tc );
251                 String javaExecutable = tc.findTool( "java" ); //NOI18N
252                 request.setJavaExecutable( javaExecutable );
253             }
254 
255             getLog().info( "PMD version: " + AbstractPmdReport.getPmdVersion() );
256             cpdResult = CpdExecutor.execute( request );
257         }
258         catch ( UnsupportedEncodingException e )
259         {
260             throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e );
261         }
262         catch ( IOException e )
263         {
264             throw new MavenReportException( e.getMessage(), e );
265         }
266     }
267 
268     private void generateMavenSiteReport( Locale locale )
269     {
270         CpdReportGenerator gen = new CpdReportGenerator( getSink(), filesToProcess, getBundle( locale ),
271                 isAggregator() );
272         gen.generate( cpdResult.getDuplications() );
273     }
274 
275     private String determineEncoding( boolean showWarn )
276         throws UnsupportedEncodingException
277     {
278         String encoding = WriterFactory.FILE_ENCODING;
279         if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
280         {
281 
282             encoding = getSourceEncoding();
283             // test encoding as CPD will convert exception into a RuntimeException
284             WriterFactory.newWriter( new ByteArrayOutputStream(), encoding );
285 
286         }
287         else if ( showWarn )
288         {
289             getLog().warn( "File encoding has not been set, using platform encoding " + WriterFactory.FILE_ENCODING
290                                + ", i.e. build is platform dependent!" );
291             encoding = WriterFactory.FILE_ENCODING;
292         }
293         return encoding;
294     }
295 
296     /**
297      * {@inheritDoc}
298      */
299     public String getOutputName()
300     {
301         return "cpd";
302     }
303 
304     private static ResourceBundle getBundle( Locale locale )
305     {
306         return ResourceBundle.getBundle( "cpd-report", locale, CpdReport.class.getClassLoader() );
307     }
308 
309     /**
310      * Create and return the correct renderer for the output type.
311      *
312      * @return the renderer based on the configured output
313      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
314      * @deprecated Use {@link CpdExecutor#createRenderer(String, String)} instead.
315      */
316     @Deprecated
317     public CPDRenderer createRenderer() throws MavenReportException
318     {
319         return CpdExecutor.createRenderer( format, getOutputEncoding() );
320     }
321 }