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.Closeable;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.ObjectInputStream;
28  import java.io.ObjectOutputStream;
29  import java.io.OutputStreamWriter;
30  import java.io.Writer;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Objects;
36  
37  import org.apache.commons.io.IOUtils;
38  import org.apache.commons.lang3.StringUtils;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugins.pmd.ExcludeViolationsFromFile;
41  import org.apache.maven.plugins.pmd.PmdCollectingRenderer;
42  import org.apache.maven.reporting.MavenReportException;
43  import org.codehaus.plexus.util.FileUtils;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import net.sourceforge.pmd.PMD;
48  import net.sourceforge.pmd.PMDConfiguration;
49  import net.sourceforge.pmd.Report;
50  import net.sourceforge.pmd.RuleContext;
51  import net.sourceforge.pmd.RulePriority;
52  import net.sourceforge.pmd.RuleSetFactory;
53  import net.sourceforge.pmd.RuleSetNotFoundException;
54  import net.sourceforge.pmd.RuleViolation;
55  import net.sourceforge.pmd.RulesetsFactoryUtils;
56  import net.sourceforge.pmd.benchmark.TextTimingReportRenderer;
57  import net.sourceforge.pmd.benchmark.TimeTracker;
58  import net.sourceforge.pmd.benchmark.TimingReport;
59  import net.sourceforge.pmd.benchmark.TimingReportRenderer;
60  import net.sourceforge.pmd.lang.Language;
61  import net.sourceforge.pmd.lang.LanguageRegistry;
62  import net.sourceforge.pmd.lang.LanguageVersion;
63  import net.sourceforge.pmd.renderers.CSVRenderer;
64  import net.sourceforge.pmd.renderers.HTMLRenderer;
65  import net.sourceforge.pmd.renderers.Renderer;
66  import net.sourceforge.pmd.renderers.TextRenderer;
67  import net.sourceforge.pmd.renderers.XMLRenderer;
68  import net.sourceforge.pmd.util.datasource.DataSource;
69  import net.sourceforge.pmd.util.datasource.FileDataSource;
70  
71  /**
72   * Executes PMD with the configuration provided via {@link PmdRequest}.
73   */
74  public class PmdExecutor extends Executor
75  {
76      private static final Logger LOG = LoggerFactory.getLogger( PmdExecutor.class );
77  
78      public static PmdResult execute( PmdRequest request ) throws MavenReportException
79      {
80          if ( request.getJavaExecutable() != null )
81          {
82              return fork( request );
83          }
84  
85          // make sure the class loaders are correct and call this in the same JVM
86          ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
87          try
88          {
89              Thread.currentThread().setContextClassLoader( PmdExecutor.class.getClassLoader() );
90              PmdExecutor executor = new PmdExecutor( request );
91              return executor.run();
92          }
93          finally
94          {
95              Thread.currentThread().setContextClassLoader( origLoader );
96          }
97      }
98  
99      private static PmdResult fork( PmdRequest request )
100             throws MavenReportException
101     {
102         File basePmdDir = new File ( request.getTargetDirectory(), "pmd" );
103         basePmdDir.mkdirs();
104         File pmdRequestFile = new File( basePmdDir, "pmdrequest.bin" );
105         try ( ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( pmdRequestFile ) ) )
106         {
107             out.writeObject( request );
108         }
109         catch ( IOException e )
110         {
111             throw new MavenReportException( e.getMessage(), e );
112         }
113 
114         String classpath = buildClasspath();
115         ProcessBuilder pb = new ProcessBuilder();
116         // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
117         pb.environment().put( "CLASSPATH", classpath );
118         pb.command().add( request.getJavaExecutable() );
119         pb.command().add( PmdExecutor.class.getName() );
120         pb.command().add( pmdRequestFile.getAbsolutePath() );
121 
122         LOG.debug( "Executing: CLASSPATH={}, command={}", classpath, pb.command() );
123         try
124         {
125             final Process p = pb.start();
126             // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
127             // and inheritIO would only inherit file handles, not the changed streams.
128             ProcessStreamHandler.start( p.getInputStream(), System.out );
129             ProcessStreamHandler.start( p.getErrorStream(), System.err );
130             int exit = p.waitFor();
131             LOG.debug( "PmdExecutor exit code: {}", exit );
132             if ( exit != 0 )
133             {
134                 throw new MavenReportException( "PmdExecutor exited with exit code " + exit );
135             }
136             return new PmdResult( new File( request.getTargetDirectory(), "pmd.xml" ), request.getOutputEncoding() );
137         }
138         catch ( IOException e )
139         {
140             throw new MavenReportException( e.getMessage(), e );
141         }
142         catch ( InterruptedException e )
143         {
144             Thread.currentThread().interrupt();
145             throw new MavenReportException( e.getMessage(), e );
146         }
147     }
148 
149     /**
150      * Execute PMD analysis from CLI.
151      * 
152      * <p>
153      * Single arg with the filename to the serialized {@link PmdRequest}.
154      * 
155      * <p>
156      * Exit-code: 0 = success, 1 = failure in executing
157      * 
158      * @param args
159      */
160     public static void main( String[] args )
161     {
162         File requestFile = new File( args[0] );
163         try ( ObjectInputStream in = new ObjectInputStream( new FileInputStream( requestFile ) ) )
164         {
165             PmdRequest request = (PmdRequest) in.readObject();
166             PmdExecutor pmdExecutor = new PmdExecutor( request );
167             pmdExecutor.setupLogLevel( request.getLogLevel() );
168             pmdExecutor.run();
169             System.exit( 0 );
170         }
171         catch ( IOException | ClassNotFoundException | MavenReportException e )
172         {
173             LOG.error( e.getMessage(), e );
174         }
175         System.exit( 1 );
176     }
177 
178     private final PmdRequest request;
179 
180     public PmdExecutor( PmdRequest request )
181     {
182         this.request = Objects.requireNonNull( request );
183     }
184 
185     private PmdResult run() throws MavenReportException
186     {
187         setupPmdLogging( request.isShowPmdLog(), request.isColorizedLog(), request.getLogLevel() );
188 
189         PMDConfiguration configuration = new PMDConfiguration();
190         LanguageVersion languageVersion = null;
191         Language language = LanguageRegistry
192                 .findLanguageByTerseName( request.getLanguage() != null ? request.getLanguage() : "java" );
193         if ( language == null )
194         {
195             throw new MavenReportException( "Unsupported language: " + request.getLanguage() );
196         }
197         if ( request.getLanguageVersion() != null )
198         {
199             languageVersion = language.getVersion( request.getLanguageVersion() );
200             if ( languageVersion == null )
201             {
202                 throw new MavenReportException( "Unsupported targetJdk value '" + request.getLanguageVersion() + "'." );
203             }
204         }
205         else
206         {
207             languageVersion = language.getDefaultVersion();
208         }
209         LOG.debug( "Using language " + languageVersion );
210         configuration.setDefaultLanguageVersion( languageVersion );
211 
212         try
213         {
214             configuration.prependClasspath( request.getAuxClasspath() );
215         }
216         catch ( IOException e )
217         {
218             throw new MavenReportException( e.getMessage(), e );
219         }
220         if ( request.getSuppressMarker() != null )
221         {
222             configuration.setSuppressMarker( request.getSuppressMarker() );
223         }
224         if ( request.getAnalysisCacheLocation() != null )
225         {
226             configuration.setAnalysisCacheLocation( request.getAnalysisCacheLocation() );
227             LOG.debug( "Using analysis cache location: " + request.getAnalysisCacheLocation() );
228         }
229         else
230         {
231             configuration.setIgnoreIncrementalAnalysis( true );
232         }
233 
234         configuration.setRuleSets( request.getRulesets() );
235         if ( request.getBenchmarkOutputLocation() != null )
236         {
237             configuration.setBenchmark( true );
238         }
239         List<File> files = request.getFiles();
240         List<DataSource> dataSources = new ArrayList<>( files.size() );
241         for ( File f : files )
242         {
243             dataSources.add( new FileDataSource( f ) );
244         }
245 
246         PmdCollectingRenderer renderer = new PmdCollectingRenderer();
247 
248         if ( StringUtils.isBlank( request.getRulesets() ) )
249         {
250             LOG.debug( "Skipping PMD execution as no rulesets are defined." );
251         }
252         else
253         {
254             if ( request.getBenchmarkOutputLocation() != null )
255             {
256                 TimeTracker.startGlobalTracking();
257             }
258 
259             try
260             {
261                 processFilesWithPMD( configuration, dataSources, renderer );
262             }
263             finally
264             {
265                 if ( request.getAuxClasspath() != null )
266                 {
267                     ClassLoader classLoader = configuration.getClassLoader();
268                     if ( classLoader instanceof Closeable )
269                     {
270                         IOUtils.closeQuietly( (Closeable) classLoader );
271                     }
272                 }
273                 if ( request.getBenchmarkOutputLocation() != null )
274                 {
275                     TimingReport timingReport = TimeTracker.stopGlobalTracking();
276                     writeBenchmarkReport( timingReport, request.getBenchmarkOutputLocation(),
277                             request.getOutputEncoding() );
278                 }
279             }
280         }
281 
282         if ( renderer.hasErrors() )
283         {
284             if ( !request.isSkipPmdError() )
285             {
286                 LOG.error( "PMD processing errors:" );
287                 LOG.error( renderer.getErrorsAsString( request.isDebugEnabled() ) );
288                 throw new MavenReportException( "Found " + renderer.getErrors().size() + " PMD processing errors" );
289             }
290             LOG.warn( "There are {} PMD processing errors:", renderer.getErrors().size() );
291             LOG.warn( renderer.getErrorsAsString( request.isDebugEnabled() ) );
292         }
293 
294         removeExcludedViolations( renderer.getViolations() );
295 
296         Report report = renderer.asReport();
297         // always write XML report, as this might be needed by the check mojo
298         // we need to output it even if the file list is empty or we have no violations
299         // so the "check" goals can check for violations
300         writeXmlReport( report );
301 
302         // write any other format except for xml and html. xml has just been produced.
303         // html format is produced by the maven site formatter. Excluding html here
304         // avoids using PMD's own html formatter, which doesn't fit into the maven site
305         // considering the html/css styling
306         String format = request.getFormat();
307         if ( !"html".equals( format ) && !"xml".equals( format ) )
308         {
309             writeFormattedReport( report );
310         }
311 
312         return new PmdResult( new File( request.getTargetDirectory(), "pmd.xml" ), request.getOutputEncoding() );
313     }
314 
315     private void writeBenchmarkReport( TimingReport timingReport, String benchmarkOutputLocation, String encoding )
316     {
317         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( benchmarkOutputLocation ), encoding ) )
318         {
319             final TimingReportRenderer renderer = new TextTimingReportRenderer();
320             renderer.render( timingReport, writer );
321         }
322         catch ( IOException e )
323         {
324             LOG.error( "Unable to generate benchmark file: {}", benchmarkOutputLocation, e );
325         }
326     }
327 
328     private void processFilesWithPMD( PMDConfiguration pmdConfiguration, List<DataSource> dataSources,
329             PmdCollectingRenderer renderer ) throws MavenReportException
330     {
331         RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.createFactory(
332                 RulePriority.valueOf( request.getMinimumPriority() ), true, true );
333         try
334         {
335             // load the ruleset once to log out any deprecated rules as warnings
336             ruleSetFactory.createRuleSets( pmdConfiguration.getRuleSets() );
337         }
338         catch ( RuleSetNotFoundException e1 )
339         {
340             throw new MavenReportException( "The ruleset could not be loaded", e1 );
341         }
342 
343         try
344         {
345             LOG.debug( "Executing PMD..." );
346             RuleContext ruleContext = new RuleContext();
347             PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext,
348                     Arrays.<Renderer>asList( renderer ) );
349 
350             LOG.debug( "PMD finished. Found {} violations.", renderer.getViolations().size() );
351         }
352         catch ( Exception e )
353         {
354             String message = "Failure executing PMD: " + e.getLocalizedMessage();
355             if ( !request.isSkipPmdError() )
356             {
357                 throw new MavenReportException( message, e );
358             }
359             LOG.warn( message, e );
360         }
361     }
362 
363     /**
364      * Use the PMD XML renderer to create the XML report format used by the
365      * check mojo later on.
366      *
367      * @param report
368      * @throws MavenReportException
369      */
370     private void writeXmlReport( Report report ) throws MavenReportException
371     {
372         File targetFile = writeReport( report, new XMLRenderer( request.getOutputEncoding() ), "xml" );
373         if ( request.isIncludeXmlInSite() )
374         {
375             File siteDir = new File( request.getReportOutputDirectory() );
376             siteDir.mkdirs();
377             try
378             {
379                 FileUtils.copyFile( targetFile, new File( siteDir, "pmd.xml" ) );
380             }
381             catch ( IOException e )
382             {
383                 throw new MavenReportException( e.getMessage(), e );
384             }
385         }
386     }
387 
388     private File writeReport( Report report, Renderer r, String extension ) throws MavenReportException
389     {
390         if ( r == null )
391         {
392             return null;
393         }
394 
395         File targetDir = new File( request.getTargetDirectory() );
396         targetDir.mkdirs();
397         File targetFile = new File( targetDir, "pmd." + extension );
398         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ),
399                 request.getOutputEncoding() ) )
400         {
401             r.setWriter( writer );
402             r.start();
403             r.renderFileReport( report );
404             r.end();
405             r.flush();
406         }
407         catch ( IOException ioe )
408         {
409             throw new MavenReportException( ioe.getMessage(), ioe );
410         }
411 
412         return targetFile;
413     }
414 
415     /**
416      * Use the PMD renderers to render in any format aside from HTML and XML.
417      *
418      * @param report
419      * @throws MavenReportException
420      */
421     private void writeFormattedReport( Report report )
422             throws MavenReportException
423     {
424         Renderer renderer = createRenderer( request.getFormat(), request.getOutputEncoding() );
425         writeReport( report, renderer, request.getFormat() );
426     }
427 
428     /**
429      * Create and return the correct renderer for the output type.
430      *
431      * @return the renderer based on the configured output
432      * @throws org.apache.maven.reporting.MavenReportException
433      *             if no renderer found for the output type
434      */
435     public static Renderer createRenderer( String format, String outputEncoding ) throws MavenReportException
436     {
437         Renderer result = null;
438         if ( "xml".equals( format ) )
439         {
440             result = new XMLRenderer( outputEncoding );
441         }
442         else if ( "txt".equals( format ) )
443         {
444             result = new TextRenderer();
445         }
446         else if ( "csv".equals( format ) )
447         {
448             result = new CSVRenderer();
449         }
450         else if ( "html".equals( format ) )
451         {
452             result = new HTMLRenderer();
453         }
454         else if ( !"".equals( format ) && !"none".equals( format ) )
455         {
456             try
457             {
458                 result = (Renderer) Class.forName( format ).getConstructor().newInstance();
459             }
460             catch ( Exception e )
461             {
462                 throw new MavenReportException(
463                         "Can't find PMD custom format " + format + ": " + e.getClass().getName(), e );
464             }
465         }
466 
467         return result;
468     }
469 
470     private void removeExcludedViolations( List<RuleViolation> violations )
471             throws MavenReportException
472     {
473         ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
474 
475         try
476         {
477             excludeFromFile.loadExcludeFromFailuresData( request.getExcludeFromFailureFile() );
478         }
479         catch ( MojoExecutionException e )
480         {
481             throw new MavenReportException( "Unable to load exclusions", e );
482         }
483 
484         LOG.debug( "Removing excluded violations. Using {} configured exclusions.",
485                 excludeFromFile.countExclusions() );
486         int violationsBefore = violations.size();
487 
488         Iterator<RuleViolation> iterator = violations.iterator();
489         while ( iterator.hasNext() )
490         {
491             RuleViolation rv = iterator.next();
492             if ( excludeFromFile.isExcludedFromFailure( rv ) )
493             {
494                 iterator.remove();
495             }
496         }
497 
498         int numberOfExcludedViolations = violationsBefore - violations.size();
499         LOG.debug( "Excluded {} violations.", numberOfExcludedViolations );
500     }
501 }