XDocsReporter.groovy

/*
 * Copyright 2005-2025 the original author or authors.
 *
 * Licensed 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
 *
 *     https://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.codehaus.mojo.spotbugs

import edu.umd.cs.findbugs.Version

import groovy.xml.slurpersupport.GPathResult
import groovy.xml.slurpersupport.NodeChild
import groovy.xml.StreamingMarkupBuilder
import java.nio.charset.Charset

import org.apache.maven.plugin.logging.Log

/**
 * The reporter controls the generation of the Spotbugs report.
 */
class XDocsReporter {

    /** The key to get the value if the line number is not available. */
    static final String NOLINE_KEY = 'report.spotbugs.noline'

    /** The bundle to get the messages from. */
    ResourceBundle bundle

    /** The logger to write logs to. */
    Log log

    /** The threshold of bugs severity. */
    String threshold

    /** The used effort for searching bugs. */
    String effort

    /** The output Writer stream. */
    Writer outputWriter

    /** Spotbugs Results. */
    GPathResult spotbugsResults

    /** Bug Classes. */
    List<String> bugClasses = []

    /** The directories containing the sources to be compiled. */
    List<String> compileSourceRoots = []

    /** The directories containing the test sources to be compiled. */
    List<String> testSourceRoots = []

    /** The output encoding. */
    Charset outputEncoding

    /**
     * Default constructor.
     *
     * @param bundle
     *            The Resource Bundle to use
     */
    XDocsReporter(ResourceBundle bundle, Log log, String threshold, String effort, String outputEncoding) {
        if (!bundle || !log || !threshold || !effort || !outputEncoding) {
            throw new IllegalArgumentException('All constructor arguments must be provided')
        }

        this.bundle = bundle
        this.log = log
        this.threshold = threshold
        this.effort = effort
        this.outputEncoding = Charset.forName(outputEncoding)
    }

    /**
     * Returns the threshold string value for the integer input.
     *
     * @param thresholdValue
     *            The ThresholdValue integer to evaluate.
     * @return The string valueof the Threshold object.
     */
    protected String evaluateThresholdParameter(String thresholdValue) {
        switch (thresholdValue) {
            case '1': return 'High'
            case '2': return 'Normal'
            case '3': return 'Low'
            case '4': return 'Exp'
            case '5': return 'Ignore'
            default: return 'Invalid Priority'
        }
    }

    /**
     * Gets the Spotbugs Version of the report.
     *
     * @return The Spotbugs Version used on the report.
     */
    protected String getSpotBugsVersion() {
        return Version.VERSION_STRING
    }

    public void generateReport() {
        StreamingMarkupBuilder xmlBuilder = new StreamingMarkupBuilder()
        xmlBuilder.encoding = outputEncoding.name()

        outputWriter << xmlBuilder.bind {
            mkp.xmlDeclaration()
            if (log.isDebugEnabled()) {
                log.debug("generateReport spotbugsResults is ${spotbugsResults}")
            }

            BugCollection(version: getSpotBugsVersion(), threshold: SpotBugsInfo.spotbugsThresholds[threshold],
                    effort: SpotBugsInfo.spotbugsEfforts[effort]) {

                if (log.isDebugEnabled()) {
                    log.debug("spotbugsResults.FindBugsSummary total_bugs is ${spotbugsResults.FindBugsSummary.@total_bugs.text()}")
                }

                spotbugsResults.FindBugsSummary.PackageStats.ClassStats.each() { NodeChild classStats ->

                    String classStatsValue = classStats.'@class'.text()
                    String classStatsBugCount = classStats.'@bugs'.text()

                    if (log.isDebugEnabled()) {
                        log.debug('classStats...')
                        log.debug("classStatsValue is ${classStatsValue}")
                        log.debug("classStatsBugCount is ${classStatsBugCount}")
                    }

                    if (Integer.parseInt(classStatsBugCount) > 0) {
                        bugClasses << classStatsValue
                    }
                }

                bugClasses.each() { String bugClass ->
                    if (log.isDebugEnabled()) {
                        log.debug("finish bugClass is ${bugClass}")
                    }
                    file(classname: bugClass) {
                        spotbugsResults.BugInstance.each() { NodeChild bugInstance ->

                            if (bugInstance.Class.find{ NodeChild classNode -> classNode.@primary == "true" }.@classname.text() != bugClass) {
                                return
                            }

                            String type = bugInstance.@type.text()
                            String category = bugInstance.@category.text()
                            String message = bugInstance.LongMessage.text()
                            String priority = evaluateThresholdParameter(bugInstance.@priority.text())
                            String line = bugInstance.SourceLine.@start[0].text()
                            if (log.isDebugEnabled()) {
                                log.debug("BugInstance message is ${message}")
                            }

                            BugInstance(type: type, priority: priority, category: category, message: message,
                                    lineNumber: ((line) ? line: "-1"))
                        }
                    }
                }

                log.debug("Printing Errors")

                Error() {
                    spotbugsResults.Error.analysisError.each() { NodeChild analysisError ->
                        AnalysisError(analysisError.message.text())
                    }

                    log.debug("Printing Missing classes")

                    spotbugsResults.Error.MissingClass.each() { NodeChild missingClass ->
                        MissingClass(missingClass.text)
                    }
                }

                Project() {
                    log.debug("Printing Source Roots")

                    if (!compileSourceRoots.isEmpty()) {
                        compileSourceRoots.each() { String srcDir ->
                            if (log.isDebugEnabled()) {
                                log.debug("SrcDir is ${srcDir}")
                            }
                            SrcDir(srcDir)
                        }
                    }

                    if (!testSourceRoots.isEmpty()) {
                        testSourceRoots.each() { String srcDir ->
                            if (log.isDebugEnabled()) {
                                log.debug("SrcDir is ${srcDir}")
                            }
                            SrcDir(srcDir)
                        }
                    }
                }
            }
        }

        outputWriter.close()
    }
}