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