SpotBugsGui.groovy

/*
 * SPDX-License-Identifier: Apache-2.0
 * See LICENSE file for details.
 *
 * Copyright 2005-2026 the original author or authors.
 */
package org.codehaus.mojo.spotbugs

import java.awt.GraphicsEnvironment
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import javax.inject.Inject

import org.apache.maven.artifact.Artifact
import org.apache.maven.execution.MavenSession
import org.apache.maven.plugin.AbstractMojo
import org.apache.maven.plugins.annotations.Mojo
import org.apache.maven.plugins.annotations.Parameter
import org.apache.maven.plugins.annotations.ResolutionScope
import org.apache.maven.toolchain.Toolchain
import org.apache.maven.toolchain.ToolchainManager
import org.codehaus.plexus.resource.ResourceManager

/**
 * Launch the Spotbugs GUI.
 * It will use all the parameters in the POM fle.
 *
 * @since 2.0
 *
 * @description Launch the Spotbugs GUI using the parameters in the POM fle.
 */
@Mojo(name = 'gui', requiresDependencyResolution = ResolutionScope.TEST, requiresProject = true)
class SpotBugsGui extends AbstractMojo implements SpotBugsPluginsTrait {

    /** Locale to use for Resource bundle. */
    static Locale locale = Locale.getDefault()

    /** Directory containing the class files for Spotbugs to analyze. */
    @Parameter(defaultValue = '${project.build.outputDirectory}', required = true)
    File classFilesDirectory

    /** Turn on Spotbugs debugging. */
    @Parameter(defaultValue = 'false', property = 'spotbugs.debug')
    boolean debug

    /** List of artifacts this plugin depends on. Used for resolving the Spotbugs core plugin. */
    @Parameter(property = 'plugin.artifacts', readonly = true, required = true)
    List<Artifact> pluginArtifacts

    /** Effort of the bug finders. Valid values are Min, Default and Max. */
    @Parameter(defaultValue = 'Default', property = 'spotbugs.effort')
    String effort

    /** The plugin list to include in the report. This is a SpotbugsInfo.COMMA-delimited list. */
    @Parameter(property = 'spotbugs.pluginList')
    String pluginList

    /**
     * Collection of PluginArtifact to work on. (PluginArtifact contains groupId, artifactId, version, type, classifier.)
     * See <a href="./usage.html#Using Detectors from a Repository">Usage</a> for details.
     *
     * @since 2.4.1
     * @since 4.8.3.0 includes classifier
     */
    @Parameter
    List<PluginArtifact> plugins

    /** Artifact resolver, needed to download the plugin jars. */
    @Inject
    org.eclipse.aether.RepositorySystem repositorySystem

    /** Used to look up Artifacts in the remote repository. */
    @Inject
    org.apache.maven.repository.RepositorySystem factory

    /** Toolchain manager used to retrieve the JDK toolchain. */
    @Inject
    ToolchainManager toolchainManager

    /** Maven Session. */
    @Parameter (defaultValue = '${session}', readonly = true, required = true)
    MavenSession session

    /** Specifies the directory where the Spotbugs native xml output will be generated. */
    @Parameter(defaultValue = '${project.build.directory}', required = true)
    File spotbugsXmlOutputDirectory

    /**
     * Set the name of the output XML file produced
     *
     * @since 3.1.12.2
     */
    @Parameter(defaultValue = 'spotbugsXml.xml', property = 'spotbugs.outputXmlFilename')
    String spotbugsXmlOutputFilename

    /**
     * The file encoding to use when reading the source files. If the property <code>project.build.sourceEncoding</code>
     * is not set, the platform default encoding is used. <strong>Note:</strong> This parameter always overrides the
     * property <code>charset</code> from Checkstyle's <code>TreeWalker</code> module.
     *
     * @since 2.2
     */
    @Parameter(defaultValue = '${project.build.sourceEncoding}', property = 'encoding')
    String encoding

    /**
     * Maximum Java heap size in megabytes  (default=512).
     *
     * @since 2.2
     */
    @Parameter(defaultValue = '512', property = 'spotbugs.maxHeap')
    int maxHeap

    /**
     * Resource Manager.
     *
     * @since 2.0
     */
    @Inject
    ResourceManager resourceManager

    @Override
    void execute() {

        List<String> auxClasspathElements = session.getCurrentProject().compileClasspathElements

        if (debug && log.isInfoEnabled()) {
            log.info("  Plugin Artifacts to be added -> ${pluginArtifacts}")
        }

        Charset effectiveEncoding
        if (encoding) {
            effectiveEncoding = Charset.forName(encoding)
        } else {
            effectiveEncoding = Charset.defaultCharset() ?: StandardCharsets.UTF_8
        }
        if (log.isInfoEnabled()) {
            log.info('File Encoding is ' + effectiveEncoding.name())
        }

        if (GraphicsEnvironment.isHeadless()) {
            log.warn('Skipping SpotBugs GUI launch in headless environment')
            return
        }

        // options must be added before the spotbugsXml path
        List<String> spotbugsArgs = []
        spotbugsArgs << getEffortParameter()
        if (pluginList || plugins) {
            spotbugsArgs << '-pluginList'
            spotbugsArgs << getSpotbugsPlugins()
        }

        Path spotbugsXml = spotbugsXmlOutputDirectory.toPath().resolve(spotbugsXmlOutputFilename)
        if (Files.exists(spotbugsXml)) {
            if (log.isDebugEnabled()) {
                log.debug("  Found an SpotBugs XML at -> ${spotbugsXml}")
            }
            spotbugsArgs << spotbugsXml.toString()
        }

        Toolchain toolchain = toolchainManager?.getToolchainFromBuildContext('jdk', session)

        String javaExecutable = 'java'

        if (toolchain != null) {
            String toolchainPath = toolchain.findTool('java')
            if (toolchainPath != null) {
                log.info("Toolchain in spotbugs-maven-plugin: ${toolchain}")
                javaExecutable = toolchainPath
            }
        }

        // Build classpath
        List<String> classpathElements = pluginArtifacts.collect { Artifact pluginArtifact -> pluginArtifact.file.getAbsolutePath() }
        String classpath = classpathElements.join(File.pathSeparator)

        // Build command
        List<String> command = []
        command << javaExecutable
        command << "-Xmx${maxHeap}m"
        command << "-Dfile.encoding=${effectiveEncoding.name()}"
        command << '-Dfindbugs.launchUI=gui2'
        command << '-cp'
        command << classpath
        command << 'edu.umd.cs.findbugs.LaunchAppropriateUI'
        command.addAll(spotbugsArgs)

        List<String> sanitizedCommand = command.collect(Object::toString)

        if (debug && log.isInfoEnabled()) {
            log.info("Launching SpotBugs GUI with command: ${sanitizedCommand}")
        }

        ProcessBuilder pb = new ProcessBuilder(sanitizedCommand)
        pb.directory(spotbugsXmlOutputDirectory)
        pb.redirectErrorStream(true)
        Process process = pb.start()
        process.inputStream.withReader(effectiveEncoding.name()) { reader ->
            reader.eachLine { line ->
                if (log.isInfoEnabled()) {
                    log.info(line)
                }
            }
        }
        int exitCode = process.waitFor()
        if (exitCode != 0) {
            log.error("SpotBugs GUI exited with code: ${exitCode}")
        }
    }
}