/* 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.util.matching.Regex import scala.collection.{immutable, mutable, Map => AMap, Set => ASet} import scala.collection.mutable.{Map => MutableMap} import scala.Console.{GREEN, RED, RESET} import scala.io.Source import org.opalj.util.PerformanceEvaluation.{run, time} import org.opalj.br._ import org.opalj.br.reader.Java8Framework.ClassFiles import org.opalj.br.analyses.Project import org.opalj.de._ import org.opalj.log.OPALLogger import org.opalj.log.GlobalLogContext import org.opalj.io.processSource import org.opalj.de.DependencyTypes.toUsageDescription 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. Afterwards 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[this] def logProgress(logMessage: String): Unit = { OPALLogger.progress(if (useAnsiColors) GREEN + logMessage + RESET else logMessage) } private[this] def logWarn(logMessage: String): Unit = { val message = if (useAnsiColors) RED + logMessage + RESET else logMessage OPALLogger.warn("project warn", message) } private[this] def logInfo(logMessage: String): Unit = { OPALLogger.info("project info", logMessage) } @volatile private[this] 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[this] 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[this] 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[this] val matchedSourceElements: mutable.HashSet[VirtualSourceElement] = mutable.HashSet.empty private[this] val allSourceElements: mutable.HashSet[VirtualSourceElement] = mutable.HashSet.empty private[this] 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 = { 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.asObjectType) 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 _ => 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.asObjectType).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.asObjectType).get case _ => throw SpecificationError(sourceElement.toJava+" is not a class") } if sourceClassFile.superclassType.map(s => !allLocalTargetSourceElements.exists(v => v.classType.asObjectType.equals(s))).getOrElse(false) } 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 = new LocalOutgoingAnnotatedWithConstraint( contextEnsembleSymbol, annotationPredicates, property, matchAny ) :: architectureCheckers } def every_element_should_implement_method( methodPredicate: SourceElementPredicate[Method] ): Unit = { architectureCheckers = new 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(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. determing 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")(GlobalLogContext) Project.JavaClassFileReader().ClassFiles(file) } /** * Load all jar files. */ def ProjectJARs(jarNames: Seq[String]): Seq[(ClassFile, URL)] = { jarNames.map(ProjectJAR(_)).flatten } /** * 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")(GlobalLogContext) Project.JavaLibraryClassFileReader.ClassFiles(file) } /** * Load all jar files using the library class loader. */ def LibraryJARs(jarNames: Seq[String]): Seq[(ClassFile, URL)] = { jarNames.map(LibraryJAR(_)).flatten } /** * Returns a list of paths contained inside the given classpath file. * A classpath file should contain paths as text seperated 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().map(_.split(pathSeparatorChar)).flatten.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)) } }