diff --git a/docs/maven-plugin.md b/docs/maven-plugin.md index c69b02e5..9628e450 100644 --- a/docs/maven-plugin.md +++ b/docs/maven-plugin.md @@ -116,10 +116,10 @@ Parameters: | **spotbugsRuleset** | String | Relative path to the XML that specifies the bug detectors which should be run. If not set the default file will be used| | **spotbugsInclude** | String | Relative path to the XML that specifies the bug instances that will be included in the report. If not set the default file will be used| | **spotbugsExclude** | String | Relative path to the XML that specifies the bug instances that will be excluded from the report. If not set the default file will be used| -| **maven.spotbugs.version** | String | The version of the spotbugs-maven-plugin that will be used (default value is **3.1.6**)| -| **spotbugs.version** | String | The version of SpotBugs that will be used (default value is **3.1.7**)| +| **maven.spotbugs.version** | String | The version of the spotbugs-maven-plugin that will be used (default value is **3.1.12.2**)| +| **spotbugs.version** | String | The version of SpotBugs that will be used (default value is **3.1.12**)| | **spotbugsPlugins** | List | A list with artifacts that contain additional detectors/patterns for SpotBugs | -| **findbugs.slf4j.version** | String | The version of the findbugs-slf4j plugin that will be used (default value is **1.2.4**)| +| **findbugs.slf4j.version** | String | The version of the findbugs-slf4j plugin that will be used (default value is **1.5.0**)| ### sat-plugin:report diff --git a/pom.xml b/pom.xml index c6a9b7f3..fcffb500 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ 0.9.0-SNAPSHOT pom - Static Code Analysis Tool Parent Pom + Static Code Analysis Tool Parent POM Tool that aggregates reports of PMD, Checkstyle and FindBugs https://github.com/openhab/static-code-analysis @@ -21,8 +21,8 @@ - openhab.org - http://www.openhab.org + openHAB.org + https://www.openhab.org @@ -37,7 +37,7 @@ git@github.com:openhab/static-code-analysis.git - github + GitHub https://github.com/openhab/static-code-analysis/issues @@ -51,20 +51,21 @@ 2.4 6.7.0 8.12 - 3.1.7 - 3.1.1 + 3.1.12 + 3.5.0 + 3.5.0 3.5 3.5 3.6.1 - 2.2.0 + 2.3.0 2.4.0 1.7.1 3.11.1 - 0.6.1 + 0.8.0 2.1.0 0.28.6 2.12.4 - 9.3.14.v20161028 + 9.4.20.v20190813 9.1.0.8 @@ -186,7 +187,7 @@ 2.4.3-01 - + org.apache.maven.plugins maven-source-plugin @@ -227,7 +228,7 @@ - + junit @@ -262,9 +263,10 @@ test - + custom-checks + sat-extension sat-plugin codestyle diff --git a/sat-extension/pom.xml b/sat-extension/pom.xml new file mode 100644 index 00000000..da8636d9 --- /dev/null +++ b/sat-extension/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + + org.openhab.tools.sat + pom + 0.9.0-SNAPSHOT + + + sat-extension + + Static Code Analysis Tool Maven Extension + Generates HTML summaries from analysis results + + + + + org.apache.maven + maven-core + ${maven.core.version} + provided + + + + + org.openhab.tools.sat + sat-plugin + ${project.version} + + + + + net.sourceforge.saxon + saxon + ${saxon.version} + + + + + + + org.codehaus.plexus + plexus-component-metadata + 1.7.1 + + + + generate-metadata + generate-test-metadata + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.codehaus.plexus + plexus-component-metadata + [1.7.1,) + + generate-metadata + generate-test-metadata + + + + + + + + + + + + + + + diff --git a/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportExecutionListener.java b/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportExecutionListener.java new file mode 100644 index 00000000..eeefcfb7 --- /dev/null +++ b/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportExecutionListener.java @@ -0,0 +1,265 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.tools.analysis.report; + +import static org.openhab.tools.analysis.report.ReportUtil.*; + +import java.io.File; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.maven.execution.AbstractExecutionListener; +import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.execution.ExecutionListener; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.PluginParameterExpressionEvaluator; +import org.apache.maven.plugin.descriptor.Parameter; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.annotations.Component; +import org.codehaus.plexus.component.annotations.Requirement; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; +import org.codehaus.plexus.logging.Logger; + +/** + * Listens to Maven executions, delegates events to the default {@link ExecutionListener} and updates HTML summaries + * based on SAT plugin configuration values. + * + * @author Wouter Born - Initial contribution + */ +@Component(role = SummaryReportExecutionListener.class) +public class SummaryReportExecutionListener extends AbstractExecutionListener { + + @Requirement + private Logger logger; + + @Requirement + private SummaryReportHtmlGenerator summaryReportHtmlGenerator; + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + /** + * The default ExecutionListener + */ + private ExecutionListener listener; + + /** + * Maps each directory for which a summary report needs to be generated to a {@link SummaryUpdater}. + */ + private final Map summaryUpdaters = Collections.synchronizedMap(new HashMap<>()); + + /** + * Updates HTML summaries in a directory based on SAT plugin configuration values. + */ + private class SummaryUpdater { + private final String directory; + private final SummaryHtmlGeneration htmlGeneration; + private final int htmlGenerationPeriod; + private Instant lastUpdate = Instant.MIN; + + public SummaryUpdater(String directory, SummaryHtmlGeneration htmlGeneration, int htmlGenerationPeriod) { + this.directory = directory; + this.htmlGeneration = htmlGeneration; + this.htmlGenerationPeriod = htmlGenerationPeriod; + } + + private File update() { + File latestSummaryReport = summaryReportHtmlGenerator.generateHtmlSummaryByRules(directory); + lastUpdate = Instant.now(); + return latestSummaryReport; + } + + public void incrementalUpdate() { + executor.submit(() -> { + if (htmlGeneration == SummaryHtmlGeneration.CONTINUOUS + || (htmlGeneration == SummaryHtmlGeneration.PERIODIC + && Instant.now().isAfter(lastUpdate.plusSeconds(htmlGenerationPeriod)))) { + File latestSummaryReport = update(); + + if (logger.isDebugEnabled()) { + logger.debug("Updated static code analysis summary report in:"); + logger.debug(latestSummaryReport.toURI().toString()); + } + } + }); + } + + public void finalUpdate() { + if (htmlGeneration != SummaryHtmlGeneration.NEVER) { + File latestSummaryReport = update(); + + if (latestSummaryReport != null) { + logger.info("Static code analysis summary report is available in:"); + logger.info(latestSummaryReport.toURI().toString()); + } + } + } + } + + public void chainListener(MavenSession session) { + listener = session.getRequest().getExecutionListener(); + session.getRequest().setExecutionListener(this); + } + + private void generateSummaryReportForExecution(ExecutionEvent event) { + try { + getOrCreateSummaryUpdater(event).incrementalUpdate(); + } catch (ExpressionEvaluationException e) { + logger.error("Exception while evaluating '" + DIRECTORY_PARAMETER + "' plugin parameter", e); + } + } + + private SummaryUpdater getOrCreateSummaryUpdater(ExecutionEvent event) throws ExpressionEvaluationException { + String directory = getPluginParameterValue(event, DIRECTORY_PARAMETER); + SummaryUpdater summaryUpdater; + synchronized (summaryUpdaters) { + summaryUpdater = summaryUpdaters.get(directory); + if (summaryUpdater == null) { + SummaryHtmlGeneration htmlGeneration = SummaryHtmlGeneration + .valueOf(getPluginParameterValue(event, HTML_GENERATION_PARAMETER).toUpperCase()); + int htmlGenerationPeriod = Integer + .parseInt(getPluginParameterValue(event, HTML_GENERATION_PERIOD_PARAMETER)); + + summaryUpdater = new SummaryUpdater(directory, htmlGeneration, htmlGenerationPeriod); + summaryUpdaters.put(directory, summaryUpdater); + } + } + return summaryUpdater; + } + + private String getPluginParameterValue(ExecutionEvent event, String parameterName) + throws ExpressionEvaluationException { + Parameter parameter = event.getMojoExecution().getMojoDescriptor().getParameterMap().get(parameterName); + + PluginParameterExpressionEvaluator evaluator = getEvaluator(event); + String parameterValue = (String) evaluator.evaluate(parameter.getExpression()); + if (parameterValue == null) { + parameterValue = (String) evaluator.evaluate(parameter.getDefaultValue()); + } + return parameterValue; + } + + public void generateFinalSummaryReports() { + synchronized (summaryUpdaters) { + summaryUpdaters.values().forEach(SummaryUpdater::finalUpdate); + } + } + + private PluginParameterExpressionEvaluator getEvaluator(ExecutionEvent event) { + PluginParameterExpressionEvaluator evaluator; + MavenSession session = event.getSession(); + MavenProject currentProject = session.getCurrentProject(); + // Maven 3: PluginParameterExpressionEvaluator gets the current project from the session: + // synchronize in case another thread wants to fetch the real current project in between + synchronized (session) { + session.setCurrentProject(event.getProject()); + evaluator = new PluginParameterExpressionEvaluator(session, event.getMojoExecution()); + session.setCurrentProject(currentProject); + } + return evaluator; + } + + // These overrides make sure the original listener still receives all events + + @Override + public void projectDiscoveryStarted(ExecutionEvent event) { + listener.projectDiscoveryStarted(event); + } + + @Override + public void sessionStarted(ExecutionEvent event) { + listener.sessionStarted(event); + } + + @Override + public void sessionEnded(ExecutionEvent event) { + listener.sessionEnded(event); + } + + @Override + public void projectSkipped(ExecutionEvent event) { + listener.projectSkipped(event); + } + + @Override + public void projectStarted(ExecutionEvent event) { + listener.projectStarted(event); + } + + @Override + public void projectSucceeded(ExecutionEvent event) { + listener.projectSucceeded(event); + } + + @Override + public void projectFailed(ExecutionEvent event) { + listener.projectFailed(event); + } + + @Override + public void forkStarted(ExecutionEvent event) { + listener.forkStarted(event); + } + + @Override + public void forkSucceeded(ExecutionEvent event) { + listener.forkSucceeded(event); + } + + @Override + public void forkFailed(ExecutionEvent event) { + listener.forkFailed(event); + } + + @Override + public void mojoSkipped(ExecutionEvent event) { + listener.mojoSkipped(event); + } + + @Override + public void mojoStarted(ExecutionEvent event) { + listener.mojoStarted(event); + } + + @Override + public void mojoSucceeded(ExecutionEvent event) { + listener.mojoSucceeded(event); + if (isReportExecution(event)) { + generateSummaryReportForExecution(event); + } + } + + @Override + public void mojoFailed(ExecutionEvent event) { + listener.mojoFailed(event); + } + + @Override + public void forkedProjectStarted(ExecutionEvent event) { + listener.forkedProjectStarted(event); + } + + @Override + public void forkedProjectSucceeded(ExecutionEvent event) { + listener.forkedProjectSucceeded(event); + } + + @Override + public void forkedProjectFailed(ExecutionEvent event) { + listener.forkedProjectFailed(event); + } + +} diff --git a/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportHtmlGenerator.java b/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportHtmlGenerator.java new file mode 100644 index 00000000..5e35a227 --- /dev/null +++ b/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportHtmlGenerator.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.tools.analysis.report; + +import static org.openhab.tools.analysis.report.ReportUtil.*; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLockInterruptionException; +import java.text.MessageFormat; +import java.time.Duration; +import java.time.Instant; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.codehaus.plexus.component.annotations.Component; +import org.codehaus.plexus.component.annotations.Requirement; +import org.codehaus.plexus.logging.Logger; + +import com.google.common.io.Files; + +import net.sf.saxon.TransformerFactoryImpl; + +/** + * Generates HTML report summaries based on the the content in the merge XML file using XSLT. + * + * @author Wouter Born - Initial contribution + */ +@Component(role = SummaryReportHtmlGenerator.class) +public class SummaryReportHtmlGenerator { + + @Requirement + private Logger logger; + + private TransformerFactory transformerFactory; + + private ClassLoader contextClassLoader; + + void initialize() { + contextClassLoader = Thread.currentThread().getContextClassLoader(); + transformerFactory = TransformerFactory.newInstance(TransformerFactoryImpl.class.getName(), contextClassLoader); + } + + File generateHtmlSummaryByRules(final String summaryReportDirectory) { + File latestMergeResult = new File(summaryReportDirectory, MERGE_XML_FILE_NAME); + File latestMergeResultCopy = new File(summaryReportDirectory, SUMMARY_XML_FILE_NAME); + + FileChannel mergeLockFileChannel = null; + FileChannel summaryLockFileChannel = null; + + try { + // Acquire the merge and summary locks + mergeLockFileChannel = acquireFileLock(summaryReportDirectory, MERGE_LOCK_FILE_NAME); + if (!latestMergeResult.exists()) { + return null; + } + summaryLockFileChannel = acquireFileLock(summaryReportDirectory, SUMMARY_LOCK_FILE_NAME); + + // Copy merge.xml to summary.xml which is used for generating the report + Files.copy(latestMergeResult, latestMergeResultCopy); + + // Release the merge lock so plugin reporting goals executed in parallel can keep merging + closeFileChannel(mergeLockFileChannel); + + File latestSummaryReport = new File(summaryReportDirectory, SUMMARY_REPORT_FILE_NAME); + run(CREATE_HTML_XSLT, latestMergeResultCopy, latestSummaryReport); + + if (!latestMergeResultCopy.delete()) { + logger.error("Unable to delete file: " + latestMergeResultCopy.getAbsolutePath()); + } + + return latestSummaryReport; + } catch (InterruptedException | FileLockInterruptionException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + throw new IllegalStateException("Exception while acquiring lock file", e); + } finally { + closeFileChannel(mergeLockFileChannel); + closeFileChannel(summaryLockFileChannel); + } + return null; + } + + private void closeFileChannel(FileChannel fileChannel) { + if (fileChannel != null) { + try { + fileChannel.close(); + } catch (IOException e) { + logger.error("Exception while closing file channel: " + fileChannel, e); + } + } + } + + private FileChannel acquireFileLock(final String summaryReportDirectory, final String fileName) + throws InterruptedException, FileLockInterruptionException { + File mergeLockFile = new File(summaryReportDirectory, fileName); + try { + return ReportUtil.acquireFileLock(mergeLockFile); + } catch (IOException e) { + throw new IllegalStateException("Exception while acquiring lock file: " + mergeLockFile, e); + } + } + + private void run(final String xslt, final File input, final File output) { + try (FileOutputStream outputStream = new FileOutputStream(output)) { + if (logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("{0} > {1} > {2}", input, xslt, output)); + } + + // Process the Source into a Transformer Object + final InputStream inputStream = contextClassLoader.getResourceAsStream(xslt); + final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + final StreamSource source = new StreamSource(reader); + + final Transformer transformer = transformerFactory.newTransformer(source); + + final StreamResult outputTarget = new StreamResult(outputStream); + final StreamSource xmlSource = new StreamSource(input); + + // Transform the XML Source to a Result + Instant start = Instant.now(); + transformer.transform(xmlSource, outputTarget); + Instant end = Instant.now(); + + if (logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Transformation '{0}' took {1}ms", xslt, + Duration.between(start, end).toMillis())); + } + } catch (IOException e) { + logger.error("IOException occurred", e); + } catch (TransformerException e) { + logger.error("TransformerException occurred", e); + } + } + +} diff --git a/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportLifecycleParticipant.java b/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportLifecycleParticipant.java new file mode 100644 index 00000000..39730c3c --- /dev/null +++ b/sat-extension/src/main/java/org/openhab/tools/analysis/report/SummaryReportLifecycleParticipant.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.tools.analysis.report; + +import org.apache.maven.AbstractMavenLifecycleParticipant; +import org.apache.maven.MavenExecutionException; +import org.apache.maven.execution.MavenSession; +import org.codehaus.plexus.component.annotations.Component; +import org.codehaus.plexus.component.annotations.Requirement; + +/** + * Handles {@link MavenSession} events so the SAT extension can generate HTML report summaries. + * + * @author Wouter Born - Initial contribution + */ +@Component(role = AbstractMavenLifecycleParticipant.class) +public class SummaryReportLifecycleParticipant extends AbstractMavenLifecycleParticipant { + + @Requirement + private SummaryReportExecutionListener summaryReportExecutionListener; + + @Requirement + private SummaryReportHtmlGenerator summaryReportHtmlGenerator; + + @Override + public void afterProjectsRead(MavenSession session) throws MavenExecutionException { + // The context class loader of the current thread needs to be used with the + // SummaryReportHtmlGenerator or it will not find the XSLT files on the class path + summaryReportHtmlGenerator.initialize(); + summaryReportExecutionListener.chainListener(session); + } + + @Override + public void afterSessionEnd(MavenSession session) throws MavenExecutionException { + summaryReportExecutionListener.generateFinalSummaryReports(); + } +} diff --git a/sat-plugin/pom.xml b/sat-plugin/pom.xml index 6720f328..975efc77 100644 --- a/sat-plugin/pom.xml +++ b/sat-plugin/pom.xml @@ -58,7 +58,7 @@ - + maven-plugin-plugin ${maven.plugin.plugin.version} @@ -78,7 +78,7 @@ - + - + diff --git a/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportUtility.java b/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportMojo.java similarity index 83% rename from sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportUtility.java rename to sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportMojo.java index 1616edcd..fa92abe8 100644 --- a/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportUtility.java +++ b/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportMojo.java @@ -35,6 +35,8 @@ */ package org.openhab.tools.analysis.report; +import static org.openhab.tools.analysis.report.ReportUtil.*; + import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -42,10 +44,13 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; +import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.Path; import java.text.MessageFormat; import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; import java.util.Date; import java.util.LinkedList; import java.util.Queue; @@ -88,14 +93,14 @@ * MarkusSprunck/static- * code-analysis-report * - * @author Markus Sprunck - Initial Implementation + * @author Markus Sprunck - Initial contribution * @author Svilen Valkanov - Some minor changes and adaptations * @author Petar Valchev - Changed the logging to be parameterized * @author Martin van Wingerden - added maven console logging of all messages + * @author Wouter Born - Synchronize summary updates to make Mojo thread-safe */ - -@Mojo(name = "report") -public class ReportUtility extends AbstractMojo { +@Mojo(name = "report", threadSafe = true) +public class ReportMojo extends AbstractMojo { /** * The directory where the individual report will be generated @@ -116,30 +121,14 @@ public class ReportUtility extends AbstractMojo { @Parameter(property = "report.summary.targetDir", defaultValue = "${session.executionRootDirectory}/target") private File summaryReportDirectory; - private static final String REPORT_SUBDIR = "report"; - @Parameter(property = "report.in.maven", defaultValue = "true") private boolean reportInMaven; - // XSLT files that are used to create the merged report, located in the resources folder - private static final String CREATE_HTML_XSLT = REPORT_SUBDIR + "/create_html.xslt"; - private static final String MERGE_XSLT = REPORT_SUBDIR + "/merge.xslt"; - private static final String PREPARE_PMD_XSLT = REPORT_SUBDIR + "/prepare_pmd.xslt"; - private static final String PREPARE_CHECKSTYLE_XSLT = REPORT_SUBDIR + "/prepare_checkstyle.xslt"; - private static final String PREPARE_FINDBUGS_XSLT = REPORT_SUBDIR + "/prepare_findbugs.xslt"; - - private static final String SUMMARY_TEMPLATE_FILE_NAME = "summary.html"; + @Parameter(property = "report.summary.html.generation", defaultValue = "PERIODIC") + private SummaryHtmlGeneration summaryHtmlGeneration; - // Input files that contain the reports of the different tools - private static final String PMD_INPUT_FILE_NAME = "pmd.xml"; - private static final String CHECKSTYLE_INPUT_FILE_NAME = "checkstyle-result.xml"; - private static final String FINDBUGS_INPUT_FILE_NAME = "spotbugsXml.xml"; - - // Name of the file that contains the merged report - public static final String RESULT_FILE_NAME = "report.html"; - public static final String SUMMARY_REPORT_FILE_NAME = "summary_report.html"; - public static final String SUMMARY_BUNLES_FILE_NAME = "summary_bundles.html"; - private static final String EMPTY = ""; + @Parameter(property = "report.summary.html.generation.period", defaultValue = "60") + private int summaryHtmlGenerationPeriod; private TransformerFactory transformerFactory; @@ -217,8 +206,16 @@ public void execute() throws MojoFailureException { // 7. Append the individual report to the summary, if it is not empty if (summaryReportDirectory != null) { ensureSummaryReportDirectoryExists(); - generateSummaryByBundle(htmlOutputFileName, mergedReport); - generateSummaryByRules(htmlOutputFileName, mergedReport); + File mergeLockFile = new File(summaryReportDirectory, MERGE_LOCK_FILE_NAME); + try (FileChannel fileChannel = acquireFileLock(mergeLockFile)) { + generateSummaryByBundle(htmlOutputFileName, mergedReport); + generateSummaryByRules(htmlOutputFileName, mergedReport); + } catch (IOException e) { + throw new MojoFailureException("Exception while acquiring merge lock file: " + mergeLockFile, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } } // 8. Report errors and warnings in Maven @@ -228,7 +225,7 @@ public void execute() throws MojoFailureException { // 9. Fail the build if the option is enabled and high priority warnings are found if (failOnError) { - checkForErrors(mergedReport, htmlOutputFileName); + failOnErrors(mergedReport); } // 10. Delete the temporary files @@ -239,9 +236,10 @@ public void execute() throws MojoFailureException { } private void run(final String xslt, final File input, final File output, final String param, final File value) { - FileOutputStream outputStream = null; - try { - getLog().debug(MessageFormat.format("{0} > {1} {2} {3} > {4}", input, xslt, param, value, output)); + try (FileOutputStream outputStream = new FileOutputStream(output)) { + if (getLog().isDebugEnabled()) { + getLog().debug(MessageFormat.format("{0} > {1} {2} {3} > {4}", input, xslt, param, value, output)); + } // Process the Source into a Transformer Object final InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(xslt); @@ -254,24 +252,22 @@ private void run(final String xslt, final File input, final File output, final S transformer.setParameter(param, value.toURI().toURL()); } - outputStream = new FileOutputStream(output); final StreamResult outputTarget = new StreamResult(outputStream); final StreamSource xmlSource = new StreamSource(input); // Transform the XML Source to a Result + Instant start = Instant.now(); transformer.transform(xmlSource, outputTarget); + Instant end = Instant.now(); + + if (getLog().isDebugEnabled()) { + getLog().debug(MessageFormat.format("Transformation '{0}' took {1}ms", xslt, + Duration.between(start, end).toMillis())); + } } catch (IOException e) { - getLog().error("IOException occcurred ", e); + getLog().error("IOException occurred", e); } catch (TransformerException e) { - getLog().error("TransformerException occcurred ", e); - } finally { - if (null != outputStream) { - try { - outputStream.close(); - } catch (final IOException e) { - getLog().error(e.getMessage()); - } - } + getLog().error("TransformerException occurred", e); } } @@ -293,7 +289,7 @@ private void reportWarningsAndErrors(File mergedReport, File reportLocation) { return; } - String format = String.format("Code Analysis Tool has found: \n %d error(s)! \n %d warning(s) \n %d info(s)", + String format = String.format("Code Analysis Tool has found: %n %d error(s)! %n %d warning(s) %n %d info(s)", errorCount, warnCount, infoCount); report(maxLevel(errorCount, warnCount, infoCount), format); @@ -308,12 +304,12 @@ private void reportWarningsAndErrors(File mergedReport, File reportLocation) { String line = messageNode.getAttribute("line"); String message = messageNode.getAttribute("message").trim(); - String logTemplate = "%s:[%s]\n%s"; + String logTemplate = "%s:[%s]%n%s"; String log = String.format(logTemplate, fileName, line, message); report(priority, log); } } - getLog().info("Detailed report can be found at: file:///" + reportLocation); + getLog().info("Detailed report can be found at: " + reportLocation.toURI()); } private String maxLevel(int errorCount, int warnCount, int infoCount) { @@ -342,14 +338,13 @@ private int countPriority(NodeList messages, String priority) { return count; } - private void checkForErrors(File secondMergeResult, File reportLocation) throws MojoFailureException { - int numberOfErrors = selectNodes(secondMergeResult, "/sca/file/message[@priority=1]").getLength(); - - if (numberOfErrors > 0) { + private void failOnErrors(File mergedReport) throws MojoFailureException { + int errorCount = selectNodes(mergedReport, "/sca/file/message[@priority=1]").getLength(); + if (errorCount > 0) { throw new MojoFailureException(String.format( - "\n" + "Code Analysis Tool has found %d error(s)! \n" - + "Please fix the errors and rerun the build. \n", - selectNodes(secondMergeResult, "/sca/file/message[@priority=1]").getLength())); + "%n" + "Code Analysis Tool has found %d error(s)! %n" + + "Please fix the errors and rerun the build. %n", + selectNodes(mergedReport, "/sca/file/message[@priority=1]").getLength())); } } @@ -373,8 +368,8 @@ private void ensureSummaryReportDirectoryExists() { } } - private void generateSummaryByBundle(File htmlOutputFileName, File secondMergeResult) { - NodeList nodes = selectNodes(secondMergeResult, "/sca/file/message"); + private void generateSummaryByBundle(File htmlOutputFile, File mergedReport) { + NodeList nodes = selectNodes(mergedReport, "/sca/file/message"); int messagesNumber = nodes.getLength(); if (messagesNumber == 0) { getLog().info("Empty report will not be appended to the summary report."); @@ -382,7 +377,7 @@ private void generateSummaryByBundle(File htmlOutputFileName, File secondMergeRe } try { - File summaryReport = new File(summaryReportDirectory, SUMMARY_BUNLES_FILE_NAME); + File summaryReport = new File(summaryReportDirectory, SUMMARY_BUNDLES_FILE_NAME); if (!summaryReport.exists()) { InputStream inputStream = Thread.currentThread().getContextClassLoader() .getResourceAsStream(REPORT_SUBDIR + "/" + SUMMARY_TEMPLATE_FILE_NAME); @@ -399,7 +394,7 @@ private void generateSummaryByBundle(File htmlOutputFileName, File secondMergeRe String reportContent = FileUtils.readFileToString(summaryReport); final String singleItem = "%s"; - Path absoluteIndividualReportPath = htmlOutputFileName.toPath(); + Path absoluteIndividualReportPath = htmlOutputFile.toPath(); Path summaryReportDirectoryPath = summaryReportDirectory.toPath(); Path relativePath = summaryReportDirectoryPath.relativize(absoluteIndividualReportPath); @@ -417,7 +412,7 @@ private void generateSummaryByBundle(File htmlOutputFileName, File secondMergeRe } private void generateSummaryByRules(final File htmlOutputFileName, final File mergedReport) { - File latestMergeResult = new File(summaryReportDirectory, "old_Merge.xml"); + File latestMergeResult = new File(summaryReportDirectory, MERGE_XML_FILE_NAME); File latestSummaryReport = new File(summaryReportDirectory, SUMMARY_REPORT_FILE_NAME); try { @@ -427,10 +422,9 @@ private void generateSummaryByRules(final File htmlOutputFileName, final File me Files.copy(mergedReport, latestMergeResult); Files.copy(htmlOutputFileName, latestSummaryReport); } else { - final File tempMergedReport = new File(summaryReportDirectory, "temp_Merge.xml"); + final File tempMergedReport = new File(summaryReportDirectory, MERGE_XML_TMP_FILE_NAME); Files.copy(latestMergeResult, tempMergedReport); run(MERGE_XSLT, tempMergedReport, latestMergeResult, "with", mergedReport); - run(CREATE_HTML_XSLT, latestMergeResult, latestSummaryReport, EMPTY, null); deleteFile(tempMergedReport); } } catch (IOException e) { @@ -455,4 +449,5 @@ private NodeList selectNodes(File file, String xPathExpression) { return new EmptyNodeList(); } } + } diff --git a/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportUtil.java b/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportUtil.java new file mode 100644 index 00000000..b1e37b9a --- /dev/null +++ b/sat-plugin/src/main/java/org/openhab/tools/analysis/report/ReportUtil.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.tools.analysis.report; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.OverlappingFileLockException; + +import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.plugin.MojoExecution; + +/** + * Provides constants and utility methods used for generating reports. + * + * @author Wouter Born - Initial contribution + */ +final class ReportUtil { + + // SAT plugin GAV parameters + static final String SAT_PLUGIN_GROUP_ID = "org.openhab.tools.sat"; + static final String SAT_PLUGIN_ARTIFACT_ID = "sat-plugin"; + static final String SAT_PLUGIN_REPORT_GOAL = "report"; + + // SAT plugin configuration parameter names + static final String DIRECTORY_PARAMETER = "summaryReportDirectory"; + static final String HTML_GENERATION_PARAMETER = "summaryHtmlGeneration"; + static final String HTML_GENERATION_PERIOD_PARAMETER = "summaryHtmlGenerationPeriod"; + + // XSLT files that are used to create the merged report, located in the resources folder + static final String REPORT_SUBDIR = "report"; + static final String CREATE_HTML_XSLT = REPORT_SUBDIR + "/create_html.xslt"; + static final String MERGE_XSLT = REPORT_SUBDIR + "/merge.xslt"; + static final String PREPARE_PMD_XSLT = REPORT_SUBDIR + "/prepare_pmd.xslt"; + static final String PREPARE_CHECKSTYLE_XSLT = REPORT_SUBDIR + "/prepare_checkstyle.xslt"; + static final String PREPARE_FINDBUGS_XSLT = REPORT_SUBDIR + "/prepare_findbugs.xslt"; + + static final String SUMMARY_TEMPLATE_FILE_NAME = "summary.html"; + + // Input files that contain the reports of the different tools + static final String PMD_INPUT_FILE_NAME = "pmd.xml"; + static final String CHECKSTYLE_INPUT_FILE_NAME = "checkstyle-result.xml"; + static final String FINDBUGS_INPUT_FILE_NAME = "spotbugsXml.xml"; + + // Name of the file that contains the merged report + static final String RESULT_FILE_NAME = "report.html"; + static final String SUMMARY_LOCK_FILE_NAME = "summary.lock"; + static final String SUMMARY_REPORT_FILE_NAME = "summary_report.html"; + static final String SUMMARY_BUNDLES_FILE_NAME = "summary_bundles.html"; + static final String SUMMARY_XML_FILE_NAME = "summary.xml"; + static final String EMPTY = ""; + + // Files used for merging the individual reports into the summary reports + static final String MERGE_LOCK_FILE_NAME = "merge.lock"; + static final String MERGE_XML_FILE_NAME = "merge.xml"; + static final String MERGE_XML_TMP_FILE_NAME = "merge.xml.tmp"; + + private ReportUtil() { + // Hidden utility class constructor + } + + @SuppressWarnings("resource") + static FileChannel acquireFileLock(File file) throws IOException, InterruptedException { + FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); + try { + while (!Thread.currentThread().isInterrupted()) { + try { + // Blocking wait until file lock is acquired + channel.lock(); + return channel; + } catch (OverlappingFileLockException e) { + // Another thread is locking the same file + Thread.sleep(100L); + } + } + } catch (InterruptedException | IOException e) { + channel.close(); + throw e; + } + // Unreachable code added for completeness because exceptions should already have been thrown + channel.close(); + throw new InterruptedException("Interrupted while waiting for lock on: " + file); + } + + static boolean isReportExecution(ExecutionEvent event) { + MojoExecution execution = event.getMojoExecution(); + return SAT_PLUGIN_GROUP_ID.equals(execution.getGroupId()) + && SAT_PLUGIN_ARTIFACT_ID.equals(execution.getArtifactId()) + && SAT_PLUGIN_REPORT_GOAL.equals(execution.getGoal()); + } +} diff --git a/sat-plugin/src/main/java/org/openhab/tools/analysis/report/SummaryHtmlGeneration.java b/sat-plugin/src/main/java/org/openhab/tools/analysis/report/SummaryHtmlGeneration.java new file mode 100644 index 00000000..f08f1d93 --- /dev/null +++ b/sat-plugin/src/main/java/org/openhab/tools/analysis/report/SummaryHtmlGeneration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.tools.analysis.report; + +/** + * Enumerates the HTML summary generation plugin configuration parameters. + * + * @author Wouter Born - Initial contribution + */ +enum SummaryHtmlGeneration { + CONTINUOUS, + PERIODIC, + ONCE, + NEVER +} diff --git a/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/CheckstyleChecker.java b/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/CheckstyleChecker.java index f50fc464..bb33b2df 100644 --- a/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/CheckstyleChecker.java +++ b/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/CheckstyleChecker.java @@ -31,10 +31,9 @@ * maven-checkstyle- * plugin with a predefined ruleset file and configuration properties * - * @author Svilen Valkanov - * + * @author Svilen Valkanov - Initial contribution */ -@Mojo(name = "checkstyle", requiresDependencyResolution = ResolutionScope.COMPILE) +@Mojo(name = "checkstyle", requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true) public class CheckstyleChecker extends AbstractChecker { /** diff --git a/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/PmdChecker.java b/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/PmdChecker.java index bb39dbcc..0b3380ca 100644 --- a/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/PmdChecker.java +++ b/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/PmdChecker.java @@ -32,10 +32,9 @@ * maven-pmd-plugin with * a predefined ruleset file and configuration properties * - * @author Svilen Valkanov - * + * @author Svilen Valkanov - Initial contribution */ -@Mojo(name = "pmd", requiresDependencyResolution = ResolutionScope.COMPILE) +@Mojo(name = "pmd", requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true) public class PmdChecker extends AbstractChecker { private static final String DEFAULT_RULESET_XML = "rulesets/pmd/rules.xml"; @@ -102,7 +101,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { element("targetDirectory", userProps.getProperty("pmd.custom.targetDirectory")), element("compileSourceRoots", userProps.getProperty("pmd.custom.compileSourceRoots")), element("rulesets", element("ruleset", defaultRulesetLocation), - element("ruleset", customRulesetLocation))); + element("ruleset", customRulesetLocation))); pmdPlugins.add(dependency("org.openhab.tools.sat.custom-checks", "pmd", plugin.getVersion())); pmdPlugins.add(dependency("net.sourceforge.pmd", "pmd-core", PMD_VERSION)); pmdPlugins.add(dependency("net.sourceforge.pmd", "pmd-java", PMD_VERSION)); diff --git a/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/SpotBugsChecker.java b/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/SpotBugsChecker.java index 885d83dc..e93240fb 100644 --- a/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/SpotBugsChecker.java +++ b/sat-plugin/src/main/java/org/openhab/tools/analysis/tools/SpotBugsChecker.java @@ -54,11 +54,9 @@ * SpotBugs is fully backward compatible with FindBugs. *

