All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opalj.br.analyses.AnalysisApplication.scala Maven / Gradle / Ivy

The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package br
package analyses

import java.net.URL
import java.io.File
import scala.util.control.ControlThrowable
import com.typesafe.config.ConfigFactory
import com.typesafe.config.Config
import org.opalj.br.reader.Java9LibraryFramework
import org.opalj.log.OPALLogger
import org.opalj.log.OPALLogger.info
import org.opalj.log.OPALLogger.error
import org.opalj.log.GlobalLogContext
import org.opalj.log.LogContext
import org.opalj.log.LogMessage

import scala.collection.immutable.ArraySeq

/**
 * Provides the necessary infrastructure to easily execute a given analysis that
 * generates some analysis result that can be printed on the command line.
 *
 * To facilitate the usage of this trait several implicit conversions are defined that
 * wrap standard analyses ([[org.opalj.br.analyses]]) such that they report
 * results that are reportable.
 *
 * This class distinguishes between class files belonging to the code base under
 * analysis and those that belong to the libraries. Those belonging to the libraries
 * are loaded using the `ClassFileReader` for library classes (basically, all method
 * bodies are skipped [[org.opalj.br.reader.Java8LibraryFramework]]).
 * The parameter to specify library classes is `-libcp=`, the parameter to specify
 * the "normal" classpath is `-cp=`.
 *
 * ==Control Flow==
 *  1. The standard parameters are checked.
 *  1. The analysis is called to let it verify the analysis specific parameters.
 *  1. The [[Project]] is created.
 *  1. The `analyze` method of the [[Analysis]] is called with the project and the parameters.
 *  1. The results are printed.
 *
 * @author Michael Eichberg
 * @author Arne Lottmann
 */
trait AnalysisApplication {

    /**
     * The analysis that will be executed.
     *
     * The `analyze` method implemented by the analysis will be called after loading
     * all class files and creating a `Project`. Additionally,
     * all specified (additional) parameters are passed to the analyze method.
     */
    val analysis: Analysis[URL, ReportableAnalysisResult]

    /**
     * Describes the analysis specific parameters. An analysis specific parameter
     * has to start with a dash ("-") and has to contain an equals sign ("=").
     *
     * @note The parameter `-cp=` is already predefined (see general documentation).
     * @note The parameter `-library=` is already predefined (see general documentation).
     */
    def analysisSpecificParametersDescription: String = ""

    /**
     * Checks if the (additional) parameters are understood by the analysis.
     * If an error is found, a list of issues is returned and the analysis will not be executed.
     *
     * This method '''must be''' overridden if the analysis defines additional
     * parameters. A method that overrides this method should `return` the list of
     * issues if it can't validate all arguments.
     * The default behavior is to check that there are no additional parameters.
     */
    def checkAnalysisSpecificParameters(parameters: Seq[String]): Iterable[String] = {
        if (parameters.isEmpty) Nil else parameters.map("unknown parameter: "+_)
    }

    /**
     * Prints out general information how to use this analysis. Printed whenever
     * the set of specified parameters is not valid.
     */
    protected def printUsage(implicit logContext: LogContext): Unit = {
        info(
            "usage",
            "java "+
                this.getClass.getName+"\n"+
                "[-help (prints this help and exits)]\n"+
                "[-renderConfig (prints the configuration)]\n"+
                "[-cp= (Default: the current folder.)]\n"+
                "[-libcp=]\n"+
                "[-projectConfig=]\n"+
                "[-completelyLoadLibraries (the bodies of library methods are loaded)]\n"+
                analysisSpecificParametersDescription
        )
        info("general", "description: "+analysis.description)
        info("general", "copyright: "+analysis.copyright)
    }

