/* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj package av package checking import scala.language.implicitConversions import java.net.URL import scala.Console.GREEN import scala.Console.RED import scala.Console.RESET import scala.collection.Map as AMap import scala.collection.Set as ASet import scala.collection.immutable import scala.collection.mutable import scala.collection.mutable.Map as MutableMap import scala.io.Source import scala.util.matching.Regex import org.opalj.br.* import org.opalj.br.analyses.Project import org.opalj.br.reader.Java8Framework.ClassFiles import org.opalj.de.* import org.opalj.de.DependencyTypes.toUsageDescription import org.opalj.io.processSource import org.opalj.log.GlobalLogContext import org.opalj.log.OPALLogger import org.opalj.util.PerformanceEvaluation.run import org.opalj.util.PerformanceEvaluation.time import scala.collection.parallel.CollectionConverters.IterableIsParallelizable /** * A specification of a project's architectural constraints. * * ===Usage=== * First define the ensembles, then the rules and at last specify the * class files that should be analyzed. The rules will then automatically be * evaluated. * * The intended way to create a specification is to create a new anonymous Specification * class that contains the specification of the architecture. Afterward, the specification * object can be used to get the list of architectural violations. * * {{{ * new Specification(project) { * ensemble('Number) { "mathematics.Number*" } * ensemble('Rational) { "mathematics.Rational*" } * ensemble('Mathematics) { "mathematics.Mathematics*" } * ensemble('Example) { "mathematics.Example*" } * * 'Example is_only_allowed_to (USE, 'Mathematics) * } * }}} * * ===Note=== * One ensemble is predefined: `Specification.empty` it represents an ensemble that * contains no source elements and which can, e.g., be used to specify that no "real" * ensemble is allowed to depend on a specific ensemble. * * @author Michael Eichberg * @author Samuel Beracasa * @author Marco Torsello */ class Specification(val project: Project[URL], val useAnsiColors: Boolean) { spec => /** * Creates a new `Specification` for the given `Project`. Error messages will * not use ANSI colors. */ def this(project: Project[URL]) = this(project, useAnsiColors = false) def this( classFiles: Iterable[(ClassFile, URL)], useAnsiColors: Boolean = false ) = this( run { Project( projectClassFilesWithSources = classFiles, Iterable.empty, libraryClassFilesAreInterfacesOnly = true /*actually not relevant*/ ) } { (t, project) => import project.logContext val logMessage = "1. reading " + project.classFilesCount + " class files took " + t.toSeconds val message = if (useAnsiColors) GREEN + logMessage + RESET else logMessage OPALLogger.progress(message) project }, useAnsiColors ) import project.logContext private def logProgress(logMessage: String): Unit = { OPALLogger.progress(if (useAnsiColors) GREEN + logMessage + RESET else logMessage) } private def logWarn(logMessage: String): Unit = { val message = if (useAnsiColors) RED + logMessage + RESET else logMessage OPALLogger.warn("project warn", message) } private def logInfo(logMessage: String): Unit = { OPALLogger.info("project info", logMessage) } @volatile private var theEnsembles: MutableMap[Symbol, (SourceElementsMatcher, ASet[VirtualSourceElement])] = scala.collection.mutable.HashMap.empty /** * The set of defined ensembles. An ensemble is identified by a symbol, a query * which matches source elements and the project's source elements that are matched. * The latter is available only after [[analyze]] was called. */ def ensembles: AMap[Symbol, (SourceElementsMatcher, ASet[VirtualSourceElement])] = theEnsembles // calculated after all class files have been loaded private val theOutgoingDependencies: MutableMap[ VirtualSourceElement, AMap[VirtualSourceElement, DependencyTypesSet] ] = scala.collection.mutable.HashMap.empty /** * Mapping between a source element and those source elements it depends on/uses. * * This mapping is automatically created when analyze is called. */ def outgoingDependencies: AMap[VirtualSourceElement, AMap[VirtualSourceElement, DependencyTypesSet]] = theOutgoingDependencies // calculated after all class files have been loaded private val theIncomingDependencies: mutable.Map[VirtualSourceElement, immutable.Set[( VirtualSourceElement, DependencyType )]] = { scala.collection.mutable.HashMap.empty } /** * Mapping between a source element and those source elements that depend on it. * * This mapping is automatically created when analyze is called. */ def incomingDependencies: AMap[VirtualSourceElement, ASet[(VirtualSourceElement, DependencyType)]] = theIncomingDependencies // calculated after the extension of all ensembles is determined private val matchedSourceElements: mutable.HashSet[VirtualSourceElement] = mutable.HashSet.empty private val allSourceElements: mutable.HashSet[VirtualSourceElement] = mutable.HashSet.empty private var unmatchedSourceElements: ASet[VirtualSourceElement] = mutable.HashSet.empty /** * Adds a new ensemble definition to this architecture specification. * * @throws SpecificationError If the ensemble is already defined. */ @throws(classOf[SpecificationError]) def ensemble( ensembleSymbol: Symbol )( sourceElementsMatcher: SourceElementsMatcher ): Unit = { if (ensembles.contains(ensembleSymbol)) throw SpecificationError("the ensemble is already defined: " + ensembleSymbol) theEnsembles += ( (ensembleSymbol, (sourceElementsMatcher, Set.empty[VirtualSourceElement])) ) } /** * Creates a `Symbol` with the given name. * * This method is primarily useful if ensemble names are created programmatically * and the code should communicate that the created name identifies an ensemble. * E.g., instead of * {{{ * for (moduleID <- 1 to 10) Symbol("module"+moduleID) * }}} * it is now possible to write * {{{ * for (moduleID <- 1 to 10) EnsembleID("module"+moduleID) * }}} * which better communicates the intention. */ def EnsembleID(ensembleName: String): Symbol = Symbol(ensembleName) /** * Represents an ensemble that contains no source elements. This can be used, e.g., * to specify that a (set of) specific source element(s) is not allowed to depend * on any other source elements (belonging to the project). */ val empty: Symbol = { ensemble(Symbol("Empty"))(NoSourceElementsMatcher) Symbol("Empty") } /** * Facilitates the definition of common source element matchers by means of common * String patterns. */ @throws(classOf[SpecificationError]) implicit def StringToSourceElementMatcher(matcher: String): SourceElementsMatcher = { if (matcher.endsWith(".*")) PackageMatcher(matcher.substring(0, matcher.length() - 2).replace('.', '/')) else if (matcher.endsWith(".**")) PackageMatcher(matcher.substring(0, matcher.length() - 3).replace('.', '/'), true) else if (matcher.endsWith("*")) ClassMatcher(matcher.substring(0, matcher.length() - 1).replace('.', '/'), true) else if (matcher.indexOf('*') == -1) ClassMatcher(matcher.replace('.', '/')) else throw SpecificationError("unsupported pattern: " + matcher); } def classes(matcher: Regex): SourceElementsMatcher = ClassMatcher(matcher) /** * Returns the class files stored at the given location. */ implicit def FileToClassFileProvider(file: java.io.File): Seq[(ClassFile, URL)] = { ClassFiles(file) } var architectureCheckers: List[ArchitectureChecker] = Nil case class GlobalIncomingConstraint( targetEnsemble: Symbol, sourceEnsembles: Seq[Symbol] ) extends DependencyChecker { override def targetEnsembles: Seq[Symbol] = Seq(targetEnsemble) override def violations(): ASet[SpecificationViolation] = { val sourceEnsembleElements = sourceEnsembles.foldLeft(Set[VirtualSourceElement]())(_ ++ ensembles(_)._2) val (_, targetEnsembleElements) = ensembles(targetEnsemble) for { targetEnsembleElement <- targetEnsembleElements if incomingDependencies.contains(targetEnsembleElement) (incomingElement, dependencyType) <- incomingDependencies(targetEnsembleElement) if !( sourceEnsembleElements.contains(incomingElement) || targetEnsembleElements.contains(incomingElement) ) } yield { DependencyViolation( project, this, incomingElement, targetEnsembleElement, dependencyType, "not allowed global incoming dependency found" ) } } override def toString: String = { s"$targetEnsemble is_only_to_be_used_by (${sourceEnsembles.mkString(",")})" } } /** * Forbids the given local dependencies between a specific source ensemble and * several target ensembles. * * ==Example Scenario== * If the ensemble `ex` is not allowed to use `ey` and the source element `x` which * belongs to ensemble `ex` has one if the given dependencies on a source element * belonging to `ey` then a [[SpecificationViolation]] is generated. */ case class LocalOutgoingNotAllowedConstraint( dependencyTypes: Set[DependencyType], sourceEnsemble: Symbol, targetEnsembles: Seq[Symbol] ) extends DependencyChecker { if (targetEnsembles.isEmpty) throw SpecificationError("no target ensembles specified: " + toString()) // WE DO NOT WANT TO CHECK THE VALIDITY OF THE ENSEMBLE IDS NOW TO MAKE IT EASY // TO INTERMIX THE DEFINITION OF ENSEMBLES AND CONSTRAINTS override def sourceEnsembles: Seq[Symbol] = Seq(sourceEnsemble) override def violations(): ASet[SpecificationViolation] = { val unknownEnsembles = targetEnsembles.filterNot(ensembles.contains(_)) if (unknownEnsembles.nonEmpty) throw SpecificationError( unknownEnsembles.mkString("unknown ensemble(s): ", ",", "") ) val (_ /*ensembleName*/, sourceEnsembleElements) = ensembles(sourceEnsemble) val notAllowedTargetSourceElements = targetEnsembles.foldLeft(Set.empty[VirtualSourceElement])(_ ++ ensembles(_)._2) for { sourceElement <- sourceEnsembleElements targets = outgoingDependencies.get(sourceElement) if targets.isDefined (targetElement, currentDependencyTypes) <- targets.get currentDependencyType <- currentDependencyTypes if ((notAllowedTargetSourceElements contains targetElement) && ((dependencyTypes equals USE) || (dependencyTypes contains currentDependencyType))) } yield { DependencyViolation( project, this, sourceElement, targetElement, currentDependencyType, "not allowed local outgoing dependency found" ) } } override def toString: String = { if (dependencyTypes equals USE) { targetEnsembles.mkString(s"$sourceEnsemble is_not_allowed_to use (", ",", ")") } else { val start = s"$sourceEnsemble is_not_allowed_to ${ dependencyTypes.map(d => toUsageDescription(d)).mkString(" and ") } (" targetEnsembles.mkString(start, ",", ")") } } } /** * Allows only the given local dependencies between a specific source ensemble and * several target ensembles. * * ==Example Scenario== * If the ensemble `ex` is only allowed to throw exceptions `ey` and the source * element `x` which belongs to ensemble `ex` throws an exception not belonging * to `ey` then a [[SpecificationViolation]] is generated. */ case class LocalOutgoingOnlyAllowedConstraint( dependencyTypes: Set[DependencyType], sourceEnsemble: Symbol, targetEnsembles: Seq[Symbol] ) extends DependencyChecker { if (targetEnsembles.isEmpty) throw SpecificationError("no target ensembles specified: " + toString()) // WE DO NOT WANT TO CHECK THE VALIDITY OF THE ENSEMBLE IDS NOW TO MAKE IT EASY // TO INTERMIX THE DEFINITION OF ENSEMBLES AND CONSTRAINTS override def sourceEnsembles: Seq[Symbol] = Seq(sourceEnsemble) override def violations(): ASet[SpecificationViolation] = { val unknownEnsembles = targetEnsembles.filterNot(ensembles.contains(_)) if (unknownEnsembles.nonEmpty) throw SpecificationError( unknownEnsembles.mkString("unknown ensemble(s): ", ",", "") ) val (_ /*ensembleName*/, sourceEnsembleElements) = ensembles(sourceEnsemble) val allAllowedLocalTargetSourceElements = // self references are allowed as well as references to source elements belonging // to a target ensemble targetEnsembles.foldLeft(sourceEnsembleElements)(_ ++ ensembles(_)._2) for { sourceElement <- sourceEnsembleElements targets = outgoingDependencies.get(sourceElement) if targets.isDefined (targetElement, currentDependencyTypes) <- targets.get currentDependencyType <- currentDependencyTypes if (!(allAllowedLocalTargetSourceElements contains targetElement) && ((dependencyTypes equals USE) || (dependencyTypes contains currentDependencyType))) // references to unmatched source elements are ignored if !(unmatchedSourceElements contains targetElement) } yield { DependencyViolation( project, this, sourceElement, targetElement, currentDependencyType, "violation of a local outgoing dependency constraint" ) } } override def toString: String = { if (dependencyTypes equals USE) { targetEnsembles.mkString(s"$sourceEnsemble is_only_allowed_to use (", ",", ")") } else { val start = s"$sourceEnsemble is_only_allowed_to ${ dependencyTypes.map(d => toUsageDescription(d)).mkString(" and ") } (" targetEnsembles.mkString(start, ",", ")") } } } /** * Checks whether all elements in the source ensemble are annotated with the given * annotation. * * ==Example Scenario== * If every element in the ensemble `ex` should be annotated with `ey` and the * source element `x` which belongs to ensemble `ex` has no annotation that matches * `ey` then a [[SpecificationViolation]] is generated. * * @param sourceEnsemble An ensemble containing elements, that should be annotated. * @param annotationPredicates The annotations that should match. * @param property A description of the property that is checked. * @param matchAny true if only one match is needed, false if all annotations should match */ case class LocalOutgoingAnnotatedWithConstraint( sourceEnsemble: Symbol, annotationPredicates: Seq[AnnotationPredicate], property: String, matchAny: Boolean ) extends PropertyChecker { def this( sourceEnsemble: Symbol, annotationPredicates: Seq[AnnotationPredicate], matchAny: Boolean = false ) = this( sourceEnsemble, annotationPredicates, annotationPredicates.map(_.toDescription).mkString("(", " - ", ")"), matchAny ) override def ensembles: Seq[Symbol] = Seq(sourceEnsemble) override def violations(): ASet[SpecificationViolation] = { val (_ /*ensembleName*/, sourceEnsembleElements) = spec.ensembles(sourceEnsemble) for { sourceElement <- sourceEnsembleElements classFile <- project.classFile(sourceElement.classType.asClassType) annotations = sourceElement match { case _: VirtualClass => classFile.annotations case vf: VirtualField => classFile.fields collectFirst { case f if f.asVirtualField(classFile).compareTo(vf) == 0 => f } match { case Some(f) => f.annotations case _ => IndexedSeq.empty } case vm: VirtualMethod => classFile.methods collectFirst { case m if m.asVirtualMethod(classFile.thisType).compareTo(vm) == 0 => m } match { case Some(m) => m.annotations case _ => IndexedSeq.empty } case _: VirtualModule => IndexedSeq.empty } // if !annotations.foldLeft(false) { // (v: Boolean, a: Annotation) => // v || annotationPredicates.foldLeft(!matchAny) { // (matched: Boolean, m: AnnotationPredicate) => // if (matchAny) { // matched || m(a) // } else { // matched && m(a) // } // } // } if !annotationPredicates.foldLeft(!matchAny) { (v: Boolean, m: AnnotationPredicate) => if (!matchAny) { v && annotations.exists { a => m(a) } } else { v || annotations.exists { a => m(a) } } } } yield { PropertyViolation( project, this, sourceElement, "the element should be ANNOTATED WITH", "required annotation not found" ) } } override def toString: String = { s"$sourceEnsemble every_element_should_be_annotated_with $property" } } /** * Checks whether all elements in the source ensemble implement the given * method. The source ensemble should contain only class elements * otherwise a [[SpecificationError]] will be thrown. * * @param sourceEnsemble An ensemble containing classes, that should implement the given method. * @param methodPredicate The method to match. */ case class LocalOutgoingShouldImplementMethodConstraint( sourceEnsemble: Symbol, methodPredicate: SourceElementPredicate[Method] ) extends PropertyChecker { override def property: String = methodPredicate.toDescription override def ensembles: Seq[Symbol] = Seq(sourceEnsemble) override def violations(): ASet[SpecificationViolation] = { val (_ /*ensembleName*/, sourceEnsembleElements) = spec.ensembles(sourceEnsemble) for { sourceElement <- sourceEnsembleElements sourceClassFile = sourceElement match { case s: VirtualClass => project.classFile(s.classType.asClassType).get case _ => throw SpecificationError(sourceElement.toJava + " is not a class") } if sourceClassFile.methods.forall(m => !methodPredicate(m)) } yield { PropertyViolation( project, this, sourceElement, "the element should IMPLEMENT METHOD", "required method implementation not found" ) } } override def toString: String = { s"$sourceEnsemble every_element_should_implement_method ($property)" } } /** * Checks whether all elements in the source ensemble extends any of * the given elements. The source ensemble should contain only class elements * otherwise a [[SpecificationError]] will be thrown. * * @param sourceEnsemble An ensemble containing classes, that should implement the given method. * @param targetEnsembles Ensembles containing elements, that should be extended by the given classes. */ case class LocalOutgoingShouldExtendConstraint( sourceEnsemble: Symbol, targetEnsembles: Seq[Symbol] ) extends PropertyChecker { override def property: String = targetEnsembles.mkString(", ") override def ensembles: Seq[Symbol] = Seq(sourceEnsemble) override def violations(): ASet[SpecificationViolation] = { val (_ /*ensembleName*/, sourceEnsembleElements) = spec.ensembles(sourceEnsemble) val allLocalTargetSourceElements = // self references are allowed as well as references to source elements belonging // to a target ensemble targetEnsembles.foldLeft(sourceEnsembleElements)(_ ++ spec.ensembles(_)._2) for { sourceElement <- sourceEnsembleElements sourceClassFile = sourceElement match { case s: VirtualClass => project.classFile(s.classType.asClassType).get case _ => throw SpecificationError(sourceElement.toJava + " is not a class") } if sourceClassFile.superclassType.exists { s => !allLocalTargetSourceElements.exists(v => v.classType.asClassType.equals(s)) } } yield { PropertyViolation( project, this, sourceElement, "the element should extend any of the given classes", "required inheritance not found" ) } } override def toString: String = { targetEnsembles.mkString(s"$sourceEnsemble every_element_should_extend (", ",", ")") } } /** * The set of all [[org.opalj.de.DependencyTypes]]. */ final val USE: Set[DependencyType] = DependencyTypes.values case class SpecificationFactory(contextEnsembleSymbol: Symbol) { def apply(sourceElementsMatcher: SourceElementsMatcher): Unit = { ensemble(contextEnsembleSymbol)(sourceElementsMatcher) } def is_only_to_be_used_by(sourceEnsembleSymbols: Symbol*): Unit = { architectureCheckers = new GlobalIncomingConstraint( contextEnsembleSymbol, sourceEnsembleSymbols.toSeq ) :: architectureCheckers } def allows_incoming_dependencies_from(sourceEnsembleSymbols: Symbol*): Unit = { architectureCheckers = new GlobalIncomingConstraint( contextEnsembleSymbol, sourceEnsembleSymbols.toSeq ) :: architectureCheckers } def is_only_allowed_to( dependencyTypes: Set[DependencyType], targetEnsembles: Symbol* ): Unit = { architectureCheckers = new LocalOutgoingOnlyAllowedConstraint( dependencyTypes, contextEnsembleSymbol, targetEnsembles.toSeq ) :: architectureCheckers } def is_not_allowed_to( dependencyTypes: Set[DependencyType], targetEnsembles: Symbol* ): Unit = { architectureCheckers = new LocalOutgoingNotAllowedConstraint( dependencyTypes, contextEnsembleSymbol, targetEnsembles.toSeq ) :: architectureCheckers } def every_element_should_be_annotated_with( annotationPredicate: AnnotationPredicate ): Unit = { architectureCheckers = new LocalOutgoingAnnotatedWithConstraint( contextEnsembleSymbol, Seq(annotationPredicate) ) :: architectureCheckers } def every_element_should_be_annotated_with( property: String, annotationPredicates: Seq[AnnotationPredicate], matchAny: Boolean = false ): Unit = { architectureCheckers = LocalOutgoingAnnotatedWithConstraint( contextEnsembleSymbol, annotationPredicates, property, matchAny ) :: architectureCheckers } def every_element_should_implement_method( methodPredicate: SourceElementPredicate[Method] ): Unit = { architectureCheckers = LocalOutgoingShouldImplementMethodConstraint( contextEnsembleSymbol, methodPredicate ) :: architectureCheckers } def every_element_should_extend(targetEnsembles: Symbol*): Unit = { architectureCheckers = new LocalOutgoingShouldExtendConstraint( contextEnsembleSymbol, targetEnsembles.toSeq ) :: architectureCheckers } } protected implicit def EnsembleSymbolToSpecificationElementFactory( ensembleSymbol: Symbol ): SpecificationFactory = { SpecificationFactory(ensembleSymbol) } protected implicit def EnsembleToSourceElementMatcher( ensembleSymbol: Symbol ): SourceElementsMatcher = { if (!ensembles.contains(ensembleSymbol)) throw SpecificationError(s"the ensemble: $ensembleSymbol is not yet defined") ensembles(ensembleSymbol)._1 } /** * Returns a textual representation of an ensemble. */ def ensembleToString(ensembleSymbol: Symbol): String = { val (sourceElementsMatcher, extension) = ensembles(ensembleSymbol) s"$ensembleSymbol{" + s"$sourceElementsMatcher " + { if (extension.isEmpty) "/* NO ELEMENTS */ " else { extension.tail.foldLeft("\n\t//" + extension.head.toString + "\n")((s, vse) => s + "\t//" + vse.toJava + "\n" ) } } + "}" } /** * Can be called after the evaluation of the extents of the ensembles to print * out the current configuration. */ def ensembleExtentsToString: String = { val s = new mutable.StringBuilder() for ((ensemble, (_, elements)) <- theEnsembles) { s ++= s"$ensemble\n" for (element <- elements) { s ++= s"\t\t\t${element.toJava}\n" } } s.result() } def analyze(): Set[SpecificationViolation] = { val dependencyStore = time { project.get(DependencyStoreWithoutSelfDependenciesKey) } { ns => logProgress("2.1. preprocessing dependencies took " + ns.toSeconds) } logInfo("Dependencies between source elements: " + dependencyStore.dependencies.size) logInfo("Dependencies on primitive types: " + dependencyStore.dependenciesOnBaseTypes.size) logInfo("Dependencies on array types: " + dependencyStore.dependenciesOnArrayTypes.size) time { for { (source, targets) <- dependencyStore.dependencies (target, dTypes) <- targets } { allSourceElements += source allSourceElements += target theOutgoingDependencies.update(source, targets) for { dType <- dTypes } { theIncomingDependencies.update( target, theIncomingDependencies.getOrElse(target, immutable.Set.empty) + ((source, dType)) ) } } } { ns => logProgress("2.2. postprocessing dependencies took " + ns.toSeconds) } logInfo("Number of source elements: " + allSourceElements.size) logInfo("Outgoing dependencies: " + theOutgoingDependencies.size) logInfo("Incoming dependencies: " + theIncomingDependencies.size) // Calculate the extension of the ensembles // time { val instantiatedEnsembles = theEnsembles.par map { ensemble => val (ensembleSymbol, (sourceElementMatcher, _)) = ensemble // if a sourceElementMatcher is reused! sourceElementMatcher.synchronized { val extension = sourceElementMatcher.extension(using project) if (extension.isEmpty && sourceElementMatcher != NoSourceElementsMatcher) logWarn(s" $ensembleSymbol (${extension.size})") else logInfo(s" $ensembleSymbol (${extension.size})") spec.synchronized { matchedSourceElements ++= extension } (ensembleSymbol, (sourceElementMatcher, extension)) } } theEnsembles = mutable.Map.from(instantiatedEnsembles.seq) unmatchedSourceElements = allSourceElements --= matchedSourceElements logInfo(" => Matched source elements: " + matchedSourceElements.size) logInfo(" => Other source elements: " + unmatchedSourceElements.size) } { ns => logProgress("3. determining the extension of the ensembles took " + ns.toSeconds) } // Check all rules // time { val result = for { architectureChecker <- architectureCheckers.par } yield { logProgress(" checking: " + architectureChecker) for (violation <- architectureChecker.violations()) yield violation } Set.empty ++ (result.filter(_.nonEmpty).flatten) } { ns => logProgress("4. checking the specified dependency constraints took " + ns.toSeconds) } } } object Specification { def ProjectDirectory(directoryName: String): Seq[(ClassFile, URL)] = { val file = new java.io.File(directoryName) if (!file.exists) throw SpecificationError("the specified directory does not exist: " + directoryName) if (!file.canRead) throw SpecificationError("cannot read the specified directory: " + directoryName) if (!file.isDirectory) throw SpecificationError("the specified directory is not a directory: " + directoryName) Project.JavaClassFileReader().ClassFiles(file) } def ProjectJAR(jarName: String): Seq[(ClassFile, URL)] = { val file = new java.io.File(jarName) if (!file.exists) throw SpecificationError("the specified directory does not exist: " + jarName) if (!file.canRead) throw SpecificationError("cannot read the specified JAR: " + jarName) if (file.isDirectory) throw SpecificationError("the specified jar file is a directory: " + jarName) OPALLogger.info("creating project", s"loading $jarName")(using GlobalLogContext) Project.JavaClassFileReader().ClassFiles(file) } /** * Load all jar files. */ def ProjectJARs(jarNames: Seq[String]): Seq[(ClassFile, URL)] = { jarNames.flatMap(ProjectJAR) } /** * Loads all class files of the specified jar file using the library class file reader. * (I.e., the all method implementations are skipped.) * * @param jarName The name of a jar file. */ def LibraryJAR(jarName: String): Seq[(ClassFile, URL)] = { val file = new java.io.File(jarName) if (!file.exists) throw SpecificationError("the specified directory does not exist: " + jarName) if (!file.canRead) throw SpecificationError("cannot read the specified JAR: " + jarName) if (file.isDirectory) throw SpecificationError("the specified jar file is a directory: " + jarName) OPALLogger.info("creating project", s"loading library $jarName")(using GlobalLogContext) Project.JavaLibraryClassFileReader.ClassFiles(file) } /** * Load all jar files using the library class loader. */ def LibraryJARs(jarNames: Seq[String]): Seq[(ClassFile, URL)] = { jarNames.flatMap(LibraryJAR) } /** * Returns a list of paths contained inside the given classpath file. * A classpath file should contain paths as text separated by a path-separator character. * On UNIX systems, this character is ':'; on Microsoft Windows systems it * is ';'. * * ===Example=== * /path/to/jar/library.jar:/path/to/library/example.jar:/path/to/library/example2.jar * * Classpath files should be used to prevent absolute paths in tests. */ def Classpath( fileName: String, pathSeparatorChar: Char = java.io.File.pathSeparatorChar ): Iterable[String] = { processSource(Source.fromFile(new java.io.File(fileName))) { s => s.getLines().flatMap(_.split(pathSeparatorChar)).toSet } } /** * Returns a list of paths that matches the given * regular expression from the given list of paths. */ def PathToJARs(paths: Iterable[String], jarName: Regex): Iterable[String] = { val matchedPaths = paths.collect { case p @ (jarName(_)) => p } if (matchedPaths.isEmpty) throw SpecificationError(s"no path is matched by: $jarName."); matchedPaths } /** * Returns a list of paths that match the given list of * regular expressions from the given list of paths. */ def PathToJARs(paths: Iterable[String], jarNames: Iterable[Regex]): Iterable[String] = { jarNames.foldLeft(Set.empty[String])((c, n) => c ++ PathToJARs(paths, n)) } }