* - * @author Svilen Valkanov - * + * @author Svilen Valkanov - Initial contribution */ - -@Mojo(name = "spotbugs", requiresDependencyResolution = ResolutionScope.COMPILE) +@Mojo(name = "spotbugs", requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true) public class SpotBugsChecker extends AbstractChecker { /** @@ -85,19 +83,19 @@ public class SpotBugsChecker extends AbstractChecker { /** * The version of the spotbugs-maven-plugin that will be used */ - @Parameter(property = "maven.spotbugs.version", defaultValue = "3.1.6") + @Parameter(property = "maven.spotbugs.version", defaultValue = "3.1.12.2") private String spotbugsMavenPluginVersion; /** * The version of the findbugs-slf4j plugin that will be used */ - @Parameter(property = "findbugs.slf4j.version", defaultValue = "1.2.4") + @Parameter(property = "findbugs.slf4j.version", defaultValue = "1.5.0") private String findBugsSlf4jPluginVersion; /** * The version of the spotbugs that will be used */ - @Parameter(property = "spotbugs.version", defaultValue = "3.1.7") + @Parameter(property = "spotbugs.version", defaultValue = "3.1.12") private String spotBugsVersion; /** @@ -217,7 +215,8 @@ private String getVisitorsString(String externalLocation) { getLog().warn("Unable to find file " + resolvedPath.toString()); } } else { - stream = this.getClass().getClassLoader().getResourceAsStream(SpotBugsChecker.DEFAULT_VISITORS_XML); } + stream = this.getClass().getClassLoader().getResourceAsStream(SpotBugsChecker.DEFAULT_VISITORS_XML); + } // Serialize the content JAXBContext context; diff --git a/sat-plugin/src/main/resources/configuration/spotbugs.properties b/sat-plugin/src/main/resources/configuration/spotbugs.properties index 314a7945..e3e08703 100644 --- a/sat-plugin/src/main/resources/configuration/spotbugs.properties +++ b/sat-plugin/src/main/resources/configuration/spotbugs.properties @@ -5,7 +5,7 @@ spotbugs.effort=Min spotbugs.xmlOutput=true spotbugs.failOnError=false spotbugs.report.dir=${project.build.directory}/code-analysis -spotbugs.fork=false +spotbugs.fork=true outputEncoding=UTF-8 spotbugs.onlyAnalyze=org.openhab.-,org.eclipse.smarthome.- spotbugs.nested=false diff --git a/sat-plugin/src/test/java/org/openhab/tools/analysis/report/test/ReportUtilityTest.java b/sat-plugin/src/test/java/org/openhab/tools/analysis/report/ReportMojoTest.java similarity index 59% rename from sat-plugin/src/test/java/org/openhab/tools/analysis/report/test/ReportUtilityTest.java rename to sat-plugin/src/test/java/org/openhab/tools/analysis/report/ReportMojoTest.java index 3408e026..2fbb5bc0 100644 --- a/sat-plugin/src/test/java/org/openhab/tools/analysis/report/test/ReportUtilityTest.java +++ b/sat-plugin/src/test/java/org/openhab/tools/analysis/report/ReportMojoTest.java @@ -10,7 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.tools.analysis.report.test; +package org.openhab.tools.analysis.report; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.verify; +import static org.openhab.tools.analysis.report.ReportUtil.RESULT_FILE_NAME; + +import java.io.File; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; @@ -19,40 +25,32 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.openhab.tools.analysis.report.ReportUtility; - -import java.io.File; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** - * Tests for the {@link ReportUtility} + * Tests for the {@link ReportMojo} * - * @author Svilen Valkanov - Initial contributation + * @author Svilen Valkanov - Initial contribution * @author Martin van Wingerden - added logging of all messages */ @RunWith(MockitoJUnitRunner.class) -public class ReportUtilityTest { +public class ReportMojoTest { private static final String TARGET_RELATIVE_DIR = "target" + File.separator + "test-classes" + File.separator + "report"; private static final String TARGET_ABSOLUTE_DIR = System.getProperty("user.dir") + File.separator + TARGET_RELATIVE_DIR; - private static final String RESULT_FILE_PATH = TARGET_ABSOLUTE_DIR + File.separator + ReportUtility.RESULT_FILE_NAME; + private static final String RESULT_FILE_PATH = TARGET_ABSOLUTE_DIR + File.separator + RESULT_FILE_NAME; @Mock private Log logger; - private ReportUtility subject; + private ReportMojo subject; private File resultFile = new File(RESULT_FILE_PATH); @Before public void setUp() throws Exception { - subject = new ReportUtility(); + subject = new ReportMojo(); subject.setLog(logger); if (resultFile.exists()) { @@ -99,16 +97,23 @@ public void assertWarningAreLoggedWhileExecuting() throws MojoFailureException { subject.execute(); - verify(logger).warn("org.sprunck.bee.Bee.java:[31]\norg.sprunck.bee.Bee defines clone() but doesn't implement Cloneable"); + verify(logger).warn( + "org.sprunck.bee.Bee.java:[31]\norg.sprunck.bee.Bee defines clone() but doesn't implement Cloneable"); verify(logger).warn("org.sprunck.bee.Bee.java:[31]\norg.sprunck.bee.Bee.clone() may return null"); - verify(logger).warn("org.sprunck.foo.Foo.java:[35]\nThe method name org.sprunck.foo.Foo.Went() doesn't start with a lower case letter"); + verify(logger).warn( + "org.sprunck.foo.Foo.java:[35]\nThe method name org.sprunck.foo.Foo.Went() doesn't start with a lower case letter"); verify(logger).error("Code Analysis Tool has found: \n 2 error(s)! \n 3 warning(s) \n 3 info(s)"); - verify(logger).error("org.sprunck.bee.Bee.java:[19]\norg.sprunck.bee.Bee.toString() ignores return value of String.concat(String)"); - verify(logger).error("org.eclipse.smarthome.auth.jaas.internal.JaasAuthenticationProvider.java:[69]\nComment matches to-do format '(TODO)|(FIXME)'."); - verify(logger).debug("org.sprunck.bee.Bee.java:[19]\nAn operation on an Immutable object (String, BigDecimal or BigInteger) won't change the object itself"); - verify(logger).debug("org.sprunck.foo.Foo.java:[36]\nDo not use if statements that are always true or always false"); - verify(logger).debug("C:\\prj\\openHAB\\EclipseIDE\\git\\smarthome\\bundles\\automation\\org.eclipse.smarthome.automation.module.core\\ESH-INF\\automation\\moduletypes\\EventTriggersTypeDefinition.json:[0]\n" + - "File does not end with a newline."); - verify(logger).info("Detailed report can be found at: file:///" + RESULT_FILE_PATH); + verify(logger).error( + "org.sprunck.bee.Bee.java:[19]\norg.sprunck.bee.Bee.toString() ignores return value of String.concat(String)"); + verify(logger).error( + "org.eclipse.smarthome.auth.jaas.internal.JaasAuthenticationProvider.java:[69]\nComment matches to-do format '(TODO)|(FIXME)'."); + verify(logger).debug( + "org.sprunck.bee.Bee.java:[19]\nAn operation on an Immutable object (String, BigDecimal or BigInteger) won't change the object itself"); + verify(logger) + .debug("org.sprunck.foo.Foo.java:[36]\nDo not use if statements that are always true or always false"); + verify(logger).debug( + "C:\\prj\\openHAB\\EclipseIDE\\git\\smarthome\\bundles\\automation\\org.eclipse.smarthome.automation.module.core\\ESH-INF\\automation\\moduletypes\\EventTriggersTypeDefinition.json:[0]\n" + + "File does not end with a newline."); + verify(logger).info("Detailed report can be found at: " + new File(RESULT_FILE_PATH).toURI()); } } diff --git a/tools/suppressions.xml b/tools/suppressions.xml index b3ad9c2f..031c8db4 100644 --- a/tools/suppressions.xml +++ b/tools/suppressions.xml @@ -5,5 +5,5 @@ - +