/* * Copyright (c) 2014 Contributor. All rights reserved. */ package dotty.tools.dotc.classpath import java.io.File import java.net.URL import scala.annotation.tailrec import scala.reflect.io.{AbstractFile, FileZipArchive, ManifestResources} import dotty.tools.io.ClassPath import dotty.tools.dotc.config.Settings import dotty.tools.dotc.core.Contexts.Context import FileUtils._ /** * A trait providing an optional cache for classpath entries obtained from zip and jar files. * It's possible to create such a cache assuming that entries in such files won't change (at * least will be the same each time we'll load classpath during the lifetime of JVM process) * - unlike class and source files in directories, which can be modified and recompiled. * It allows us to e.g. reduce significantly memory used by PresentationCompilers in Scala IDE * when there are a lot of projects having a lot of common dependencies. */ sealed trait ZipAndJarFileLookupFactory { private val cache = collection.mutable.Map.empty[AbstractFile, ClassPath] def create(zipFile: AbstractFile)(implicit ctx: Context): ClassPath = { if (ctx.settings.YdisableFlatCpCaching.value) createForZipFile(zipFile) else createUsingCache(zipFile) } protected def createForZipFile(zipFile: AbstractFile): ClassPath private def createUsingCache(zipFile: AbstractFile)(implicit ctx: Context): ClassPath = cache.synchronized { def newClassPathInstance = { if (ctx.settings.verbose.value || ctx.settings.Ylogcp.value) println(s"$zipFile is not yet in the classpath cache") createForZipFile(zipFile) } cache.getOrElseUpdate(zipFile, newClassPathInstance) } } /** * Manages creation of classpath for class files placed in zip and jar files. * It should be the only way of creating them as it provides caching. */ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory { private case class ZipArchiveClassPath(zipFile: File) extends ZipArchiveFileLookup[ClassFileEntryImpl] with NoSourcePaths { override def findClassFile(className: String): Option[AbstractFile] = { val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className) classes(pkg).find(_.name == simpleClassName).map(_.file) } override private[dotty] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage) override protected def createFileEntry(file: FileZipArchive#Entry): ClassFileEntryImpl = ClassFileEntryImpl(file) override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isClass } /** * This type of classpath is closely related to the support for JSR-223. * Its usage can be observed e.g. when running: * jrunscript -classpath scala-compiler.jar;scala-reflect.jar;scala-library.jar -l scala * with a particularly prepared scala-library.jar. It should have all classes listed in the manifest like e.g. this entry: * Name: scala/Function2$mcFJD$sp.class */ private case class ManifestResourcesClassPath(file: ManifestResources) extends ClassPath with NoSourcePaths { override def findClassFile(className: String): Option[AbstractFile] = { val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className) classes(pkg).find(_.name == simpleClassName).map(_.file) } override def asClassPathStrings: Seq[String] = Seq(file.path) override def asURLs: Seq[URL] = file.toURLs() import ManifestResourcesClassPath.PackageFileInfo import ManifestResourcesClassPath.PackageInfo /** * A cache mapping package name to abstract file for package directory and subpackages of given package. * * ManifestResources can iterate through the collections of entries from e.g. remote jar file. * We can't just specify the path to the concrete directory etc. so we can't just 'jump' into * given package, when it's needed. On the other hand we can iterate over entries to get * AbstractFiles, iterate over entries of these files etc. * * Instead of traversing a tree of AbstractFiles once and caching all entries or traversing each time, * when we need subpackages of a given package or its classes, we traverse once and cache only packages. * Classes for given package can be then easily loaded when they are needed. */ private lazy val cachedPackages: collection.mutable.HashMap[String, PackageFileInfo] = { val packages = collection.mutable.HashMap[String, PackageFileInfo]() def getSubpackages(dir: AbstractFile): List[AbstractFile] = (for (file <- dir if file.isPackage) yield file)(collection.breakOut) @tailrec def traverse(packagePrefix: String, filesForPrefix: List[AbstractFile], subpackagesQueue: collection.mutable.Queue[PackageInfo]): Unit = filesForPrefix match { case pkgFile :: remainingFiles => val subpackages = getSubpackages(pkgFile) val fullPkgName = packagePrefix + pkgFile.name packages.put(fullPkgName, PackageFileInfo(pkgFile, subpackages)) val newPackagePrefix = fullPkgName + "." subpackagesQueue.enqueue(PackageInfo(newPackagePrefix, subpackages)) traverse(packagePrefix, remainingFiles, subpackagesQueue) case Nil if subpackagesQueue.nonEmpty => val PackageInfo(packagePrefix, filesForPrefix) = subpackagesQueue.dequeue() traverse(packagePrefix, filesForPrefix, subpackagesQueue) case _ => } val subpackages = getSubpackages(file) packages.put(ClassPath.RootPackage, PackageFileInfo(file, subpackages)) traverse(ClassPath.RootPackage, subpackages, collection.mutable.Queue()) packages } override private[dotty] def packages(inPackage: String): Seq[PackageEntry] = cachedPackages.get(inPackage) match { case None => Seq.empty case Some(PackageFileInfo(_, subpackages)) => val prefix = PackageNameUtils.packagePrefix(inPackage) subpackages.map(packageFile => PackageEntryImpl(prefix + packageFile.name)) } override private[dotty] def classes(inPackage: String): Seq[ClassFileEntry] = cachedPackages.get(inPackage) match { case None => Seq.empty case Some(PackageFileInfo(pkg, _)) => (for (file <- pkg if file.isClass) yield ClassFileEntryImpl(file))(collection.breakOut) } override private[dotty] def list(inPackage: String): ClassPathEntries = ClassPathEntries(packages(inPackage), classes(inPackage)) } private object ManifestResourcesClassPath { case class PackageFileInfo(packageFile: AbstractFile, subpackages: Seq[AbstractFile]) case class PackageInfo(packageName: String, subpackages: List[AbstractFile]) } override protected def createForZipFile(zipFile: AbstractFile): ClassPath = if (zipFile.file == null) createWithoutUnderlyingFile(zipFile) else ZipArchiveClassPath(zipFile.file) private def createWithoutUnderlyingFile(zipFile: AbstractFile) = zipFile match { case manifestRes: ManifestResources => ManifestResourcesClassPath(manifestRes) case _ => val errorMsg = s"Abstract files which don't have an underlying file and are not ManifestResources are not supported. There was $zipFile" throw new IllegalArgumentException(errorMsg) } } /** * Manages creation of classpath for source files placed in zip and jar files. * It should be the only way of creating them as it provides caching. */ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory { private case class ZipArchiveSourcePath(zipFile: File) extends ZipArchiveFileLookup[SourceFileEntryImpl] with NoClassPaths { override def asSourcePathString: String = asClassPathString override private[dotty] def sources(inPackage: String): Seq[SourceFileEntry] = files(inPackage) override protected def createFileEntry(file: FileZipArchive#Entry): SourceFileEntryImpl = SourceFileEntryImpl(file) override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource } override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file) }