blob: cc25c80eaef080f953633150ca35e4f7a6ea82ff [file] [log] [blame]
package org.apache.maven.plugins.pmd.exec;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.pmd.ExcludeViolationsFromFile;
import org.apache.maven.reporting.MavenReportException;
import org.codehaus.plexus.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sourceforge.pmd.PmdAnalysis;
import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.RulePriority;
import net.sourceforge.pmd.RuleSetLoadException;
import net.sourceforge.pmd.RuleSetLoader;
import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.benchmark.TextTimingReportRenderer;
import net.sourceforge.pmd.benchmark.TimeTracker;
import net.sourceforge.pmd.benchmark.TimingReport;
import net.sourceforge.pmd.benchmark.TimingReportRenderer;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.renderers.CSVRenderer;
import net.sourceforge.pmd.renderers.HTMLRenderer;
import net.sourceforge.pmd.renderers.Renderer;
import net.sourceforge.pmd.renderers.TextRenderer;
import net.sourceforge.pmd.renderers.XMLRenderer;
import net.sourceforge.pmd.util.Predicate;
/**
* Executes PMD with the configuration provided via {@link PmdRequest}.
*/
public class PmdExecutor extends Executor
{
private static final Logger LOG = LoggerFactory.getLogger( PmdExecutor.class );
public static PmdResult execute( PmdRequest request ) throws MavenReportException
{
if ( request.getJavaExecutable() != null )
{
return fork( request );
}
// make sure the class loaders are correct and call this in the same JVM
ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
try
{
Thread.currentThread().setContextClassLoader( PmdExecutor.class.getClassLoader() );
PmdExecutor executor = new PmdExecutor( request );
return executor.run();
}
finally
{
Thread.currentThread().setContextClassLoader( origLoader );
}
}
private static PmdResult fork( PmdRequest request )
throws MavenReportException
{
File basePmdDir = new File ( request.getTargetDirectory(), "pmd" );
basePmdDir.mkdirs();
File pmdRequestFile = new File( basePmdDir, "pmdrequest.bin" );
try ( ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( pmdRequestFile ) ) )
{
out.writeObject( request );
}
catch ( IOException e )
{
throw new MavenReportException( e.getMessage(), e );
}
String classpath = buildClasspath();
ProcessBuilder pb = new ProcessBuilder();
// note: using env variable instead of -cp cli arg to avoid length limitations under Windows
pb.environment().put( "CLASSPATH", classpath );
pb.command().add( request.getJavaExecutable() );
pb.command().add( PmdExecutor.class.getName() );
pb.command().add( pmdRequestFile.getAbsolutePath() );
LOG.debug( "Executing: CLASSPATH={}, command={}", classpath, pb.command() );
try
{
final Process p = pb.start();
// Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
// and inheritIO would only inherit file handles, not the changed streams.
ProcessStreamHandler.start( p.getInputStream(), System.out );
ProcessStreamHandler.start( p.getErrorStream(), System.err );
int exit = p.waitFor();
LOG.debug( "PmdExecutor exit code: {}", exit );
if ( exit != 0 )
{
throw new MavenReportException( "PmdExecutor exited with exit code " + exit );
}
return new PmdResult( new File( request.getTargetDirectory(), "pmd.xml" ), request.getOutputEncoding() );
}
catch ( IOException e )
{
throw new MavenReportException( e.getMessage(), e );
}
catch ( InterruptedException e )
{
Thread.currentThread().interrupt();
throw new MavenReportException( e.getMessage(), e );
}
}
/**
* Execute PMD analysis from CLI.
*
* <p>
* Single arg with the filename to the serialized {@link PmdRequest}.
*
* <p>
* Exit-code: 0 = success, 1 = failure in executing
*
* @param args
*/
public static void main( String[] args )
{
File requestFile = new File( args[0] );
try ( ObjectInputStream in = new ObjectInputStream( new FileInputStream( requestFile ) ) )
{
PmdRequest request = (PmdRequest) in.readObject();
PmdExecutor pmdExecutor = new PmdExecutor( request );
pmdExecutor.setupLogLevel( request.getLogLevel() );
pmdExecutor.run();
System.exit( 0 );
}
catch ( IOException | ClassNotFoundException | MavenReportException e )
{
LOG.error( e.getMessage(), e );
}
System.exit( 1 );
}
private final PmdRequest request;
public PmdExecutor( PmdRequest request )
{
this.request = Objects.requireNonNull( request );
}
private PmdResult run() throws MavenReportException
{
setupPmdLogging( request.isShowPmdLog(), request.getLogLevel() );
PMDConfiguration configuration = new PMDConfiguration();
LanguageVersion languageVersion = null;
Language language = LanguageRegistry
.findLanguageByTerseName( request.getLanguage() != null ? request.getLanguage() : "java" );
if ( language == null )
{
throw new MavenReportException( "Unsupported language: " + request.getLanguage() );
}
if ( request.getLanguageVersion() != null )
{
languageVersion = language.getVersion( request.getLanguageVersion() );
if ( languageVersion == null )
{
throw new MavenReportException( "Unsupported targetJdk value '" + request.getLanguageVersion() + "'." );
}
}
else
{
languageVersion = language.getDefaultVersion();
}
LOG.debug( "Using language " + languageVersion );
configuration.setDefaultLanguageVersion( languageVersion );
if ( request.getSourceEncoding() != null )
{
configuration.setSourceEncoding( request.getSourceEncoding() );
}
configuration.prependAuxClasspath( request.getAuxClasspath() );
if ( request.getSuppressMarker() != null )
{
configuration.setSuppressMarker( request.getSuppressMarker() );
}
if ( request.getAnalysisCacheLocation() != null )
{
configuration.setAnalysisCacheLocation( request.getAnalysisCacheLocation() );
LOG.debug( "Using analysis cache location: " + request.getAnalysisCacheLocation() );
}
else
{
configuration.setIgnoreIncrementalAnalysis( true );
}
configuration.setRuleSets( request.getRulesets() );
configuration.setMinimumPriority( RulePriority.valueOf( request.getMinimumPriority() ) );
if ( request.getBenchmarkOutputLocation() != null )
{
configuration.setBenchmark( true );
}
List<File> files = request.getFiles();
Report report = null;
if ( request.getRulesets().isEmpty() )
{
LOG.debug( "Skipping PMD execution as no rulesets are defined." );
}
else
{
if ( request.getBenchmarkOutputLocation() != null )
{
TimeTracker.startGlobalTracking();
}
try
{
report = processFilesWithPMD( configuration, files );
}
finally
{
if ( request.getAuxClasspath() != null )
{
ClassLoader classLoader = configuration.getClassLoader();
if ( classLoader instanceof Closeable )
{
Closeable closeable = (Closeable) classLoader;
try
{
closeable.close();
}
catch ( IOException ex )
{
// ignore
}
}
}
if ( request.getBenchmarkOutputLocation() != null )
{
TimingReport timingReport = TimeTracker.stopGlobalTracking();
writeBenchmarkReport( timingReport, request.getBenchmarkOutputLocation(),
request.getOutputEncoding() );
}
}
}
if ( report != null && !report.getProcessingErrors().isEmpty() )
{
List<Report.ProcessingError> errors = report.getProcessingErrors();
if ( !request.isSkipPmdError() )
{
LOG.error( "PMD processing errors:" );
LOG.error( getErrorsAsString( errors, request.isDebugEnabled() ) );
throw new MavenReportException( "Found " + errors.size()
+ " PMD processing errors" );
}
LOG.warn( "There are {} PMD processing errors:", errors.size() );
LOG.warn( getErrorsAsString( errors, request.isDebugEnabled() ) );
}
report = removeExcludedViolations( report );
// always write XML report, as this might be needed by the check mojo
// we need to output it even if the file list is empty or we have no violations
// so the "check" goals can check for violations
writeXmlReport( report );
// write any other format except for xml and html. xml has just been produced.
// html format is produced by the maven site formatter. Excluding html here
// avoids using PMD's own html formatter, which doesn't fit into the maven site
// considering the html/css styling
String format = request.getFormat();
if ( !"html".equals( format ) && !"xml".equals( format ) )
{
writeFormattedReport( report );
}
return new PmdResult( new File( request.getTargetDirectory(), "pmd.xml" ), request.getOutputEncoding() );
}
/**
* Gets the errors as a single string. Each error is in its own line.
* @param withDetails if <code>true</code> then add the error details additionally (contains e.g. the stacktrace)
* @return the errors as string
*/
private String getErrorsAsString( List<Report.ProcessingError> errors, boolean withDetails )
{
List<String> errorsAsString = new ArrayList<>( errors.size() );
for ( Report.ProcessingError error : errors )
{
errorsAsString.add( error.getFile() + ": " + error.getMsg() );
if ( withDetails )
{
errorsAsString.add( error.getDetail() );
}
}
return String.join( System.lineSeparator(), errorsAsString );
}
private void writeBenchmarkReport( TimingReport timingReport, String benchmarkOutputLocation, String encoding )
{
try ( Writer writer = new OutputStreamWriter( new FileOutputStream( benchmarkOutputLocation ), encoding ) )
{
final TimingReportRenderer renderer = new TextTimingReportRenderer();
renderer.render( timingReport, writer );
}
catch ( IOException e )
{
LOG.error( "Unable to generate benchmark file: {}", benchmarkOutputLocation, e );
}
}
private Report processFilesWithPMD( PMDConfiguration pmdConfiguration, List<File> files )
throws MavenReportException
{
Report report = null;
RuleSetLoader rulesetLoader = RuleSetLoader.fromPmdConfig( pmdConfiguration )
.warnDeprecated( true );
try
{
// load the ruleset once to log out any deprecated rules as warnings
rulesetLoader.loadFromResources( pmdConfiguration.getRuleSetPaths() );
}
catch ( RuleSetLoadException e1 )
{
throw new MavenReportException( "The ruleset could not be loaded", e1 );
}
try ( PmdAnalysis pmdAnalysis = PmdAnalysis.create( pmdConfiguration ) )
{
for ( File file : files )
{
pmdAnalysis.files().addFile( file.toPath() );
}
LOG.debug( "Executing PMD..." );
report = pmdAnalysis.performAnalysisAndCollectReport();
LOG.debug( "PMD finished. Found {} violations.", report.getViolations().size() );
}
catch ( Exception e )
{
String message = "Failure executing PMD: " + e.getLocalizedMessage();
if ( !request.isSkipPmdError() )
{
throw new MavenReportException( message, e );
}
LOG.warn( message, e );
}
return report;
}
/**
* Use the PMD XML renderer to create the XML report format used by the
* check mojo later on.
*
* @param report
* @throws MavenReportException
*/
private void writeXmlReport( Report report ) throws MavenReportException
{
File targetFile = writeReport( report, new XMLRenderer( request.getOutputEncoding() ) );
if ( request.isIncludeXmlInSite() )
{
File siteDir = new File( request.getReportOutputDirectory() );
siteDir.mkdirs();
try
{
FileUtils.copyFile( targetFile, new File( siteDir, "pmd.xml" ) );
}
catch ( IOException e )
{
throw new MavenReportException( e.getMessage(), e );
}
}
}
private File writeReport( Report report, Renderer r ) throws MavenReportException
{
if ( r == null )
{
return null;
}
File targetDir = new File( request.getTargetDirectory() );
targetDir.mkdirs();
String extension = r.defaultFileExtension();
File targetFile = new File( targetDir, "pmd." + extension );
LOG.debug( "Target PMD output file: {}", targetFile );
try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ),
request.getOutputEncoding() ) )
{
r.setWriter( writer );
r.start();
if ( report != null )
{
r.renderFileReport( report );
}
r.end();
r.flush();
}
catch ( IOException ioe )
{
throw new MavenReportException( ioe.getMessage(), ioe );
}
return targetFile;
}
/**
* Use the PMD renderers to render in any format aside from HTML and XML.
*
* @param report
* @throws MavenReportException
*/
private void writeFormattedReport( Report report )
throws MavenReportException
{
Renderer renderer = createRenderer( request.getFormat(), request.getOutputEncoding() );
writeReport( report, renderer );
}
/**
* Create and return the correct renderer for the output type.
*
* @return the renderer based on the configured output
* @throws org.apache.maven.reporting.MavenReportException
* if no renderer found for the output type
*/
public static Renderer createRenderer( String format, String outputEncoding ) throws MavenReportException
{
LOG.debug( "Renderer requested: {}", format );
Renderer result = null;
if ( "xml".equals( format ) )
{
result = new XMLRenderer( outputEncoding );
}
else if ( "txt".equals( format ) )
{
result = new TextRenderer();
}
else if ( "csv".equals( format ) )
{
result = new CSVRenderer();
}
else if ( "html".equals( format ) )
{
result = new HTMLRenderer();
}
else if ( !"".equals( format ) && !"none".equals( format ) )
{
try
{
result = (Renderer) Class.forName( format ).getConstructor().newInstance();
}
catch ( Exception e )
{
throw new MavenReportException(
"Can't find PMD custom format " + format + ": " + e.getClass().getName(), e );
}
}
return result;
}
private Report removeExcludedViolations( Report report )
throws MavenReportException
{
if ( report == null )
{
return null;
}
ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
try
{
excludeFromFile.loadExcludeFromFailuresData( request.getExcludeFromFailureFile() );
}
catch ( MojoExecutionException e )
{
throw new MavenReportException( "Unable to load exclusions", e );
}
LOG.debug( "Removing excluded violations. Using {} configured exclusions.",
excludeFromFile.countExclusions() );
int violationsBefore = report.getViolations().size();
Report filtered = report.filterViolations( new Predicate<RuleViolation>()
{
@Override
public boolean test( RuleViolation ruleViolation )
{
return !excludeFromFile.isExcludedFromFailure( ruleViolation );
}
} );
int numberOfExcludedViolations = violationsBefore - filtered.getViolations().size();
LOG.debug( "Excluded {} violations.", numberOfExcludedViolations );
return filtered;
}
}