    def main(args: Array[String]): Unit = {

        implicit val logContext: LogContext = GlobalLogContext

        def showError(message: String): Unit = error("project configuration", message)

        //
        // Categories of args
        //
        var unknownArgs = List.empty[String]
        var cp = IndexedSeq.empty[String]
        var libcp = IndexedSeq.empty[String]
        var projectConfig: Option[String] = None
        var completelyLoadLibraries = false
        var renderConfig = false

        //
        // 1. Process args
        //
        def splitCPath(path: String) = path.substring(path.indexOf('=') + 1).split(File.pathSeparator)
        def splitLibCPath(path: String) = path.substring(path.indexOf('=') + 1).split(File.pathSeparator)
        args.foreach { arg =>
            if (arg == "-help") {
                printUsage
                sys.exit(0)
            } else if (arg.startsWith("-cp=")) {
                cp ++= splitCPath(arg)
            } else if (arg.startsWith("-libcp=")) {
                libcp ++= splitLibCPath(arg)
            } else if (arg == "-completelyLoadLibraries") {
                completelyLoadLibraries = true
            } else if (arg.startsWith("-projectConfig=")) {
                projectConfig = Some(arg.substring(arg.indexOf('=') + 1))
            } else if (arg == "-renderConfig") {
                renderConfig = true
            } else {
                unknownArgs ::= arg
            }

        }

        //
        // 2. Check parsed args
        //
        // Input files must be either directories, or class/jar files.
        //
        def verifyFile(filename: String): Option[File] = {
            val file = new File(filename)

            def workingDirectory: String = {
                s"(working directory: ${System.getProperty("user.dir")})"
            }

            if (!file.exists) {
                showError(s"File does not exist: $file $workingDirectory.")
                None
            } else if (!file.canRead) {
                showError(s"Cannot read: $file $workingDirectory.")
                None
            } else if (!file.isDirectory &&
                !filename.endsWith(".jar") &&
                !filename.endsWith(".ear") &&
                !filename.endsWith(".war") &&
                !filename.endsWith(".zip") &&
                !filename.endsWith(".jmod") &&
                !filename.endsWith(".class")) {
                showError(s"Input file is neither a directory nor a class or JAR/JMod file: $file.")
                None
            } else
                Some(file)
        }

        def verifyFiles(filenames: IndexedSeq[String]): Seq[File] = filenames.flatMap(verifyFile)

        if (cp.isEmpty) cp = ArraySeq.unsafeWrapArray(Array(System.getProperty("user.dir")))
        info("project configuration", s"the classpath is ${cp.mkString}")
        val cpFiles = verifyFiles(cp)
        if (cpFiles.isEmpty) {
            showError("Nothing to analyze.")
            printUsage
            sys.exit(1)
        }

        val libcpFiles = verifyFiles(libcp)

        if (unknownArgs.nonEmpty)
            info("project configuration", "analysis specific parameters: "+unknownArgs.mkString(", "))
        val issues = checkAnalysisSpecificParameters(unknownArgs)
        if (issues.nonEmpty) {
            issues.foreach { i => error("project configuration", i) }
            printUsage
            sys.exit(2)
        }

        //
        // 3. Setup project context
        //
        val project: Project[URL] = try {
            val config =
                if (projectConfig.isEmpty)
                    ConfigFactory.load()
                else
                    ConfigFactory.load(projectConfig.get)
            setupProject(cpFiles, libcpFiles, completelyLoadLibraries, config)
        } catch {
            case ct: ControlThrowable => throw ct;
            case t: Throwable =>
                error("fatal", "setting up the project failed", t)
                printUsage
                sys.exit(2)
        }

        //
        // 4. execute analysis
        //

        if (renderConfig) {
            val effectiveConfiguration =
                "Effective configuration:\n"+org.opalj.util.renderConfig(project.config)
            info("project configuration", effectiveConfiguration)
        }

        info("info", "executing analysis: "+analysis.title+".")
        // TODO Add progressmanagement.
        val result = analysis.analyze(project, unknownArgs.toSeq, ProgressManagement.None)
        OPALLogger.log(LogMessage.plainInfo(result.toConsoleString))
    }

    protected def handleParsingExceptions(
        project:    SomeProject,
        exceptions: Iterable[Throwable]
    ): Unit = {
        if (exceptions.isEmpty)
            return ;

        implicit val logContext: LogContext = project.logContext
        for (exception <- exceptions) {
            error("creating project", "ignoring invalid class file", exception)
        }
    }

    def setupProject(
        cpFiles:                 Iterable[File],
        libcpFiles:              Iterable[File],
        completelyLoadLibraries: Boolean,
        configuredConfig:        Config
    )(
        implicit
        initialLogContext: LogContext
    ): Project[URL] = {
        info("creating project", "reading project class files")
        val JavaClassFileReader = Project.JavaClassFileReader(initialLogContext, configuredConfig)

        val (classFiles, exceptions1) =
            reader.readClassFiles(
                cpFiles,
                JavaClassFileReader.ClassFiles,
                file => info("creating project", "\tfile: "+file)
            )

        val (libraryClassFiles, exceptions2) = {
            if (libcpFiles.nonEmpty) {
                info("creating project", "reading library class files")
                reader.readClassFiles(
                    libcpFiles,
                    if (completelyLoadLibraries) {
                        JavaClassFileReader.ClassFiles
                    } else {
                        Java9LibraryFramework.ClassFiles
                    },
                    file => info("creating project", "\tfile: "+file)
                )
            } else {
                (Iterable.empty[(ClassFile, URL)], List.empty[Throwable])
            }
        }
        val project =
            Project(
                classFiles,
                libraryClassFiles,
                libraryClassFilesAreInterfacesOnly = !completelyLoadLibraries,
                Iterable.empty
            )(config = configuredConfig)
        handleParsingExceptions(project, exceptions1 ++ exceptions2)

        val statistics =
            project
                .statistics.map(kv => "- "+kv._1+": "+kv._2)
                .toList.sorted.reverse
                .mkString("project statistics:\n\t", "\n\t", "\n")
        info("project", statistics)(project.logContext)
        project
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy