/* sbt -- Simple Build Tool * Copyright 2008, 2009, 2010 Mark Harrah */ package sbt import Path._ import IO.{pathSplit, wrapNull} import java.io.File import java.net.URL import scala.collection.{generic, immutable, mutable, TraversableLike} /** A Path represents a file in a project. * @see sbt.PathFinder*/ sealed abstract class Path extends PathFinder { /** Creates a base directory for this path. This is used by copy and zip functions * to determine the relative path that should be used in the destination. For example, * if the following path is specified to be copied to directory 'd', * * ((a / b) ###) / x / y * * the copied path would be * * d / x / y * * The relativePath method is used to return the relative path to the base directory. */ override def ### : Path = new BaseDirectory(this) private[sbt] def addTo(pathSet: mutable.Set[Path]) { if(asFile.exists) pathSet += this } override def / (component: String): Path = if(component == ".") this else new RelativePath(this, component) /** True if and only if the file represented by this path exists.*/ def exists = asFile.exists /** True if and only if the file represented by this path is a directory.*/ def isDirectory = asFile.isDirectory /** The last modified time of the file represented by this path.*/ def lastModified = asFile.lastModified /* True if and only if file that this path represents exists and the file represented by the path 'p' * does not exist or was modified before the file for this path.*/ def newerThan(p: Path): Boolean = exists && (!p.exists || lastModified > p.lastModified) /* True if and only if file that this path represents does not exist or the file represented by the path 'p' * exists and was modified after the file for this path.*/ def olderThan(p: Path): Boolean = p newerThan this /** The file represented by this path.*/ def asFile: File /** The file represented by this path converted to a URL.*/ def asURL = asFile.toURI.toURL /** The string representation of this path relative to the base directory. The project directory is the * default base directory if one is not specified explicitly using the ### operator.*/ lazy val relativePath: String = relativePathString(sep.toString) def relativePathString(separator: String): String final def projectRelativePath: String = projectRelativePathString(sep.toString) def projectRelativePathString(separator: String): String def absolutePath: String = asFile.getAbsolutePath private[sbt] def prependTo(s: String): String /** The last component of this path.*/ def name = asFile.getName /** The extension part of the name of this path. This is the part of the name after the last period, or the empty string if there is no period.*/ def ext = baseAndExt._2 /** The base of the name of this path. This is the part of the name before the last period, or the full name if there is no period.*/ def base = baseAndExt._1 def baseAndExt: (String, String) = { val nme = name val dot = nme.lastIndexOf('.') if(dot < 0) (nme, "") else (nme.substring(0, dot), nme.substring(dot+1)) } /** Equality of Paths is defined in terms of the underlying File.*/ override final def equals(other: Any) = other match { case op: Path => asFile == op.asFile case _ => false } /** The hash code of a Path is that of the underlying File.*/ override final def hashCode = asFile.hashCode } private final class BaseDirectory(private[sbt] val path: Path) extends Path { override def ### : Path = this override def toString = path.toString def asFile = path.asFile def relativePathString(separator: String) = "" def projectRelativePathString(separator: String) = path.projectRelativePathString(separator) private[sbt] def prependTo(s: String) = "." + sep + s } private[sbt] final class FilePath(file: File) extends Path { lazy val asFile = absolute(file) override def toString = absolutePath def relativePathString(separator: String) = asFile.getName def projectRelativePathString(separator: String) = relativePathString(separator) private[sbt] def prependTo(s: String) = absolutePath + sep + s } // toRoot is the path between this and the root project path and is used for toString private[sbt] final class ProjectDirectory(file: File, toRoot: Option[Path]) extends Path { def this(file: File) = this(file, None) lazy val asFile = absolute(file) override def toString = foldToRoot(_.toString, ".") def relativePathString(separator: String) = "" def projectRelativePathString(separator: String) = "" private[sbt] def prependTo(s: String) = foldToRoot(_.prependTo(s), "." + sep + s) private[sbt] def foldToRoot[T](f: Path => T, orElse: T) = toRoot.map(f).getOrElse(orElse) } private[sbt] final class RelativePath(val parentPath: Path, val component: String) extends Path { checkComponent(component) override def toString = parentPath prependTo component lazy val asFile = new File(parentPath.asFile, component) private[sbt] def prependTo(s: String) = parentPath prependTo (component + sep + s) def relativePathString(separator: String) = relative(parentPath.relativePathString(separator), separator) def projectRelativePathString(separator: String) = relative(parentPath.projectRelativePathString(separator), separator) private def relative(parentRelative: String, separator: String) = { if(parentRelative.isEmpty) component else parentRelative + separator + component } } object Path extends Alternatives with Mapper { import java.io.File import File.pathSeparator implicit def fileToPath(file: File): Path = Path.fromFile(file) implicit def pathToFile(path: Path): File = path.asFile implicit def pathsToFiles[CC[X] <: TraversableLike[X,CC[X]]](cc: CC[Path])(implicit cb: generic.CanBuildFrom[CC[Path], File, CC[File]]): CC[File] = cc.map(_.asFile) implicit def filesToPaths[CC[X] <: TraversableLike[X,CC[X]]](cc: CC[File])(implicit cb: generic.CanBuildFrom[CC[File], Path, CC[Path]]): CC[Path] = cc.map(fileToPath) implicit def filesToFinder(cc: Traversable[File]): PathFinder = finder(cc) implicit def pathsToFinder(cc: Traversable[Path]): PathFinder = lazyPathFinder(cc) def fileProperty(name: String) = Path.fromFile(System.getProperty(name)) def userHome = fileProperty("user.home") def absolute(file: File) = new File(file.toURI.normalize).getAbsoluteFile /** Constructs a String representation of Paths. The absolute path String of each Path is * separated by the platform's path separator.*/ def makeString(paths: Iterable[Path]): String = makeString(paths, pathSeparator) /** Constructs a String representation of Paths. The absolute path String of each Path is * separated by the given separator String.*/ def makeString(paths: Iterable[Path], sep: String): String = paths.map(_.absolutePath).mkString(sep) /** Constructs a String representation of Paths. The relative path String of each Path is * separated by the platform's path separator.*/ def makeRelativeString(paths: Iterable[Path]): String = paths.map(_.relativePathString(sep.toString)).mkString(pathSeparator) def splitString(projectPath: Path, value: String): Iterable[Path] = { for(pathString <- pathSplit(value) if pathString.length > 0) yield Path.fromString(projectPath, pathString) } /** A PathFinder that always produces the empty set of Paths.*/ def emptyPathFinder = new PathFinder { private[sbt] def addTo(pathSet: mutable.Set[Path]) {} } /** A PathFinder that selects the paths provided by the paths argument, which is * reevaluated on each call to the PathFinder's get method. */ def lazyPathFinder(paths: => Traversable[Path]): PathFinder = new PathFinder { private[sbt] def addTo(pathSet: mutable.Set[Path]) = pathSet ++= paths } def finder(files: => Traversable[File]): PathFinder = lazyPathFinder { fromFiles(files) } /** The separator character of the platform.*/ val sep = java.io.File.separatorChar /** Checks the string to verify that it is a legal path component. The string must be non-empty, * not a slash, and not '.' or '..'.*/ def checkComponent(c: String): String = { require(c.length > 0, "Path component must not be empty") require(c.indexOf('/') == -1, "Path component '" + c + "' must not have forward slashes in it") require(c.indexOf('\\') == -1, "Path component '" + c + "' must not have backslashes in it") require(c != "..", "Path component cannot be '..'") require(c != ".", "Path component cannot be '.'") c } /** Converts a path string relative to the given base path to a Path. */ def fromString(basePath: Path, value: String): Path = { if(value.isEmpty) basePath else { val components = value.split("""[/\\]""") (basePath /: components)( (path, component) => path / component ) } } def baseAncestor(path: Path): Option[Path] = path match { case pd: ProjectDirectory => None case fp: FilePath => None case rp: RelativePath => baseAncestor(rp.parentPath) case b: BaseDirectory => Some(b.path) } def relativize(basePath: Path, path: Path): Option[Path] = relativize(basePath, path.asFile) def relativize(basePath: Path, file: File): Option[Path] = basePathString(basePath) flatMap { baseString => relativize(basePath, baseString, file) } def relativize(basePath: Path, basePathString: String, file: File): Option[Path] = { val pathString = file.getAbsolutePath if(pathString.startsWith(basePathString)) Some(fromString(basePath, pathString.substring(basePathString.length))) else None } private[sbt] def relativize(baseFile: File, file: File): Option[String] = { val pathString = file.getAbsolutePath baseFileString(baseFile) flatMap { baseString => { if(pathString.startsWith(baseString)) Some(pathString.substring(baseString.length)) else None } } } private[sbt] def basePathString(basePath: Path): Option[String] = baseFileString(basePath.asFile) private def baseFileString(baseFile: File): Option[String] = { if(baseFile.isDirectory) { val cp = baseFile.getAbsolutePath assert(cp.length > 0) if(cp.charAt(cp.length - 1) == File.separatorChar) Some(cp) else Some(cp + File.separatorChar) } else None } def fromFile(file: String): Path = fromFile(new File(file)) def fromFile(file: File): Path = new FilePath(file) import collection.generic.{CanBuildFrom, FilterMonadic} def fromFiles[Repr, That](files: FilterMonadic[File, Repr])(implicit bf: CanBuildFrom[Repr, Path, That]): That = files.map(fromFile) def getFiles(files: Traversable[Path]): immutable.Set[File] = files.map(_.asFile).toSet def getURLs(files: Traversable[Path]): Array[URL] = files.map(_.asURL).toArray } /** A path finder constructs a set of paths. The set is evaluated by a call to the get * method. The set will be different for different calls to get if the underlying filesystem * has changed.*/ sealed abstract class PathFinder extends NotNull { /** The union of the paths found by this PathFinder with the paths found by 'paths'.*/ def +++(paths: PathFinder): PathFinder = new Paths(this, paths) /** Excludes all paths from excludePaths from the paths selected by this PathFinder.*/ def ---(excludePaths: PathFinder): PathFinder = new ExcludePaths(this, excludePaths) /** Constructs a new finder that selects all paths with a name that matches filter and are * descendents of paths selected by this finder.*/ def **(filter: FileFilter): PathFinder = new DescendentOrSelfPathFinder(this, filter) def *** : PathFinder = **(AllPassFilter) /** Constructs a new finder that selects all paths with a name that matches filter and are * immediate children of paths selected by this finder.*/ def *(filter: FileFilter): PathFinder = new ChildPathFinder(this, filter) /** Constructs a new finder that selects all paths with name literal that are immediate children * of paths selected by this finder.*/ def / (literal: String): PathFinder = new ChildPathFinder(this, new ExactFilter(literal)) /** Constructs a new finder that selects all paths with name literal that are immediate children * of paths selected by this finder.*/ final def \ (literal: String): PathFinder = this / literal /** Makes the paths selected by this finder into base directories. * @see Path.### */ def ### : PathFinder = new BasePathFinder(this) def x_![T](mapper: File => Option[T]): Traversable[(File,T)] = x(mapper, false) /** Applies `mapper` to each path selected by this PathFinder and returns the path paired with the non-empty result. * If the result is empty (None) and `errorIfNone` is true, an exception is thrown. * If `errorIfNone` is false, the path is dropped from the returned Traversable.*/ def x[T](mapper: File => Option[T], errorIfNone: Boolean = true): Traversable[(File,T)] = { val apply = if(errorIfNone) mapper | fail else mapper for(file <- getFiles; mapped <- apply(file)) yield (file, mapped) } /** Pairs each path selected by this PathFinder with its relativePath.*/ def xx: Traversable[(File, String)] = get.map(path => (path.asFile, path.relativePath)) /** Selects all descendent paths with a name that matches include and do not have an intermediate * path with a name that matches intermediateExclude. Typical usage is: * * descendentsExcept("*.jar", ".svn")*/ def descendentsExcept(include: FileFilter, intermediateExclude: FileFilter): PathFinder = (this ** include) --- (this ** intermediateExclude ** include) /** Evaluates this finder. The set returned by this method will reflect the underlying filesystem at the * time of calling. If the filesystem changes, two calls to this method might be different.*/ final def get: immutable.Set[Path] = { val pathSet = new mutable.HashSet[Path] addTo(pathSet) pathSet.toSet } /** Only keeps paths for which `f` returns true. It is non-strict, so it is not evaluated until the returned finder is evaluated.*/ final def filter(f: Path => Boolean): PathFinder = Path.lazyPathFinder(get.filter(f)) /* Non-strict flatMap: no evaluation occurs until the returned finder is evaluated.*/ final def flatMap(f: Path => PathFinder): PathFinder = Path.lazyPathFinder(get.flatMap(p => f(p).get)) /** Evaluates this finder and converts the results to an `Array` of `URL`s..*/ final def getURLs: Array[URL] = Path.getURLs(get) /** Evaluates this finder and converts the results to a `Set` of `File`s.*/ final def getFiles: immutable.Set[File] = Path.getFiles(get) /** Evaluates this finder and converts the results to a `Set` of absolute path strings.*/ final def getPaths: immutable.Set[String] = strictMap(_.absolutePath) /** Evaluates this finder and converts the results to a `Set` of relative path strings.*/ final def getRelativePaths: immutable.Set[String] = strictMap(_.relativePath) final def strictMap[T](f: Path => T): immutable.Set[T] = get.map(f).toSet private[sbt] def addTo(pathSet: mutable.Set[Path]) /** Create a PathFinder from this one where each path has a unique name. * A single path is arbitrarily selected from the set of paths with the same name.*/ def distinct: PathFinder = Path.lazyPathFinder((Map() ++ get.map(p => (p.asFile.getName, p))) .values.toList ) /** Constructs a string by evaluating this finder, converting the resulting Paths to absolute path strings, and joining them with the platform path separator.*/ final def absString = Path.makeString(get) /** Constructs a string by evaluating this finder, converting the resulting Paths to relative path strings, and joining them with the platform path separator.*/ final def relativeString = Path.makeRelativeString(get) /** Constructs a debugging string for this finder by evaluating it and separating paths by newlines.*/ override def toString = get.mkString("\n ", "\n ","") } private class BasePathFinder(base: PathFinder) extends PathFinder { private[sbt] def addTo(pathSet: mutable.Set[Path]) { for(path <- base.get) pathSet += (path ###) } } private abstract class FilterPath extends PathFinder with FileFilter { def parent: PathFinder def filter: FileFilter final def accept(file: File) = filter.accept(file) protected def handlePath(path: Path, pathSet: mutable.Set[Path]) { for(matchedFile <- wrapNull(path.asFile.listFiles(this))) pathSet += path / matchedFile.getName } } private class DescendentOrSelfPathFinder(val parent: PathFinder, val filter: FileFilter) extends FilterPath { private[sbt] def addTo(pathSet: mutable.Set[Path]) { for(path <- parent.get) { if(accept(path.asFile)) pathSet += path handlePathDescendent(path, pathSet) } } private def handlePathDescendent(path: Path, pathSet: mutable.Set[Path]) { handlePath(path, pathSet) for(childDirectory <- wrapNull(path.asFile.listFiles(DirectoryFilter))) handlePathDescendent(path / childDirectory.getName, pathSet) } } private class ChildPathFinder(val parent: PathFinder, val filter: FileFilter) extends FilterPath { private[sbt] def addTo(pathSet: mutable.Set[Path]) { for(path <- parent.get) handlePath(path, pathSet) } } private class Paths(a: PathFinder, b: PathFinder) extends PathFinder { private[sbt] def addTo(pathSet: mutable.Set[Path]) { a.addTo(pathSet) b.addTo(pathSet) } } private class ExcludePaths(include: PathFinder, exclude: PathFinder) extends PathFinder { private[sbt] def addTo(pathSet: mutable.Set[Path]) { val includeSet = new mutable.HashSet[Path] include.addTo(includeSet) val excludeSet = new mutable.HashSet[Path] exclude.addTo(excludeSet) includeSet --= excludeSet pathSet ++= includeSet } }