View Javadoc
1   package org.apache.maven.plugins.pmd.exec;
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.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.Writer;
30  import java.util.ArrayList;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Objects;
34  
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugins.pmd.ExcludeDuplicationsFromFile;
37  import org.apache.maven.reporting.MavenReportException;
38  import org.codehaus.plexus.util.FileUtils;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import net.sourceforge.pmd.cpd.CPD;
43  import net.sourceforge.pmd.cpd.CPDConfiguration;
44  import net.sourceforge.pmd.cpd.CSVRenderer;
45  import net.sourceforge.pmd.cpd.EcmascriptLanguage;
46  import net.sourceforge.pmd.cpd.JSPLanguage;
47  import net.sourceforge.pmd.cpd.JavaLanguage;
48  import net.sourceforge.pmd.cpd.Language;
49  import net.sourceforge.pmd.cpd.LanguageFactory;
50  import net.sourceforge.pmd.cpd.Match;
51  import net.sourceforge.pmd.cpd.SimpleRenderer;
52  import net.sourceforge.pmd.cpd.XMLRenderer;
53  import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
54  
55  /**
56   * Executes CPD with the configuration provided via {@link CpdRequest}.
57   */
58  public class CpdExecutor extends Executor
59  {
60      private static final Logger LOG = LoggerFactory.getLogger( CpdExecutor.class );
61  
62      public static CpdResult execute( CpdRequest request ) throws MavenReportException
63      {
64          if ( request.getJavaExecutable() != null )
65          {
66              return fork( request );
67          }
68  
69          ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
70          try
71          {
72              Thread.currentThread().setContextClassLoader( CpdExecutor.class.getClassLoader() );
73              CpdExecutor cpdExecutor = new CpdExecutor( request );
74              return cpdExecutor.run();
75          }
76          finally
77          {
78              Thread.currentThread().setContextClassLoader( origLoader );
79          }
80      }
81  
82      private static CpdResult fork( CpdRequest request )
83              throws MavenReportException
84      {
85          File basePmdDir = new File ( request.getTargetDirectory(), "pmd" );
86          basePmdDir.mkdirs();
87          File cpdRequestFile = new File( basePmdDir, "cpdrequest.bin" );
88          try ( ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( cpdRequestFile ) ) )
89          {
90              out.writeObject( request );
91          }
92          catch ( IOException e )
93          {
94              throw new MavenReportException( e.getMessage(), e );
95          }
96  
97          String classpath = buildClasspath();
98          ProcessBuilder pb = new ProcessBuilder();
99          // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
100         pb.environment().put( "CLASSPATH", classpath );
101         pb.command().add( request.getJavaExecutable() );
102         pb.command().add( CpdExecutor.class.getName() );
103         pb.command().add( cpdRequestFile.getAbsolutePath() );
104 
105         LOG.debug( "Executing: CLASSPATH={}, command={}", classpath, pb.command() );
106         try
107         {
108             final Process p = pb.start();
109             // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
110             // and inheritIO would only inherit file handles, not the changed streams.
111             ProcessStreamHandler.start( p.getInputStream(), System.out );
112             ProcessStreamHandler.start( p.getErrorStream(), System.err );
113             int exit = p.waitFor();
114             LOG.debug( "CpdExecutor exit code: {}", exit );
115             if ( exit != 0 )
116             {
117                 throw new MavenReportException( "CpdExecutor exited with exit code " + exit );
118             }
119             return new CpdResult( new File( request.getTargetDirectory(), "cpd.xml" ), request.getOutputEncoding() );
120         }
121         catch ( IOException e )
122         {
123             throw new MavenReportException( e.getMessage(), e );
124         }
125         catch ( InterruptedException e )
126         {
127             Thread.currentThread().interrupt();
128             throw new MavenReportException( e.getMessage(), e );
129         }
130     }
131 
132     /**
133      * Execute CPD analysis from CLI.
134      *
135      * <p>
136      * Single arg with the filename to the serialized {@link CpdRequest}.
137      *
138      * <p>
139      * Exit-code: 0 = success, 1 = failure in executing
140      *
141      * @param args
142      */
143     public static void main( String[] args )
144     {
145         File requestFile = new File( args[0] );
146         try ( ObjectInputStream in = new ObjectInputStream( new FileInputStream( requestFile ) ) )
147         {
148             CpdRequest request = (CpdRequest) in.readObject();
149             CpdExecutor cpdExecutor = new CpdExecutor( request );
150             cpdExecutor.setupLogLevel( request.getLogLevel() );
151             cpdExecutor.run();
152             System.exit( 0 );
153         }
154         catch ( IOException | ClassNotFoundException | MavenReportException e )
155         {
156             LOG.error( e.getMessage(), e );
157         }
158         System.exit( 1 );
159     }
160 
161     private final CpdRequest request;
162 
163     /** Helper to exclude duplications from the result. */
164     private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
165 
166     public CpdExecutor( CpdRequest request )
167     {
168         this.request = Objects.requireNonNull( request );
169     }
170 
171     private CpdResult run() throws MavenReportException
172     {
173         setupPmdLogging( request.isShowPmdLog(), request.isColorizedLog(), request.getLogLevel() );
174 
175         try
176         {
177             excludeDuplicationsFromFile.loadExcludeFromFailuresData( request.getExcludeFromFailureFile() );
178         }
179         catch ( MojoExecutionException e )
180         {
181             throw new MavenReportException( "Error loading exclusions", e );
182         }
183 
184         CPDConfiguration cpdConfiguration = new CPDConfiguration();
185         cpdConfiguration.setMinimumTileSize( request.getMinimumTokens() );
186         
187         Language cpdLanguage;
188         if ( "java".equals ( request.getLanguage() ) || null == request.getLanguage() )
189         {
190             cpdLanguage = new JavaLanguage( request.getLanguageProperties() );
191         }
192         else if ( "javascript".equals( request.getLanguage() ) )
193         {
194             cpdLanguage = new EcmascriptLanguage();
195         }
196         else if ( "jsp".equals( request.getLanguage() ) )
197         {
198             cpdLanguage = new JSPLanguage();
199         }
200         else
201         {
202             cpdLanguage = LanguageFactory.createLanguage( request.getLanguage(), request.getLanguageProperties() );
203         }
204         
205         cpdConfiguration.setLanguage( cpdLanguage );
206         cpdConfiguration.setSourceEncoding( request.getSourceEncoding() );
207 
208         CPD cpd = new CPD( cpdConfiguration );
209         try
210         {
211             cpd.add( request.getFiles() );
212         }
213         catch ( IOException e )
214         {
215             throw new MavenReportException( e.getMessage(), e );
216         }
217 
218         LOG.debug( "Executing CPD..." );
219         cpd.go();
220         LOG.debug( "CPD finished." );
221 
222         // always create XML format. we need to output it even if the file list is empty or we have no duplications
223         // so the "check" goals can check for violations
224         writeXmlReport( cpd );
225 
226         // html format is handled by maven site report, xml format has already been rendered
227         String format = request.getFormat();
228         if ( !"html".equals( format ) && !"xml".equals( format ) )
229         {
230             writeFormattedReport( cpd );
231         }
232 
233         return new CpdResult( new File( request.getTargetDirectory(), "cpd.xml" ), request.getOutputEncoding() );
234     }
235 
236     private void writeXmlReport( CPD cpd ) throws MavenReportException
237     {
238         File targetFile = writeReport( cpd, new XMLRenderer( request.getOutputEncoding() ), "xml" );
239         if ( request.isIncludeXmlInSite() )
240         {
241             File siteDir = new File( request.getReportOutputDirectory() );
242             siteDir.mkdirs();
243             try
244             {
245                 FileUtils.copyFile( targetFile, new File( siteDir, "cpd.xml" ) );
246             }
247             catch ( IOException e )
248             {
249                 throw new MavenReportException( e.getMessage(), e );
250             }
251         }
252     }
253 
254     private File writeReport( CPD cpd, CPDRenderer r, String extension ) throws MavenReportException
255     {
256         if ( r == null )
257         {
258             return null;
259         }
260 
261         File targetDir = new File( request.getTargetDirectory() );
262         targetDir.mkdirs();
263         File targetFile = new File( targetDir, "cpd." + extension );
264         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ),
265                 request.getOutputEncoding() ) )
266         {
267             r.render( filterMatches( cpd.getMatches() ), writer );
268             writer.flush();
269         }
270         catch ( IOException ioe )
271         {
272             throw new MavenReportException( ioe.getMessage(), ioe );
273         }
274         return targetFile;
275     }
276 
277     private void writeFormattedReport( CPD cpd )
278             throws MavenReportException
279         {
280             CPDRenderer r = createRenderer( request.getFormat(), request.getOutputEncoding() );
281             writeReport( cpd, r, request.getFormat() );
282 
283         }
284 
285     /**
286      * Create and return the correct renderer for the output type.
287      *
288      * @return the renderer based on the configured output
289      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
290      */
291     public static CPDRenderer createRenderer( String format, String outputEncoding )
292         throws MavenReportException
293     {
294         CPDRenderer renderer = null;
295         if ( "xml".equals( format ) )
296         {
297             renderer = new XMLRenderer( outputEncoding );
298         }
299         else if ( "csv".equals( format ) )
300         {
301             renderer = new CSVRenderer();
302         }
303         else if ( "txt".equals( format ) )
304         {
305             renderer = new SimpleRenderer();
306         }
307         else if ( !"".equals( format ) && !"none".equals( format ) )
308         {
309             try
310             {
311                 renderer = (CPDRenderer) Class.forName( format ).getConstructor().newInstance();
312             }
313             catch ( Exception e )
314             {
315                 throw new MavenReportException( "Can't find CPD custom format " + format + ": "
316                     + e.getClass().getName(), e );
317             }
318         }
319 
320         return renderer;
321     }
322 
323     private Iterator<Match> filterMatches( Iterator<Match> matches )
324     {
325         LOG.debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
326             + " configured exclusions." );
327 
328         List<Match> filteredMatches = new ArrayList<>();
329         int excludedDuplications = 0;
330         while ( matches.hasNext() )
331         {
332             Match match = matches.next();
333             if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) )
334             {
335                 excludedDuplications++;
336             }
337             else
338             {
339                 filteredMatches.add( match );
340             }
341         }
342 
343         LOG.debug( "Excluded " + excludedDuplications + " duplications." );
344         return filteredMatches.iterator();
345     }
346 
347 }