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