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