package sbt
import Using._
import ErrorHandling.translate
import java.io.{BufferedReader, ByteArrayOutputStream, BufferedWriter, File, FileInputStream, InputStream, OutputStream, PrintWriter}
import java.io.{ObjectInputStream, ObjectStreamClass}
import java.net.{URI, URISyntaxException, URL}
import java.nio.charset.Charset
import java.util.Properties
import java.util.jar.{Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest}
import java.util.zip.{CRC32, GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOutputStream}
import scala.collection.immutable.TreeSet
import scala.collection.mutable.{HashMap,HashSet}
import scala.reflect.{Manifest => SManifest}
import Function.tupled
object IO
{
private val MaximumTries = 10
private lazy val random = new java.util.Random
val temporaryDirectory = new File(System.getProperty("java.io.tmpdir"))
private val BufferSize = 8192
val Newline = System.getProperty("line.separator")
val utf8 = Charset.forName("UTF-8")
def classLocation(cl: Class[_]): URL =
{
val codeSource = cl.getProtectionDomain.getCodeSource
if(codeSource == null) error("No class location for " + cl)
else codeSource.getLocation
}
def classLocationFile(cl: Class[_]): File = toFile(classLocation(cl))
def classLocation[T](implicit mf: SManifest[T]): URL = classLocation(mf.erasure)
def classLocationFile[T](implicit mf: SManifest[T]): File = classLocationFile(mf.erasure)
def toFile(url: URL): File =
try { new File(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
def asFile(url: URL): File = urlAsFile(url) getOrElse error("URL is not a file: " + url)
def urlAsFile(url: URL): Option[File] =
url.getProtocol match
{
case "file" => Some(toFile(url))
case "jar" =>
val path = url.getPath
val end = path.indexOf('!')
Some(new File(new URI(if(end == -1) path else path.substring(0, end))))
case _ => None
}
def assertDirectory(file: File) { assert(file.isDirectory, (if(file.exists) "Not a directory: " else "Directory not found: ") + file) }
def assertDirectories(file: File*) { file.foreach(assertDirectory) }
def split(name: String): (String, String) =
{
val lastDot = name.lastIndexOf('.')
if(lastDot >= 0)
(name.substring(0, lastDot), name.substring(lastDot+1))
else
(name, "")
}
def touch(files: Traversable[File]): Unit = files.foreach(f => touch(f))
def touch(file: File, setModified: Boolean = true)
{
val absFile = file.getAbsoluteFile
createDirectory(absFile.getParentFile)
val created = translate("Could not create file " + absFile) { absFile.createNewFile() }
if(created || absFile.isDirectory)
()
else if(setModified && !absFile.setLastModified(System.currentTimeMillis))
error("Could not update last modified time for file " + absFile)
}
def createDirectories(dirs: Traversable[File]): Unit =
dirs.foreach(createDirectory)
def createDirectory(dir: File): Unit =
{
def failBase = "Could not create directory " + dir
var tryCount = 0
while (!dir.exists && !dir.mkdirs() && tryCount < 100) { tryCount += 1 }
if(dir.isDirectory)
()
else if(dir.exists) {
error(failBase + ": file exists and is not a directory.")
}
else
error(failBase)
}
def gzip(in: File, out: File)
{
require(in != out, "Input file cannot be the same as the output file.")
Using.fileInputStream(in) { inputStream =>
Using.fileOutputStream()(out) { outputStream =>
gzip(inputStream, outputStream)
}
}
}
def gzip(input: InputStream, output: OutputStream): Unit =
gzipOutputStream(output) { gzStream => transfer(input, gzStream) }
def gunzip(in: File, out: File)
{
require(in != out, "Input file cannot be the same as the output file.")
Using.fileInputStream(in) { inputStream =>
Using.fileOutputStream()(out) { outputStream =>
gunzip(inputStream, outputStream)
}
}
}
def gunzip(input: InputStream, output: OutputStream): Unit =
gzipInputStream(input) { gzStream => transfer(gzStream, output) }
def unzip(from: File, toDirectory: File, filter: NameFilter = AllPassFilter, preserveLastModified: Boolean = true): Set[File] =
fileInputStream(from)(in => unzipStream(in, toDirectory, filter, preserveLastModified))
def unzipURL(from: URL, toDirectory: File, filter: NameFilter = AllPassFilter, preserveLastModified: Boolean = true): Set[File] =
urlInputStream(from)(in => unzipStream(in, toDirectory, filter, preserveLastModified))
def unzipStream(from: InputStream, toDirectory: File, filter: NameFilter = AllPassFilter, preserveLastModified: Boolean = true): Set[File] =
{
createDirectory(toDirectory)
zipInputStream(from) { zipInput => extract(zipInput, toDirectory, filter, preserveLastModified) }
}
private def extract(from: ZipInputStream, toDirectory: File, filter: NameFilter, preserveLastModified: Boolean) =
{
val set = new HashSet[File]
def next()
{
val entry = from.getNextEntry
if(entry == null)
()
else
{
val name = entry.getName
if(filter.accept(name))
{
val target = new File(toDirectory, name)
if(entry.isDirectory)
createDirectory(target)
else
{
set += target
translate("Error extracting zip entry '" + name + "' to '" + target + "': ") {
fileOutputStream(false)(target) { out => transfer(from, out) }
}
}
if(preserveLastModified)
target.setLastModified(entry.getTime)
}
else
{
}
from.closeEntry()
next()
}
}
next()
Set() ++ set
}
def download(url: URL, to: File) =
Using.urlInputStream(url) { inputStream =>
transfer(inputStream, to)
}
def transfer(in: File, out: File): Unit =
fileInputStream(in){ in => transfer(in, out) }
def transfer(in: File, out: OutputStream): Unit =
fileInputStream(in){ in => transfer(in, out) }
def transfer(in: InputStream, to: File): Unit =
Using.fileOutputStream()(to) { outputStream =>
transfer(in, outputStream)
}
def transfer(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, false)
def transferAndClose(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, true)
private def transferImpl(in: InputStream, out: OutputStream, close: Boolean)
{
try
{
val buffer = new Array[Byte](BufferSize)
def read()
{
val byteCount = in.read(buffer)
if(byteCount >= 0)
{
out.write(buffer, 0, byteCount)
read()
}
}
read()
}
finally { if(close) in.close }
}
def withTemporaryDirectory[T](action: File => T): T =
{
val dir = createTemporaryDirectory
try { action(dir) }
finally { delete(dir) }
}
def createTemporaryDirectory: File = createUniqueDirectory(temporaryDirectory)
def createUniqueDirectory(baseDirectory: File): File =
{
def create(tries: Int): File =
{
if(tries > MaximumTries)
error("Could not create temporary directory.")
else
{
val randomName = "sbt_" + java.lang.Integer.toHexString(random.nextInt)
val f = new File(baseDirectory, randomName)
try { createDirectory(f); f }
catch { case e: Exception => create(tries + 1) }
}
}
create(0)
}
def withTemporaryFile[T](prefix: String, postfix: String)(action: File => T): T =
{
val file = File.createTempFile(prefix, postfix)
try { action(file) }
finally { file.delete() }
}
private[sbt] def jars(dir: File): Iterable[File] = listFiles(dir, GlobFilter("*.jar"))
def deleteIfEmpty(dirs: collection.Set[File]): Unit =
{
val isEmpty = new HashMap[File, Boolean]
def visit(f: File): Boolean = isEmpty.getOrElseUpdate(f, dirs(f) && f.isDirectory && (f.listFiles forall visit) )
dirs foreach visit
for( (f, true) <- isEmpty) f.delete
}
def delete(files: Iterable[File]): Unit = files.foreach(delete)
def deleteFilesEmptyDirs(files: Iterable[File]): Unit =
{
def isEmptyDirectory(dir: File) = dir.isDirectory && listFiles(dir).isEmpty
def parents(fs: Set[File]) = fs.map(_.getParentFile)
def deleteEmpty(dirs: Set[File])
{
val empty = dirs filter isEmptyDirectory
if(empty.nonEmpty)
{
empty foreach { _.delete() }
deleteEmpty(parents(empty))
}
}
delete(files)
deleteEmpty(parents(files.toSet))
}
def delete(file: File)
{
translate("Error deleting file " + file + ": ")
{
val deleted = file.delete()
if(!deleted && file.isDirectory)
{
delete(listFiles(file))
file.delete
}
}
}
def listFiles(filter: java.io.FileFilter)(dir: File): Array[File] = wrapNull(dir.listFiles(filter))
def listFiles(dir: File, filter: java.io.FileFilter): Array[File] = wrapNull(dir.listFiles(filter))
def listFiles(dir: File): Array[File] = wrapNull(dir.listFiles())
private[sbt] def wrapNull(a: Array[File]) =
if(a == null)
new Array[File](0)
else
a
def jar(sources: Traversable[(File,String)], outputJar: File, manifest: Manifest): Unit =
archive(sources.toSeq, outputJar, Some(manifest))
def zip(sources: Traversable[(File,String)], outputZip: File): Unit =
archive(sources.toSeq, outputZip, None)
private def archive(sources: Seq[(File,String)], outputFile: File, manifest: Option[Manifest])
{
if(outputFile.isDirectory)
error("Specified output file " + outputFile + " is a directory.")
else
{
val outputDir = outputFile.getParentFile
createDirectory(outputDir)
withZipOutput(outputFile, manifest)
{ output =>
val createEntry: (String => ZipEntry) = if(manifest.isDefined) new JarEntry(_) else new ZipEntry(_)
writeZip(sources, output)(createEntry)
}
}
}
private def writeZip(sources: Seq[(File,String)], output: ZipOutputStream)(createEntry: String => ZipEntry)
{
val files = sources.flatMap { case (file,name) => if (file.isFile) (file, normalizeName(name)) :: Nil else Nil }
val now = System.currentTimeMillis
val emptyCRC = new CRC32().getValue()
def addDirectoryEntry(name: String)
{
output putNextEntry makeDirectoryEntry(name)
output.closeEntry()
}
def makeDirectoryEntry(name: String) =
{
val e = createEntry(name)
e setTime now
e setSize 0
e setMethod ZipEntry.STORED
e setCrc emptyCRC
e
}
def makeFileEntry(file: File, name: String) =
{
val e = createEntry(name)
e setTime file.lastModified
e
}
def addFileEntry(file: File, name: String)
{
output putNextEntry makeFileEntry(file, name)
transfer(file, output)
output.closeEntry()
}
allDirectoryPaths(files) foreach addDirectoryEntry
files foreach { case (file, name) => addFileEntry(file, name) }
}
private def relativeComponents(path: String): List[String] =
path.split("/").toList.dropRight(1)
private def directories(path: List[String]): List[String] =
path.foldLeft(List(""))( (e,l) => (e.head + l + "/") :: e )
private def directoryPaths(path: String): List[String] =
directories(relativeComponents(path)).filter(_.length > 1)
private def allDirectoryPaths(files: Iterable[(File,String)]) =
TreeSet[String]() ++ (files flatMap { case (file, name) => directoryPaths(name) })
private def normalizeDirName(name: String) =
{
val norm1 = normalizeName(name)
if(norm1.endsWith("/")) norm1 else (norm1 + "/")
}
private def normalizeName(name: String) =
{
val sep = File.separatorChar
if(sep == '/') name else name.replace(sep, '/')
}
private def withZipOutput(file: File, manifest: Option[Manifest])(f: ZipOutputStream => Unit)
{
fileOutputStream(false)(file) { fileOut =>
val (zipOut, ext) =
manifest match
{
case Some(mf) =>
{
import Attributes.Name.MANIFEST_VERSION
val main = mf.getMainAttributes
if(!main.containsKey(MANIFEST_VERSION))
main.put(MANIFEST_VERSION, "1.0")
(new JarOutputStream(fileOut, mf), "jar")
}
case None => (new ZipOutputStream(fileOut), "zip")
}
try { f(zipOut) }
finally { zipOut.close }
}
}
def relativize(base: File, file: File): Option[String] =
{
val pathString = file.getAbsolutePath
baseFileString(base) flatMap
{
baseString =>
{
if(pathString.startsWith(baseString))
Some(pathString.substring(baseString.length))
else
None
}
}
}
private def baseFileString(baseFile: File): Option[String] =
{
if(baseFile.isDirectory)
{
val cp = baseFile.getAbsolutePath
assert(cp.length > 0)
val normalized = if(cp.charAt(cp.length - 1) == File.separatorChar) cp else cp + File.separatorChar
Some(normalized)
}
else
None
}
def copy(sources: Traversable[(File,File)], overwrite: Boolean = false, preserveLastModified: Boolean = false): Set[File] =
sources.map( tupled(copyImpl(overwrite, preserveLastModified)) ).toSet
private def copyImpl(overwrite: Boolean, preserveLastModified: Boolean)(from: File, to: File): File =
{
if(overwrite || !to.exists || from.lastModified > to.lastModified)
{
if(from.isDirectory)
createDirectory(to)
else
{
createDirectory(to.getParentFile)
copyFile(from, to, preserveLastModified)
}
}
to
}
def copyDirectory(source: File, target: File, overwrite: Boolean = false, preserveLastModified: Boolean = false): Unit =
copy( (PathFinder(source) ***) x Path.rebase(source, target), overwrite, preserveLastModified)
def copyFile(sourceFile: File, targetFile: File, preserveLastModified: Boolean = false)
{
require(sourceFile.exists, "Source file '" + sourceFile.getAbsolutePath + "' does not exist.")
require(!sourceFile.isDirectory, "Source file '" + sourceFile.getAbsolutePath + "' is a directory.")
fileInputChannel(sourceFile) { in =>
fileOutputChannel(targetFile) { out =>
val copied = out.transferFrom(in, 0, in.size)
if(copied != in.size)
error("Could not copy '" + sourceFile + "' to '" + targetFile + "' (" + copied + "/" + in.size + " bytes copied)")
}
}
if(preserveLastModified)
copyLastModified(sourceFile, targetFile)
}
def copyLastModified(sourceFile: File, targetFile: File) = targetFile.setLastModified( sourceFile.lastModified )
def defaultCharset = utf8
def write(file: File, content: String, charset: Charset = defaultCharset, append: Boolean = false): Unit =
writer(file, content, charset, append) { _.write(content) }
def writer[T](file: File, content: String, charset: Charset, append: Boolean = false)(f: BufferedWriter => T): T =
{
if(charset.newEncoder.canEncode(content))
fileWriter(charset, append)(file) { f }
else
error("String cannot be encoded by charset " + charset.name)
}
def reader[T](file: File, charset: Charset = defaultCharset)(f: BufferedReader => T): T =
fileReader(charset)(file) { f }
def read(file: File, charset: Charset = defaultCharset): String =
{
val out = new ByteArrayOutputStream(file.length.toInt)
transfer(file, out)
out.toString(charset.name)
}
def readStream(in: InputStream, charset: Charset = defaultCharset): String =
{
val out = new ByteArrayOutputStream
transfer(in, out)
out.toString(charset.name)
}
def readBytes(file: File): Array[Byte] = fileInputStream(file)(readBytes)
def readBytes(in: InputStream): Array[Byte] =
{
val out = new ByteArrayOutputStream
transfer(in, out)
out.toByteArray
}
def append(file: File, content: String, charset: Charset = defaultCharset): Unit =
write(file, content, charset, true)
def append(file: File, bytes: Array[Byte]): Unit =
writeBytes(file, bytes, true)
def write(file: File, bytes: Array[Byte]): Unit =
writeBytes(file, bytes, false)
private def writeBytes(file: File, bytes: Array[Byte], append: Boolean): Unit =
fileOutputStream(append)(file) { _.write(bytes) }
def readLinesURL(url: URL, charset: Charset = defaultCharset): List[String] =
urlReader(charset)(url)(readLines)
def readLines(file: File, charset: Charset = defaultCharset): List[String] =
fileReader(charset)(file)(readLines)
def readLines(in: BufferedReader): List[String] =
foldLines[List[String]](in, Nil)( (accum, line) => line :: accum ).reverse
def foreachLine(in: BufferedReader)(f: String => Unit): Unit =
foldLines(in, ())( (_, line) => f(line) )
def foldLines[T](in: BufferedReader, init: T)(f: (T, String) => T): T =
{
def readLine(accum: T): T =
{
val line = in.readLine()
if(line eq null) accum else readLine(f(accum, line))
}
readLine(init)
}
def writeLines(file: File, lines: Seq[String], charset: Charset = defaultCharset, append: Boolean = false): Unit =
writer(file, lines.headOption.getOrElse(""), charset, append) { w =>
lines.foreach { line => w.write(line); w.newLine() }
}
def writeLines(writer: PrintWriter, lines: Seq[String]): Unit =
lines foreach writer.println
def write(properties: Properties, label: String, to: File) =
fileOutputStream()(to) { output => properties.store(output, label) }
def load(properties: Properties, from: File): Unit =
if(from.exists)
fileInputStream(from){ input => properties.load(input) }
private val PathSeparatorPattern = java.util.regex.Pattern.compile(File.pathSeparator)
def pathSplit(s: String) = PathSeparatorPattern.split(s)
def stash[T](files: Set[File])(f: => T): T =
withTemporaryDirectory { dir =>
val stashed = stashLocations(dir, files.toArray)
move(stashed)
try { f } catch { case e: Exception =>
try { move(stashed.map(_.swap)); throw e }
catch { case _: Exception => throw e }
}
}
private def stashLocations(dir: File, files: Array[File]) =
for( (file, index) <- files.zipWithIndex) yield
(file, new File(dir, index.toHexString))
def move(files: Traversable[(File, File)]): Unit =
files.foreach(Function.tupled(move))
def move(a: File, b: File): Unit =
{
if(b.exists)
delete(b)
createDirectory(b.getParentFile)
if(!a.renameTo(b))
{
copyFile(a, b, true)
delete(a)
}
}
def gzipFileOut[T](file: File)(f: OutputStream => T): T =
Using.fileOutputStream()(file) { fout =>
Using.gzipOutputStream(fout) { outg =>
Using.bufferedOutputStream(outg)(f) }}
def gzipFileIn[T](file: File)(f: InputStream => T): T =
Using.fileInputStream(file) { fin =>
Using.gzipInputStream(fin) { ing =>
Using.bufferedInputStream(ing)(f) }}
def directoryURI(dir: File): URI =
{
assertAbsolute(dir)
directoryURI(dir.toURI.normalize)
}
def directoryURI(uri: URI): URI =
{
if(!uri.isAbsolute) return uri;
val str = uri.toASCIIString
val dirStr = if(str.endsWith("/") || uri.getScheme != "file") str else str + "/"
(new URI(dirStr)).normalize
}
def toURI(f: File): URI = if(f.isAbsolute) f.toURI else new URI(normalizeName(f.getPath))
def resolve(base: File, f: File): File =
{
assertAbsolute(base)
val fabs = if(f.isAbsolute) f else new File(directoryURI(new File(base, f.getPath)))
assertAbsolute(fabs)
fabs
}
def assertAbsolute(f: File) = assert(f.isAbsolute, "Not absolute: " + f)
def assertAbsolute(uri: URI) = assert(uri.isAbsolute, "Not absolute: " + uri)
def parseClasspath(s: String): Seq[File] = IO.pathSplit(s).map(new File(_)).toSeq
def objectInputStream(wrapped: InputStream, loader: ClassLoader): ObjectInputStream = new ObjectInputStream(wrapped)
{
override def resolveClass(osc: ObjectStreamClass): Class[_] =
{
val c = Class.forName(osc.getName, false, loader)
if(c eq null) super.resolveClass(osc) else c
}
}
}