blob: 8e96bf411b0e8f288a954101f48d86efb164b6bd [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.plugins.pmd.exec;
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 net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.PmdAnalysis;
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;
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;
/**
* 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;
}
}