Kotlin I/O review/M11 fixes: Stream --> Sequence, recurse() returned back,

additional helpers like File.bufferedReader() and String.byteInputStream(),
copyRecursively / deleteRecursively were rewritten using FileTreeWalk,
FilePathComponents introduced as a replacement of FileIterator,
classes / methods / properties permissions fixed, Linux specific things,
resolveSibling rewritten using FilePathComponents
This commit is contained in:
Mikhail Glukhikh
2015-03-18 18:52:18 +03:00
parent f560677b15
commit 5b636eef7b
10 changed files with 507 additions and 413 deletions
+29
View File
@@ -9,42 +9,61 @@ import java.io.BufferedReader
*/ */
public val defaultBufferSize: Int = 64 * 1024 public val defaultBufferSize: Int = 64 * 1024
/**
* Returns the default block size for forEachBlock()
*/
public val defaultBlockSize: Int = 4096
/**
* Returns the minimum block size for forEachBlock()
*/
public val minimumBlockSize: Int = 512
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Any?) { public fun print(message: Any?) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Int) { public fun print(message: Int) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Long) { public fun print(message: Long) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Byte) { public fun print(message: Byte) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Short) { public fun print(message: Short) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Char) { public fun print(message: Char) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Boolean) { public fun print(message: Boolean) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Float) { public fun print(message: Float) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: Double) { public fun print(message: Double) {
System.out.print(message) System.out.print(message)
} }
/** Prints the given message to the standard output stream. */ /** Prints the given message to the standard output stream. */
public fun print(message: CharArray) { public fun print(message: CharArray) {
System.out.print(message) System.out.print(message)
@@ -54,42 +73,52 @@ public fun print(message: CharArray) {
public fun println(message: Any?) { public fun println(message: Any?) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Int) { public fun println(message: Int) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Long) { public fun println(message: Long) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Byte) { public fun println(message: Byte) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Short) { public fun println(message: Short) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Char) { public fun println(message: Char) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Boolean) { public fun println(message: Boolean) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Float) { public fun println(message: Float) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: Double) { public fun println(message: Double) {
System.out.println(message) System.out.println(message)
} }
/** Prints the given message and newline to the standard output stream. */ /** Prints the given message and newline to the standard output stream. */
public fun println(message: CharArray) { public fun println(message: CharArray) {
System.out.println(message) System.out.println(message)
} }
/** Prints a newline to the standard output stream. */ /** Prints a newline to the standard output stream. */
public fun println() { public fun println() {
System.out.println() System.out.println()
+1 -1
View File
@@ -20,7 +20,7 @@ private fun constructMessage(file: File, other: File?, reason: String?): String
open public class FileSystemException(public val file: File, open public class FileSystemException(public val file: File,
public val other: File? = null, public val other: File? = null,
public val reason: String? = null public val reason: String? = null
) : IOException(constructMessage(file, other, reason)) ) : IOException(constructMessage(file, other, reason))
/** /**
* An exception class which is used when some file to create or copy to already exists * An exception class which is used when some file to create or copy to already exists
+39 -5
View File
@@ -4,15 +4,40 @@ import java.io.*
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.CharsetDecoder import java.nio.charset.CharsetDecoder
import java.nio.charset.CharsetEncoder import java.nio.charset.CharsetEncoder
import java.util.NoSuchElementException
// Note: hasNext() is incorrect because available() means "number of available bytes WITHOUT BLOCKING"
/** Returns an [Iterator] of bytes in this input stream. */ /** Returns an [Iterator] of bytes in this input stream. */
deprecated("It's not recommended to iterate through input stream bytes")
public fun InputStream.iterator(): ByteIterator = public fun InputStream.iterator(): ByteIterator =
object : ByteIterator() { object : ByteIterator() {
override fun hasNext(): Boolean = available() > 0
public override fun nextByte(): Byte = read().toByte() var nextByte = -1
var nextPrepared = false
var finished = false
private fun prepareNext() {
if (!nextPrepared && !finished) {
nextByte = read()
nextPrepared = true
finished = (nextByte == -1)
}
}
public override fun hasNext(): Boolean {
prepareNext()
return !finished
}
public override fun nextByte(): Byte {
prepareNext()
if (finished)
throw NoSuchElementException("Input stream is over")
val res = nextByte.toByte()
nextPrepared = false
return res
}
} }
/** /**
@@ -28,12 +53,21 @@ else
/** Creates a reader on this input stream using UTF-8 or the specified [charset]. */ /** Creates a reader on this input stream using UTF-8 or the specified [charset]. */
public fun InputStream.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = InputStreamReader(this, charset) public fun InputStream.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = InputStreamReader(this, charset)
/** Creates a buffered reader on this input stream using UTF-8 or the specified [charset]. */
public fun InputStream.bufferedReader(charset: Charset = Charsets.UTF_8): BufferedReader = reader(charset).buffered()
/** Creates a reader on this input stream using the specified [charset]. */ /** Creates a reader on this input stream using the specified [charset]. */
public fun InputStream.reader(charset: String): InputStreamReader = InputStreamReader(this, charset) public fun InputStream.reader(charset: String): InputStreamReader = InputStreamReader(this, charset)
/** Creates a buffered reader on this input stream using the specified [charset]. */
public fun InputStream.bufferedReader(charset: String): BufferedReader = reader(charset).buffered()
/** Creates a reader on this input stream using the specified [decoder]. */ /** Creates a reader on this input stream using the specified [decoder]. */
public fun InputStream.reader(decoder: CharsetDecoder): InputStreamReader = InputStreamReader(this, decoder) public fun InputStream.reader(decoder: CharsetDecoder): InputStreamReader = InputStreamReader(this, decoder)
/** Creates a reader on this input stream using the specified [decoder]. */
public fun InputStream.bufferedReader(decoder: CharsetDecoder): BufferedReader = reader(decoder).buffered()
/** /**
* Creates a buffered output stream wrapping this stream. * Creates a buffered output stream wrapping this stream.
* @param bufferSize the buffer size to use. * @param bufferSize the buffer size to use.
@@ -74,7 +108,7 @@ public fun InputStream.copyTo(out: OutputStream, bufferSize: Int = defaultBuffer
*/ */
public fun InputStream.readBytes(estimatedSize: Int = defaultBufferSize): ByteArray { public fun InputStream.readBytes(estimatedSize: Int = defaultBufferSize): ByteArray {
val buffer = ByteArrayOutputStream(estimatedSize) val buffer = ByteArrayOutputStream(estimatedSize)
this.copyTo(buffer) copyTo(buffer)
return buffer.toByteArray() return buffer.toByteArray()
} }
+72 -44
View File
@@ -7,10 +7,34 @@ import java.util.ArrayList
import java.util.NoSuchElementException import java.util.NoSuchElementException
/** /**
* @return a new [FileReader] for reading the contents of this file. * Returns a new [FileReader] for reading the content of this file.
*/ */
public fun File.reader(): FileReader = FileReader(this) public fun File.reader(): FileReader = FileReader(this)
/**
* Returns a new [BufferedReader] for reading the content of this file.
*
* @param bufferSize necessary size of the buffer
*/
public fun File.bufferedReader(bufferSize: Int = defaultBufferSize): BufferedReader = reader().buffered(bufferSize)
/**
* Returns a new [FileWriter] for writing the content of this file.
*/
public fun File.writer(): FileWriter = FileWriter(this)
/**
* Returns a new [BufferedWriter] for writing the content of this file.
*
* @param bufferSize necessary size of the buffer
*/
public fun File.bufferedWriter(bufferSize: Int = defaultBufferSize): BufferedWriter = writer().buffered(bufferSize)
/**
* Returns a new [PrintWriter] for writing the content of this file.
*/
public fun File.printWriter(): PrintWriter = PrintWriter(writer().buffered())
/** /**
* Reads the entire content of this file as a byte array. * Reads the entire content of this file as a byte array.
* *
@@ -23,9 +47,9 @@ public fun File.readBytes(): ByteArray {
} }
/** /**
* Replaces the contents of this file with [data]. * Replaces the content of this file with [data].
* *
* @data byte array to write into this file * @param data byte array to write into this file
*/ */
deprecated("Use replaceBytes instead") deprecated("Use replaceBytes instead")
public fun File.writeBytes(data: ByteArray): Unit { public fun File.writeBytes(data: ByteArray): Unit {
@@ -33,18 +57,18 @@ public fun File.writeBytes(data: ByteArray): Unit {
} }
/** /**
* Replaces the contents of this file with [data]. * Replaces the content of this file with [data].
* *
* @data byte array to write into this file * @param data byte array to write into this file
*/ */
public fun File.replaceBytes(data: ByteArray): Unit { public fun File.replaceBytes(data: ByteArray): Unit {
return FileOutputStream(this).use { it.write(data) } return FileOutputStream(this).use { it.write(data) }
} }
/** /**
* Appends [data] to the contents of this file. * Appends [data] to the content of this file.
* *
* @data byte array to append to this file * @param data byte array to append to this file
*/ */
public fun File.appendBytes(data: ByteArray): Unit { public fun File.appendBytes(data: ByteArray): Unit {
return FileOutputStream(this, true).use { it.write(data) } return FileOutputStream(this, true).use { it.write(data) }
@@ -58,7 +82,6 @@ public fun File.appendBytes(data: ByteArray): Unit {
* @param charset character set to use * @param charset character set to use
* @return the entire content of this file as a String * @return the entire content of this file as a String
*/ */
deprecated("There is a same function with default parameter value")
public fun File.readText(charset: String): String = readBytes().toString(charset) public fun File.readText(charset: String): String = readBytes().toString(charset)
/** /**
@@ -72,7 +95,7 @@ public fun File.readText(charset: String): String = readBytes().toString(charset
public fun File.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset) public fun File.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)
/** /**
* Replaces the contents of this file with [text] encoded using the specified [charset]. * Replaces the content of this file with [text] encoded using the specified [charset].
* *
* @param text text to write into file * @param text text to write into file
* @param charset character set to use * @param charset character set to use
@@ -83,7 +106,7 @@ public fun File.writeText(text: String, charset: String): Unit {
} }
/** /**
* Replaces the contents of this file with [text] encoded using the specified [charset]. * Replaces the content of this file with [text] encoded using the specified [charset].
* *
* @param text text to write into file * @param text text to write into file
* @param charset character set to use * @param charset character set to use
@@ -94,7 +117,7 @@ public fun File.replaceText(text: String, charset: String): Unit {
} }
/** /**
* Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset]. * Replaces the content of this file with [text] encoded using UTF-8 or specified [charset].
* *
* @param text text to write into file * @param text text to write into file
* @param charset character set to use * @param charset character set to use
@@ -105,7 +128,7 @@ public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit
} }
/** /**
* Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset]. * Replaces the content of this file with [text] encoded using UTF-8 or specified [charset].
* *
* @param text text to write into file * @param text text to write into file
* @param charset character set to use * @param charset character set to use
@@ -115,7 +138,7 @@ public fun File.replaceText(text: String, charset: Charset = Charsets.UTF_8): Un
} }
/** /**
* Appends [text] to the contents of this file using UTF-8 or the specified [charset]. * Appends [text] to the content of this file using UTF-8 or the specified [charset].
* *
* @param text text to append to file * @param text text to append to file
* @param charset character set to use * @param charset character set to use
@@ -125,35 +148,45 @@ public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Uni
} }
/** /**
* Appends [text] to the contents of the file using the specified [charset]. * Appends [text] to the content of the file using the specified [charset].
* *
* @param text text to append to file * @param text text to append to file
* @param charset character set to use * @param charset character set to use
*/ */
deprecated("There is a same function with default parameter value")
public fun File.appendText(text: String, charset: String): Unit { public fun File.appendText(text: String, charset: String): Unit {
appendBytes(text.toByteArray(charset)) appendBytes(text.toByteArray(charset))
} }
/** /**
* Reads file by byte blocks and calls [closure] for each block read. Block size depends on implementation * Reads file by byte blocks and calls [closure] for each block read.
* but is never less than 512 bytes. * Block has default size which is implementation-dependent.
* This functions passes the byte array and amount of bytes in the array to the [closure] function. * This functions passes the byte array and amount of bytes in the array to the [closure] function.
* *
* You can use this function for huge files. * You can use this function for huge files.
* *
* @param closure function to proceed file blocks * @param closure function to proceed file blocks
*/ */
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit { public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit = forEachBlock(closure, defaultBlockSize)
val arr = ByteArray(4096)
/**
* Reads file by byte blocks and calls [closure] for each block read.
* This functions passes the byte array and amount of bytes in the array to the [closure] function.
*
* You can use this function for huge files.
*
* @param closure function to proceed file blocks
* @param blockSize size of a block, replaced by 512 if it's less, 4096 by default
*/
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit, blockSize: Int): Unit {
val arr = ByteArray(if (blockSize < minimumBlockSize) minimumBlockSize else blockSize)
val fis = FileInputStream(this) val fis = FileInputStream(this)
try { try {
do { do {
val size = fis.read(arr) val size = fis.read(arr)
if (size == -1) { if (size <= 0) {
break break
} else if (size > 0) { } else {
closure(arr, size) closure(arr, size)
} }
} while (true) } while (true)
@@ -172,12 +205,8 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit {
* @param closure function to proceed file lines * @param closure function to proceed file lines
*/ */
public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: String) -> Unit): Unit { public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: String) -> Unit): Unit {
val reader = BufferedReader(InputStreamReader(FileInputStream(this), charset)) // Note: close is called at forEachLine
try { BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(closure)
reader.forEachLine(closure)
} finally {
reader.close()
}
} }
/** /**
@@ -188,7 +217,6 @@ public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: S
* @param charset character set to use * @param charset character set to use
* @param closure function to proceed file lines * @param closure function to proceed file lines
*/ */
deprecated("There is a same function with default parameter value")
public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): Unit = forEachLine(Charset.forName(charset), closure) public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): Unit = forEachLine(Charset.forName(charset), closure)
/** /**
@@ -199,7 +227,6 @@ public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): U
* @param charset character set to use * @param charset character set to use
* @return list of file lines * @return list of file lines
*/ */
deprecated("There is a same function with default parameter value")
public fun File.readLines(charset: String): List<String> = readLines(Charset.forName(charset)) public fun File.readLines(charset: String): List<String> = readLines(Charset.forName(charset))
/** /**
@@ -216,11 +243,11 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
return result return result
} }
/** @return a buffered reader wrapping this Reader, or this Reader itself if it is already buffered. */ /** Returns a buffered reader wrapping this Reader, or this Reader itself if it is already buffered. */
public fun Reader.buffered(bufferSize: Int = defaultBufferSize): BufferedReader public fun Reader.buffered(bufferSize: Int = defaultBufferSize): BufferedReader
= if (this is BufferedReader) this else BufferedReader(this, bufferSize) = if (this is BufferedReader) this else BufferedReader(this, bufferSize)
/** @return a buffered reader wrapping this Writer, or this Writer itself if it is already buffered. */ /** Returns a buffered reader wrapping this Writer, or this Writer itself if it is already buffered. */
public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter
= if (this is BufferedWriter) this else BufferedWriter(this, bufferSize) = if (this is BufferedWriter) this else BufferedWriter(this, bufferSize)
@@ -230,7 +257,7 @@ public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter
* *
* @param block function to proceed file lines * @param block function to proceed file lines
*/ */
public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines -> lines.forEach(block) } public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { it.forEach(block) }
/** /**
* Calls the [block] callback giving it a sequence of all the lines in this file and closes the reader once * Calls the [block] callback giving it a sequence of all the lines in this file and closes the reader once
@@ -238,29 +265,31 @@ public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines
* @return the value returned by [block]. * @return the value returned by [block].
*/ */
public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T = public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
this.buffered().use { block(it.lines()) } buffered().use { block(it.lines()) }
/** /**
* @return a stream of corresponding file lines * Returns a sequence of corresponding file lines
* *
* *Note*: the caller must close the underlying `BufferedReader` * *Note*: the caller must close the underlying `BufferedReader`
* when the iteration is finished; as the user may not complete the iteration loop (e.g. using a method like find() or any() on the iterator * when the iteration is finished; as the user may not complete the iteration loop (e.g. using a method like find() or any() on the iterator
* may terminate the iteration early. * may terminate the iteration early.
* *
* We suggest you try the method [useLines] instead which closes the stream when the processing is complete. * We suggest you try the method [useLines] instead which closes the stream when the processing is complete.
*
* @return a sequence of corresponding file lines
*/ */
public fun BufferedReader.lines(): Sequence<String> = LinesStream(this) public fun BufferedReader.lines(): Sequence<String> = LinesStream(this)
deprecated("Use lines() function which returns Stream<String>") deprecated("Use lines() function which returns Sequence<String>")
public fun BufferedReader.lineIterator(): Iterator<String> = lines().iterator() public fun BufferedReader.lineIterator(): Iterator<String> = lines().iterator()
private class LinesStream(private val reader: BufferedReader) : Sequence<String> { private class LinesStream(private val reader: BufferedReader) : Sequence<String> {
override fun iterator(): Iterator<String> { override public fun iterator(): Iterator<String> {
return object : Iterator<String> { return object : Iterator<String> {
private var nextValue: String? = null private var nextValue: String? = null
private var done = false private var done = false
override fun hasNext(): Boolean { override public fun hasNext(): Boolean {
if (nextValue == null && !done) { if (nextValue == null && !done) {
nextValue = reader.readLine() nextValue = reader.readLine()
if (nextValue == null) done = true if (nextValue == null) done = true
@@ -268,7 +297,7 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
return nextValue != null return nextValue != null
} }
public override fun next(): String { override public fun next(): String {
if (!hasNext()) { if (!hasNext()) {
throw NoSuchElementException() throw NoSuchElementException()
} }
@@ -285,7 +314,7 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
* *
* *Note*: It is the caller's responsibility to close this resource. * *Note*: It is the caller's responsibility to close this resource.
* *
* @return the string with corresponding file contents * @return the string with corresponding file content
*/ */
public fun Reader.readText(): String { public fun Reader.readText(): String {
val buffer = StringWriter() val buffer = StringWriter()
@@ -322,7 +351,6 @@ public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long
* @param charset a character set to use * @param charset a character set to use
* @return a string with this URL entire content * @return a string with this URL entire content
*/ */
deprecated("There is a same function with default parameter value")
public fun URL.readText(charset: String): String = readBytes().toString(charset) public fun URL.readText(charset: String): String = readBytes().toString(charset)
/** /**
@@ -342,13 +370,13 @@ public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes()
* *
* @return a byte array with this URL entire content * @return a byte array with this URL entire content
*/ */
public fun URL.readBytes(): ByteArray = this.openStream()!!.use<InputStream, ByteArray>{ it.readBytes() } public fun URL.readBytes(): ByteArray = openStream().use<InputStream, ByteArray>{ it.readBytes() }
/** /**
* Executes the given [block] function on this resource and then closes it down correctly whether an exception * Executes the given [block] function on this resource and then closes it down correctly whether an exception
* is thrown or not * is thrown or not
* *
* @block a function to proceed this closable resource * @param block a function to proceed this closable resource
* @return the result of [block] function on this closable resource * @return the result of [block] function on this closable resource
*/ */
public inline fun <T : Closeable, R> T.use(block: (T) -> R): R { public inline fun <T : Closeable, R> T.use(block: (T) -> R): R {
@@ -358,7 +386,7 @@ public inline fun <T : Closeable, R> T.use(block: (T) -> R): R {
} catch (e: Exception) { } catch (e: Exception) {
closed = true closed = true
try { try {
this.close() close()
} catch (closeException: Exception) { } catch (closeException: Exception) {
// eat the closeException as we are already throwing the original cause // eat the closeException as we are already throwing the original cause
// and we don't want to mask the real exception // and we don't want to mask the real exception
@@ -371,7 +399,7 @@ public inline fun <T : Closeable, R> T.use(block: (T) -> R): R {
throw e throw e
} finally { } finally {
if (!closed) { if (!closed) {
this.close() close()
} }
} }
} }
@@ -1,138 +0,0 @@
package kotlin.io
import java.io.File
import java.util.NoSuchElementException
fun String.getRootName(): String {
var first = this.indexOf(File.separatorChar, 0)
if (first==0) {
if (length() > 1 && this[1]==File.separatorChar) {
// Network names like //my.host/home/something => //my.host/home/ should be root
first = this.indexOf(File.separatorChar, 2)
if (first>=0) {
first = this.indexOf(File.separatorChar, first+1)
if (first >= 0)
return this.substring(0, first+1)
}
}
return this.substring(0, 1)
}
// C:\
if (first > 0 && this[first-1]==':') {
first++
return this.substring(0, first)
}
// C:
if (first==-1 && this.endsWith(':'))
return this
return ""
}
val File.rootName: String
get() = separatorsToSystem().getRootName()
/**
* Returns root component of this abstract name, like / from /home/user, or C:\ from C:\file.tmp,
* or //my.host/home for //my.host/home/user,
* or null if this name is relative, like bar/gav
*/
public val File.root: File?
get() {
val name = rootName
return if (name.length() > 0) File(name) else null
}
/**
* Iterates over elements of file, e.g. for /foo/bar/gav
* we will have foo, bar and gav
*/
class FileIterator(private val f: File): Iterator<File> {
private val path = f.separatorsToSystem()
val root = path.getRootName()
// Begin from the last root character
private var separatorIndex = root.length()-1
private var nextIndex = separatorIndex
/**
* Returns the next element for this file iterator
* @throws NoSuchElementException if there is no next element
*/
override fun next(): File {
if (!hasNext()) {
throw NoSuchElementException()
}
val prevIndex = separatorIndex
separatorIndex = nextIndex
return File(path.substring(prevIndex+1, nextIndex))
}
/**
* Returns true if the next element is available for this file iterator,
* or false otherwise
*/
override fun hasNext(): Boolean {
if (nextIndex == separatorIndex) {
// Consider special "" case
if (separatorIndex==-1 && path.length()==0) {
nextIndex = 0
return true
}
// /foo/bar/gav/, next points to last /
if (nextIndex >= path.length()-1)
return false
nextIndex = path.indexOf(File.separatorChar, separatorIndex+1)
// To remove all "" like in the middle of /home//something/
if (nextIndex==separatorIndex+1) {
separatorIndex = nextIndex
return hasNext()
}
}
if (nextIndex == -1)
nextIndex = path.length()
return true
}
}
/**
* Contains number of components in this abstract path name,
* e.g. 3 in /home/user/tmp (home, user, tmp) or 0 in /.
* Empty path name has one component which is the empty name itself.
*/
public val File.nameCount: Int
get() {
var i = 0
for (elem in this) i++
return i
}
/**
* Returns a relative pathname which is a subsequence of this pathname,
* beginning from component [beginIndex], inclusive,
* ending at component [endIndex], exclusive.
* Number 0 belongs to a component closest to the root,
* number count-1 belongs to a component farthest from the root
* @throws IllegalArgumentException if [beginIndex] is negative,
* or [endIndex] is greater than existing number of components,
* or [beginIndex] is greater than or equals to [endIndex]
*/
public fun File.subPath(beginIndex: Int, endIndex: Int): File {
if (beginIndex < 0 || beginIndex >= endIndex || endIndex > nameCount)
throw IllegalArgumentException()
var res = ""
var i = 0
for (elem in this) {
if (i >= beginIndex && i < endIndex) {
res += elem.toString()
if (i != endIndex-1)
res += File.separatorChar
}
i++
}
return File(res)
}
@@ -0,0 +1,100 @@
package kotlin.io
import java.io.File
import java.util.NoSuchElementException
private fun String.getRootName(): String {
// Note: separators should be already replaced to system ones
var first = indexOf(File.separatorChar, 0)
println("$this.getRootName: first=$first")
if (first == 0) {
if (length() > 1) {
// Network names like //my.host/home/something ? => //my.host/home/ should be root
// We have to consider here also /my.host/home/something because on Unix
// File converts // just to /
first = indexOf(File.separatorChar, 2)
if (first >= 0) {
val dot = indexOf('.', 2)
if (dot >= 0 && dot < first) {
first = indexOf(File.separatorChar, first + 1)
if (first >= 0)
return substring(0, first + 1)
}
}
}
return substring(0, 1)
}
// C:\
if (first > 0 && this[first - 1] == ':') {
first++
return substring(0, first)
}
// C:
if (first == -1 && endsWith(':'))
return this
return ""
}
/**
* Returns a string representation of root component of this abstract name, like / from /home/user, or C:\ from C:\file.tmp,
* or //my.host/home for //my.host/home/user,
* or empty string if this name is relative, like bar/gav
*/
public val File.rootName: String
get() = separatorsToSystem().getRootName()
/**
* Returns root component of this abstract name, like / from /home/user, or C:\ from C:\file.tmp,
* or //my.host/home for //my.host/home/user,
* or null if this name is relative, like bar/gav
*/
public val File.root: File?
get() {
val name = rootName
return if (name.length() > 0) File(name) else null
}
public data class FilePathComponents(public val rootName: String, public val fileList: List<File>) {
public val size: Int = fileList.size()
public fun subPath(beginIndex: Int, endIndex: Int): File {
if (beginIndex < 0 || beginIndex >= endIndex || endIndex > size)
throw IllegalArgumentException()
var res = ""
var first = true
for (elem in fileList.subList(beginIndex, endIndex)) {
if (!first) {
res += File.separatorChar
} else {
first = false
}
res += elem.toString()
}
return File(res)
}
}
public fun File.filePathComponents(): FilePathComponents {
val path = separatorsToSystem()
val rootName = path.getRootName()
val subPath = path.substring(rootName.length())
// if: a special case when we have only root component
// Split not only by / or \, but also by //, ///, \\, \\\, etc.
val list = if (rootName.length() > 0 && subPath.isEmpty()) listOf() else
subPath.split("""\Q${File.separatorChar}\E+""").toList().map { it -> File(it) }
return FilePathComponents(rootName, list)
}
/**
* Returns a relative pathname which is a subsequence of this pathname,
* beginning from component [beginIndex], inclusive,
* ending at component [endIndex], exclusive.
* Number 0 belongs to a component closest to the root,
* number count-1 belongs to a component farthest from the root
* @throws IllegalArgumentException if [beginIndex] is negative,
* or [endIndex] is greater than existing number of components,
* or [beginIndex] is greater than or equals to [endIndex]
*/
public fun File.subPath(beginIndex: Int, endIndex: Int): File = filePathComponents().subPath(beginIndex, endIndex)
@@ -1,4 +1,4 @@
package kotlin.io.files package kotlin.io
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -13,7 +13,12 @@ import java.util.Stack
* An alternative version of file walker * An alternative version of file walker
*/ */
enum class FileWalkDirection { /**
* An enumeration to describe possible walk directions.
* There are two of them: beginning from parents, ending with children,
* and beginning from children, ending with parents. Both use depth-first search.
*/
public enum class FileWalkDirection {
/** Depth-first search, directory is visited BEFORE its files */ /** Depth-first search, directory is visited BEFORE its files */
TOP_DOWN TOP_DOWN
/** Depth-first search, directory is visited AFTER its files */ /** Depth-first search, directory is visited AFTER its files */
@@ -21,24 +26,31 @@ enum class FileWalkDirection {
// Do we want also breadth-first search? // Do we want also breadth-first search?
} }
class FileTreeWalk(private val start: File, /**
private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN, * This class is intended to implement different file walk methods.
private val enter: (File) -> Unit = {}, * It allows to iterate through all files inside [start] directory.
private val leave: (File) -> Unit = {}, * If [start] is just a file, walker iterates only it.
private val fail: (f: File, e: IOException) -> Unit = { f, e -> Unit }, * If [start] does not exist, walker does not do any iterations at all.
private val filter: (File) -> Boolean = { it -> true }, */
private val maxDepth: Int = Int.MAX_VALUE public class FileTreeWalk(private val start: File,
): Stream<File> { private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
private val enter: (File) -> Unit = {},
private val leave: (File) -> Unit = {},
private val fail: (f: File, e: IOException) -> Unit = { f, e -> Unit },
private val filter: (File) -> Boolean = { true },
private val maxDepth: Int = Int.MAX_VALUE
) : Sequence<File> {
private abstract class DirectoryState(val rootDir: File) { private abstract class DirectoryState(public val rootDir: File) {
init { init {
if (!rootDir.isDirectory()) if (!rootDir.isDirectory())
throw IllegalArgumentException("Directory is needed") throw IllegalArgumentException("Directory is needed")
} }
abstract public fun step(): File? abstract public fun step(): File?
} }
private inner class BottomUpDirectoryState(rootDir: File): DirectoryState(rootDir) { private inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) {
private var rootVisited = false private var rootVisited = false
@@ -72,7 +84,7 @@ class FileTreeWalk(private val start: File,
} }
} }
private inner class TopDownDirectoryState(rootDir: File): DirectoryState(rootDir) { private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {
private var rootVisited = false private var rootVisited = false
@@ -119,9 +131,8 @@ class FileTreeWalk(private val start: File,
init { init {
if (!start.exists()) { if (!start.exists()) {
throw FileNotFoundException("The start file doesn't exist: $start") end = true
} } else if (start.isDirectory() && filter(start)) {
if (start.isDirectory() && filter(start)) {
pushState(start) pushState(start)
} }
} }
@@ -133,6 +144,7 @@ class FileTreeWalk(private val start: File,
}) })
} }
tailRecursive
private fun gotoNext(): File? { private fun gotoNext(): File? {
if (end) { if (end) {
// We are already at the end // We are already at the end
@@ -141,7 +153,7 @@ class FileTreeWalk(private val start: File,
// There is nothing in the state // There is nothing in the state
// We must visit "start" if it's a file and matches the filter // We must visit "start" if it's a file and matches the filter
end = true end = true
return if (!start.isDirectory() && filter(start)) start else null return if (start.exists() && !start.isDirectory() && filter(start)) start else null
} }
// Take next file from the top of the stack // Take next file from the top of the stack
val topState = state.peek() val topState = state.peek()
@@ -210,13 +222,14 @@ class FileTreeWalk(private val start: File,
return FileTreeWalk(start, direction, enter, leave, fail, filter, depth) return FileTreeWalk(start, direction, enter, leave, fail, filter, depth)
} }
private val it = object: Iterator<File> { private val it = object : Iterator<File> {
override fun hasNext(): Boolean { override public fun hasNext(): Boolean {
if (nextFile == null) if (nextFile == null)
nextFile = gotoNext() nextFile = gotoNext()
return (nextFile != null) return (nextFile != null)
} }
override fun next(): File {
override public fun next(): File {
if (nextFile == null) if (nextFile == null)
nextFile = gotoNext() nextFile = gotoNext()
val res = nextFile val res = nextFile
@@ -229,23 +242,38 @@ class FileTreeWalk(private val start: File,
} }
} }
override fun iterator(): Iterator<File> { override public fun iterator(): Iterator<File> {
return it return it
} }
} }
fun File.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileTreeWalk = /**
* Gets a sequence for visiting this directory and all its content.
*
* @param direction walk direction, top-down (by default) or bottom-up
*/
public fun File.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileTreeWalk =
FileTreeWalk(this, direction) FileTreeWalk(this, direction)
/** /**
* Gets a stream for visiting this directory and all its content in top-down order. * Gets a sequence for visiting this directory and all its content in top-down order.
* Depth-first search is used and directories are visited before all their files * Depth-first search is used and directories are visited before all their files
*/ */
public fun File.walkTopDown(): FileTreeWalk = walk(FileWalkDirection.TOP_DOWN) public fun File.walkTopDown(): FileTreeWalk = walk(FileWalkDirection.TOP_DOWN)
/** /**
* Gets a stream for visiting this directory and all its content in bottom-up order. * Gets a sequence for visiting this directory and all its content in bottom-up order.
* Depth-first search is used and directories are visited after all their files * Depth-first search is used and directories are visited after all their files
*/ */
public fun File.walkBottomUp(): FileTreeWalk = walk(FileWalkDirection.BOTTOM_UP) public fun File.walkBottomUp(): FileTreeWalk = walk(FileWalkDirection.BOTTOM_UP)
/**
* Recursively process this file and all children with the given block.
* Note that if this file doesn't exist, then the block will be executed on it anyway.
*
* @param block the function to call on each file
*/
deprecated("It's recommended to use walkTopDown() / walkBottomUp()")
public fun File.recurse(block: (File) -> Unit): Unit {
walkTopDown().forEach { block(it) }
}
+108 -149
View File
@@ -1,6 +1,7 @@
package kotlin.io package kotlin.io
import java.io.* import java.io.*
import java.nio.charset.Charset
import java.util.ArrayList import java.util.ArrayList
/** /**
@@ -11,7 +12,7 @@ import java.util.ArrayList
* If [directory] is not specified then the default temporary-file directory will be used. * If [directory] is not specified then the default temporary-file directory will be used.
* *
* @return a file object corresponding to a newly-created directory. * @return a file object corresponding to a newly-created directory.
* *
* @throws IOException in case of input/output error * @throws IOException in case of input/output error
* @throws IllegalArgumentException if [prefix] is shorter than three symbols * @throws IllegalArgumentException if [prefix] is shorter than three symbols
*/ */
@@ -33,7 +34,7 @@ public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directo
* If [directory] is not specified then the default temporary-file directory will be used. * If [directory] is not specified then the default temporary-file directory will be used.
* *
* @return a file object corresponding to a newly-created file. * @return a file object corresponding to a newly-created file.
* *
* @throws IOException in case of input/output error * @throws IOException in case of input/output error
* @throws IllegalArgumentException if [prefix] is shorter than three symbols * @throws IllegalArgumentException if [prefix] is shorter than three symbols
*/ */
@@ -42,39 +43,37 @@ public fun createTempFile(prefix: String = "tmp", suffix: String? = null, direct
} }
/** /**
* @return this if this file is a directory, or the parent if it is a file inside a directory * Returns this if this file is a directory, or the parent if it is a file inside a directory
*
* Note: this property looks strange
*/ */
public val File.directory: File public val File.directory: File
get() = if (isDirectory()) this else parent!! get() = if (isDirectory()) this else parent!!
/** /**
* @return parent of this abstract path name, or null if it has no parent * Returns parent of this abstract path name, or null if it has no parent
*/ */
public val File.parent: File? public val File.parent: File?
get() = getParentFile() get() = getParentFile()
/** /**
* @return the canonical path of this file. * Returns the canonical path of this file.
*/ */
public val File.canonicalPath: String public val File.canonicalPath: String
get() = getCanonicalPath() get() = getCanonicalPath()
/** /**
* @return the file name or "" for an empty name * Returns the file name
*/ */
public val File.name: String public val File.name: String
get() = getName() get() = getName()
/** /**
* @return the file path or "" for an empty name * Returns the file path
*/ */
public val File.path: String public val File.path: String
get() = getPath() get() = getPath()
/** /**
* @return the extension of this file (not including the dot), or an empty string if it doesn't have one. * Returns the extension of this file (not including the dot), or an empty string if it doesn't have one.
*/ */
public val File.extension: String public val File.extension: String
get() { get() {
@@ -110,6 +109,12 @@ public fun String.allSeparatorsToSystem(): String {
return separatorsToSystem().pathSeparatorsToSystem() return separatorsToSystem().pathSeparatorsToSystem()
} }
/** Creates a new reader for the string */
public fun String.reader(): StringReader = StringReader(this)
/** Creates a new byte input stream for the string */
public fun String.byteInputStream(charset: Charset = Charsets.UTF_8): InputStream = ByteArrayInputStream(toByteArray(charset))
/** /**
* Returns a pathname of this file with all path separators replaced with File.pathSeparator * Returns a pathname of this file with all path separators replaced with File.pathSeparator
* *
@@ -120,7 +125,7 @@ public fun File.separatorsToSystem(): String {
} }
/** /**
* @return file's name without an extension. * Returns file's name without an extension.
*/ */
public val File.nameWithoutExtension: String public val File.nameWithoutExtension: String
get() = name.substringBeforeLast(".") get() = name.substringBeforeLast(".")
@@ -131,51 +136,43 @@ public val File.nameWithoutExtension: String
* If this file matches the [base] file, then an empty string will be returned. * If this file matches the [base] file, then an empty string will be returned.
* *
* @return relative path from [base] to this * @return relative path from [base] to this
* *
* @throws IllegalArgumentException if child and parent have different roots. * @throws IllegalArgumentException if child and parent have different roots.
*/ */
public fun File.relativeTo(base: File): String { public fun File.relativeTo(base: File): String {
// Check roots // Check roots
if (root != base.root) val components = filePathComponents()
val baseComponents = base.filePathComponents()
if (components.rootName != base.rootName)
throw IllegalArgumentException("this and base files have different roots") throw IllegalArgumentException("this and base files have different roots")
var it = iterator() var i = 0
var baseIt = base.iterator() while (i < components.size && i < baseComponents.size && components.fileList[i] == baseComponents.fileList[i])
var same = true i++
var sameComponents = 0 val sameCount = i
var baseComponents = 0 val baseCount = baseComponents.size
// Determine number of same components for this and base
// and total number of components for base
while (baseIt.hasNext()) {
baseComponents++
val baseName = baseIt.next()
if (same && it.hasNext()) {
if(it.next() == baseName) {
sameComponents++
} else {
same = false
}
} else {
same = false
}
}
// Add all .. // Add all ..
var res = "" var res = ""
for (i in sameComponents+1..baseComponents-1) for (j in sameCount + 1..baseCount - 1)
res += (".." + File.separator) res += (".." + File.separator)
val components = nameCount
// If .. is the last element, no separator should present // If .. is the last element, no separator should present
if (baseComponents > sameComponents) { if (baseCount > sameCount) {
res += if (sameComponents < components) ".." + File.separator else ".." res += if (sameCount < components.size) ".." + File.separator else ".."
} }
// Add remaining this components // Add remaining this components
if (sameComponents < components-1) if (sameCount < components.size - 1)
res += (subPath(sameComponents, components-1).toString() + File.separator) res += (components.subPath(sameCount, components.size - 1).toString() + File.separator)
// The last one should be without separator // The last one should be without separator
if (sameComponents < components) if (sameCount < components.size)
res += subPath(components-1, components).toString() res += components.subPath(components.size - 1, components.size).toString()
return res return res
} }
/**
* Calculates the relative path for this file from [descendant] file.
* Note that the [descendant] file is treated as a directory.
* If this file matches the [descendant] directory or does not belong to it,
* then an empty string will be returned.
*/
deprecated("Use relativeTo() function instead") deprecated("Use relativeTo() function instead")
public fun File.relativePath(descendant: File): String { public fun File.relativePath(descendant: File): String {
val prefix = directory.canonicalPath val prefix = directory.canonicalPath
@@ -192,7 +189,7 @@ public fun File.relativePath(descendant: File): String {
/** /**
* Copies this file to the given output [dst], returning the number of bytes copied. * Copies this file to the given output [dst], returning the number of bytes copied.
* *
* If some directories on a way to the [dst] are missing, then they will be created. * If some directories on a way to the [dst] are missing, then they will be created.
* If the [dst] file already exists, then this function will fail unless [overwrite] argument is set to true and * If the [dst] file already exists, then this function will fail unless [overwrite] argument is set to true and
* the [dst] file is not a non-empty directory. * the [dst] file is not a non-empty directory.
@@ -215,8 +212,8 @@ public fun File.copyTo(dst: File, overwrite: Boolean = false, bufferSize: Int =
} else if (dst.exists()) { } else if (dst.exists()) {
if (!overwrite) { if (!overwrite) {
throw FileAlreadyExistsException(file = this, throw FileAlreadyExistsException(file = this,
other = dst, other = dst,
reason = "The destination file already exists") reason = "The destination file already exists")
} else if (dst.isDirectory() && dst.listFiles().any()) { } else if (dst.isDirectory() && dst.listFiles().any()) {
// In this case file should be copied *into* this directory, // In this case file should be copied *into* this directory,
// no matter whether it is empty or not // no matter whether it is empty or not
@@ -246,6 +243,8 @@ public enum class OnErrorAction {
TERMINATE TERMINATE
} }
private class TerminateException(file: File) : FileSystemException(file) {}
/** /**
* Copies this file with all its children to the specified destination [dst] path. * Copies this file with all its children to the specified destination [dst] path.
* If some directories on the way to the destination are missing, then they will be created. * If some directories on the way to the destination are missing, then they will be created.
@@ -261,83 +260,68 @@ public enum class OnErrorAction {
* IOException - if some problems occur when copying. * IOException - if some problems occur when copying.
* *
* @return false if the copying was terminated, true otherwise. * @return false if the copying was terminated, true otherwise.
* *
* Note that if this function fails, then partial copying may have taken place. * Note that if this function fails, then partial copying may have taken place.
*/ */
public fun File.copyRecursively(dst: File, public fun File.copyRecursively(dst: File,
onError: (File, IOException) -> OnErrorAction = onError: (File, IOException) -> OnErrorAction =
{(file: File, e: IOException) -> throw e } { file, e -> throw e }
): Boolean { ): Boolean {
fun copy(src: File): OnErrorAction? { if (!exists()) {
if (!src.exists()) { return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist")) !=
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist")) OnErrorAction.TERMINATE
} }
val relPath = src.relativeTo(this@copyRecursively) try {
val dstFile = File(dst, relPath) for (src in walkTopDown().fail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) { if (!src.exists()) {
return onError(dstFile, FileAlreadyExistsException(file = src, if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) ==
other = dstFile, OnErrorAction.TERMINATE)
reason = "The destination file already exists")) return false
} } else {
try { val relPath = src.relativeTo(this)
if (src.isDirectory()) { val dstFile = File(dst, relPath)
dstFile.mkdirs() if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) {
val children = src.listFiles() if (onError(dstFile, FileAlreadyExistsException(file = src,
if (children == null) { other = dstFile,
return onError(src, AccessDeniedException(file = src, reason = "The destination file already exists")) == OnErrorAction.TERMINATE)
reason = "Cannot list files in a directory")) return false
} } else if (src.isDirectory()) {
for (child in children) { dstFile.mkdirs()
val result = copy(child) } else {
if (result == OnErrorAction.TERMINATE) { if (src.copyTo(dstFile, true) != src.length()) {
return result if (onError(src, IOException("src.length() != dst.length()")) == OnErrorAction.TERMINATE)
return false
} }
} }
} else {
if (src.copyTo(dstFile, true) != src.length()) {
return onError(src, IOException("src.length() != dst.length()"))
}
} }
} catch (e: IOException) {
return onError(src, e)
} }
return null return true
} catch (e: TerminateException) {
return false
} }
val result = copy(this)
return result != OnErrorAction.TERMINATE
} }
/** /**
* Delete this file with all its children. * Delete this file with all its children.
* Note that if this operation fails then partial deletion may have taken place.
* *
* @return true if the file or directory is successfully deleted, false otherwise. * @return true if the file or directory is successfully deleted, false otherwise.
*
* Note that if this operation fails then partial deletion may have taken place.
*/ */
public fun File.deleteRecursively(): Boolean { public fun File.deleteRecursively(): Boolean {
if (isDirectory()) { var result = exists()
listFiles()?.forEach { it.deleteRecursively() } walkBottomUp().forEach { if (!it.delete()) result = false }
} return result
return delete()
} }
/** /**
* @return an array of files and directories in the directory that match the specified [filter] * Returns an array of files and directories in the directory that match the specified [filter]
* or null if this file does not denote a directory. * or null if this file does not denote a directory.
*/ */
public fun File.listFiles(filter: (file: File) -> Boolean): Array<File>? = listFiles( public fun File.listFiles(filter: (file: File) -> Boolean): Array<File>? = listFiles(
object : FileFilter { object : FileFilter {
override fun accept(file: File) = filter(file) override fun accept(file: File) = filter(file)
} }
) )
/**
* @return an iterator to go through pathname components, e.g.
* /foo/bar/gav has components foo, bar and gav
*/
public fun File.iterator(): Iterator<File> = FileIterator(this)
/** /**
* Determines whether this file belongs to the same root as [o] * Determines whether this file belongs to the same root as [o]
@@ -348,22 +332,14 @@ public fun File.iterator(): Iterator<File> = FileIterator(this)
* @return true if this path starts with [o] path, false otherwise * @return true if this path starts with [o] path, false otherwise
*/ */
public fun File.startsWith(o: File): Boolean { public fun File.startsWith(o: File): Boolean {
val it = FileIterator(this) val components = filePathComponents()
val otherIt = FileIterator(o) val otherComponents = o.filePathComponents()
// Roots must be same OR other path can have no root if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
if (it.root != otherIt.root && otherIt.root != "")
return false return false
// Other path is empty if (components.size < otherComponents.size)
if (!otherIt.hasNext())
return true
val count = this.nameCount
val otherCount = o.nameCount
// This path is shorted than the other
if (count < otherCount)
return false return false
// Compare first elements until other ends for (i in 0..otherComponents.size - 1) {
while (otherIt.hasNext()) { if (components.fileList[i] != otherComponents.fileList[i])
if (it.next() != otherIt.next())
return false return false
} }
return true return true
@@ -388,32 +364,17 @@ public fun File.startsWith(o: String): Boolean = startsWith(File(o))
* @return true if this path ends with [o] path, false otherwise * @return true if this path ends with [o] path, false otherwise
*/ */
public fun File.endsWith(o: File): Boolean { public fun File.endsWith(o: File): Boolean {
val it = FileIterator(this) val components = filePathComponents()
val otherIt = FileIterator(o) val otherComponents = o.filePathComponents()
// Roots must be same OR other path can have no root if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
if (it.root != otherIt.root && otherIt.root != "")
return false return false
// Other path is empty val shift = components.size - otherComponents.size
if (!otherIt.hasNext()) if (shift < 0)
return true
var theirs = otherIt.next()
val count = this.nameCount
val otherCount = o.nameCount
// This path is shorted than the other
if (count < otherCount)
return false return false
var ours = it.next() for (i in 0..otherComponents.size - 1) {
// Move forward to have same number of elements remaining if (components.fileList[i + shift] != otherComponents.fileList[i])
for (i in 0..count-otherCount-1) { return false
ours = it.next()
} }
// Check all next elements are same, until the end
while (ours == theirs && it.hasNext() && otherIt.hasNext()) {
ours = it.next()
theirs = otherIt.next()
}
if (ours != theirs || it.hasNext() || otherIt.hasNext())
return false
return true return true
} }
@@ -434,17 +395,15 @@ public fun File.endsWith(o: String): Boolean = endsWith(File(o))
* @return normalized pathname with . and possibly .. removed * @return normalized pathname with . and possibly .. removed
*/ */
public fun File.normalize(): File { public fun File.normalize(): File {
val rootName = rootName val components = filePathComponents()
val list: MutableList<String> = ArrayList() val rootName = components.rootName
for (elem in this) val list: MutableList<String> = components.fileList.filter { it.toString() != "." }.map { it.toString() }.toLinkedList()
if (elem.toString() != ".")
list.add(elem.toString())
var first = 0 var first = 0
var dots = list.subList(first, list.size()).indexOf("..") var dots = list.subList(first, list.size()).indexOf("..")
while (dots != -1) { while (dots != -1) {
if (dots > 0) { if (dots > 0) {
list.remove(dots+first) list.remove(dots + first)
list.remove(dots+first-1) list.remove(dots + first - 1)
} else { } else {
first++ first++
} }
@@ -474,10 +433,7 @@ public fun File.resolve(o: File): File {
if (o.root != null) if (o.root != null)
return o return o
val ourName = toString() val ourName = toString()
if (ourName.endsWith(File.separatorChar)) return if (ourName.endsWith(File.separatorChar)) File(ourName + o) else File(ourName + File.separatorChar + o)
return File(ourName + o)
else
return File(ourName + File.separatorChar + o)
} }
/** /**
@@ -497,11 +453,14 @@ public fun File.resolve(o: String): File = resolve(File(o))
* @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent * @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent
*/ */
public fun File.resolveSibling(o: File): File { public fun File.resolveSibling(o: File): File {
val parentFile = parent val components = filePathComponents()
if (parentFile != null) val rootName = components.rootName
return parentFile.resolve(o) val parentFile = when (components.size) {
else 0 -> null
return o 1 -> File(rootName)
else -> File(rootName + components.subPath(0, components.size - 1).path)
}
return if (parentFile != null) parentFile.resolve(o) else o
} }
/** /**
+104 -50
View File
@@ -1,15 +1,13 @@
package test.io package test.io
import java.io.*
import org.junit.Test as test import org.junit.Test as test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import java.io.File
import java.io.IOException
import java.io.FileNotFoundException
import java.util.NoSuchElementException import java.util.NoSuchElementException
import java.util.HashSet import java.util.HashSet
import java.util.ArrayList import java.util.ArrayList
import kotlin.io.files.walkBottomUp import kotlin.io.walkBottomUp
import kotlin.io.files.walkTopDown import kotlin.io.walkTopDown
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -29,7 +27,8 @@ class FilesTest {
try { try {
createTempDir("a") createTempDir("a")
assert(false) assert(false)
} catch(e: IllegalArgumentException) {} } catch(e: IllegalArgumentException) {
}
val dir2 = createTempDir("temp") val dir2 = createTempDir("temp")
assert(dir2.exists() && dir2.isDirectory() && dir2.name.endsWith(".tmp")) assert(dir2.exists() && dir2.isDirectory() && dir2.name.endsWith(".tmp"))
@@ -49,7 +48,8 @@ class FilesTest {
try { try {
createTempFile("a") createTempFile("a")
assert(false) assert(false)
} catch(e: IllegalArgumentException) {} } catch(e: IllegalArgumentException) {
}
val file2 = createTempFile("temp") val file2 = createTempFile("temp")
assert(file2.exists() && file2.name.endsWith(".tmp")) assert(file2.exists() && file2.name.endsWith(".tmp"))
@@ -81,7 +81,7 @@ class FilesTest {
try { try {
val referenceNames = val referenceNames =
listOf("", "1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6", "7.txt", "8", "8/9.txt").map( listOf("", "1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6", "7.txt", "8", "8/9.txt").map(
{it -> it.separatorsToSystem()}).toHashSet() { it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>() val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown()) { for (file in basedir.walkTopDown()) {
val name = file.relativeTo(basedir) val name = file.relativeTo(basedir)
@@ -106,7 +106,7 @@ class FilesTest {
try { try {
val referenceNames = val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map( listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet() { it -> it.separatorsToSystem() }).toHashSet()
val namesTopDownEnter = HashSet<String>() val namesTopDownEnter = HashSet<String>()
val namesTopDownLeave = HashSet<String>() val namesTopDownLeave = HashSet<String>()
val namesTopDown = HashSet<String>() val namesTopDown = HashSet<String>()
@@ -116,12 +116,14 @@ class FilesTest {
namesTopDownEnter.add(name) namesTopDownEnter.add(name)
assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance") assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance")
} }
fun leave(file: File) { fun leave(file: File) {
val name = file.relativeTo(basedir) val name = file.relativeTo(basedir)
assertFalse(namesTopDownLeave.contains(name), "$name is left twice") assertFalse(namesTopDownLeave.contains(name), "$name is left twice")
namesTopDownLeave.add(name) namesTopDownLeave.add(name)
assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance") assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance")
} }
fun visit(file: File) { fun visit(file: File) {
val name = file.relativeTo(basedir) val name = file.relativeTo(basedir)
if (file.isDirectory()) { if (file.isDirectory()) {
@@ -163,9 +165,10 @@ class FilesTest {
try { try {
val referenceNames = val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map( listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet() { it -> it.separatorsToSystem() }).toHashSet()
assertEquals(referenceNames, basedir.walkTopDown().filter{ it.isDirectory() }.map{ assertEquals(referenceNames, basedir.walkTopDown().filter { it.isDirectory() }.map {
it.relativeTo(basedir) }.toHashSet()) it.relativeTo(basedir)
}.toHashSet())
} finally { } finally {
basedir.deleteRecursively() basedir.deleteRecursively()
} }
@@ -177,7 +180,7 @@ class FilesTest {
try { try {
val referenceNames = val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map( listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet() { it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>() val namesTopDown = HashSet<String>()
fun enter(file: File) { fun enter(file: File) {
assertTrue(file.isDirectory()) assertTrue(file.isDirectory())
@@ -202,7 +205,7 @@ class FilesTest {
try { try {
val referenceNames = val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map( listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet() { it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>() val namesTopDown = HashSet<String>()
fun enter(file: File) { fun enter(file: File) {
assertTrue(file.isDirectory()) assertTrue(file.isDirectory())
@@ -229,9 +232,10 @@ class FilesTest {
// Everything ended with 3 is filtered // Everything ended with 3 is filtered
return (!file.name.endsWith("3")); return (!file.name.endsWith("3"));
} }
val referenceNames = val referenceNames =
listOf("", "1", "1/2", "6", "7.txt", "8", "8/9.txt").map( listOf("", "1", "1/2", "6", "7.txt", "8", "8/9.txt").map(
{it -> it.separatorsToSystem()}).toHashSet() { it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>() val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown().filter(::filter)) { for (file in basedir.walkTopDown().filter(::filter)) {
val name = file.relativeTo(basedir) val name = file.relativeTo(basedir)
@@ -254,18 +258,18 @@ class FilesTest {
test fun withTotalFilter() { test fun withTotalFilter() {
val basedir = createTestFiles() val basedir = createTestFiles()
try { try {
// Everything is filtered
fun filter(file: File) = false
val referenceNames: Set<String> = setOf() val referenceNames: Set<String> = setOf()
val namesTopDown = HashSet<String>() val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown().filter(::filter)) { // Everything is filtered
for (file in basedir.walkTopDown().filter({ false })) {
val name = file.relativeTo(basedir) val name = file.relativeTo(basedir)
assertFalse(namesTopDown.contains(name), "$name is visited twice") assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name) namesTopDown.add(name)
} }
assertEquals(referenceNames, namesTopDown) assertEquals(referenceNames, namesTopDown)
val namesBottomUp = HashSet<String>() val namesBottomUp = HashSet<String>()
for (file in basedir.walkBottomUp().filter(::filter)) { // Everything is filtered
for (file in basedir.walkBottomUp().filter({ false })) {
val name = file.relativeTo(basedir) val name = file.relativeTo(basedir)
assertFalse(namesBottomUp.contains(name), "$name is visited twice") assertFalse(namesBottomUp.contains(name), "$name is visited twice")
namesBottomUp.add(name) namesBottomUp.add(name)
@@ -303,7 +307,7 @@ class FilesTest {
test fun withReduce() { test fun withReduce() {
val basedir = createTestFiles() val basedir = createTestFiles()
try { try {
val res = basedir.walkTopDown().reduce { (a, b) -> if (a.canonicalPath > b.canonicalPath) a else b } val res = basedir.walkTopDown().reduce { a, b -> if (a.canonicalPath > b.canonicalPath) a else b }
assertTrue(res.endsWith("9.txt"), "Expected end with 9.txt actual: ${res.name}") assertTrue(res.endsWith("9.txt"), "Expected end with 9.txt actual: ${res.name}")
} finally { } finally {
basedir.deleteRecursively() basedir.deleteRecursively()
@@ -321,27 +325,30 @@ class FilesTest {
stack.add(dir) stack.add(dir)
dirs.add(dir.relativeTo(basedir)) dirs.add(dir.relativeTo(basedir))
} }
fun afterVisitDirectory(dir: File) { fun afterVisitDirectory(dir: File) {
assertEquals(stack.last(), dir) assertEquals(stack.last(), dir)
stack.remove(stack.lastIndex) stack.remove(stack.lastIndex)
} }
fun visitFile(file: File) { fun visitFile(file: File) {
assert(stack.last().listFiles().contains(file), file) assert(stack.last().listFiles().contains(file), file)
files.add(file.relativeTo(basedir)) files.add(file.relativeTo(basedir))
} }
fun visitDirectoryFailed(dir: File, e: IOException) { fun visitDirectoryFailed(dir: File, e: IOException) {
assertEquals(stack.last(), dir) assertEquals(stack.last(), dir)
stack.remove(stack.lastIndex) stack.remove(stack.lastIndex)
failed.add(dir.name) failed.add(dir.name)
} }
basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory). basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).
fail(::visitDirectoryFailed).forEach{ it -> if (!it.isDirectory()) visitFile(it) } fail(::visitDirectoryFailed).forEach { it -> if (!it.isDirectory()) visitFile(it) }
assert(stack.isEmpty()) assert(stack.isEmpty())
val sep = File.separator val sep = File.separator
for (fileName in array("", "1", "1${sep}2", "1${sep}3", "6", "8")) { for (fileName in array("", "1", "1${sep}2", "1${sep}3", "6", "8")) {
assert(dirs.contains(fileName), fileName) assert(dirs.contains(fileName), fileName)
} }
for (fileName in array("1${sep}3${sep}4.txt", "1${sep}3${sep}4.txt", "7.txt", "8${sep}9.txt")) { for (fileName in array("1${sep}3${sep}4.txt", "1${sep}3${sep}4.txt", "7.txt", "8${sep}9.txt")) {
assert(files.contains(fileName), fileName) assert(files.contains(fileName), fileName)
} }
@@ -349,7 +356,7 @@ class FilesTest {
files.clear() files.clear()
dirs.clear() dirs.clear()
basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).maxDepth(1). basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).maxDepth(1).
forEach{ it -> if (it != basedir) visitFile(it) } forEach { it -> if (it != basedir) visitFile(it) }
assert(stack.isEmpty()) assert(stack.isEmpty())
assert(dirs.size() == 1 && dirs.contains(""), dirs.size()) assert(dirs.size() == 1 && dirs.contains(""), dirs.size())
for (file in array("1", "6", "7.txt", "8")) { for (file in array("1", "6", "7.txt", "8")) {
@@ -362,7 +369,7 @@ class FilesTest {
files.clear() files.clear()
dirs.clear() dirs.clear()
basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory). basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).
fail(::visitDirectoryFailed).forEach{ it -> if (!it.isDirectory()) visitFile(it) } fail(::visitDirectoryFailed).forEach { it -> if (!it.isDirectory()) visitFile(it) }
assert(stack.isEmpty()) assert(stack.isEmpty())
assert(failed.size() == 1 && failed.contains("1"), failed.size()) assert(failed.size() == 1 && failed.contains("1"), failed.size())
assert(dirs.size() == 4, dirs.size()) assert(dirs.size() == 4, dirs.size())
@@ -393,7 +400,7 @@ class FilesTest {
assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it) assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it)
visited.add(it) visited.add(it)
} }
basedir.walkTopDown().forEach( block ) basedir.walkTopDown().forEach(block)
assert(visited.size() == 10, visited.size()) assert(visited.size() == 10, visited.size())
} finally { } finally {
@@ -412,7 +419,7 @@ class FilesTest {
assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it) assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it)
visited.add(it) visited.add(it)
} }
basedir.walkTopDown().forEach( block ) basedir.walkTopDown().forEach(block)
assert(visited.size() == 6, visited.size()) assert(visited.size() == 6, visited.size())
} }
} finally { } finally {
@@ -460,7 +467,7 @@ class FilesTest {
try { try {
File(basedir, "8/4.txt".separatorsToSystem()).createNewFile() File(basedir, "8/4.txt".separatorsToSystem()).createNewFile()
var count = 0 var count = 0
basedir.walkTopDown().takeWhile{ it -> count == 0 }.forEach { basedir.walkTopDown().takeWhile { it -> count == 0 }.forEach {
if (it.name == "4.txt") { if (it.name == "4.txt") {
count++ count++
} }
@@ -511,10 +518,13 @@ class FilesTest {
} finally { } finally {
dir.delete() dir.delete()
} }
try { }
dir.walkTopDown()
assert(false) test fun recurse() {
} catch(e: FileNotFoundException) {} val set: MutableSet<String> = HashSet()
val dir = createTestFiles()
dir.recurse { set.add(it.path) }
assertEquals(10, set.size())
} }
} }
@@ -525,8 +535,12 @@ class FilesTest {
createTempFile("temp2", ".java", dir) createTempFile("temp2", ".java", dir)
createTempFile("temp3", ".kt", dir) createTempFile("temp3", ".kt", dir)
// This line works only with Kotlin File.listFiles(filter)
val result = dir.listFiles { it.getName().endsWith(".kt") } val result = dir.listFiles { it.getName().endsWith(".kt") }
assertEquals(2, result!!.size()) assertEquals(2, result!!.size())
// This line works both with Kotlin File.listFiles(filter) and the same Java function because of SAM
val result2 = dir.listFiles { it -> it.getName().endsWith(".kt") }
assertEquals(2, result2!!.size())
} }
test fun relativeToTest() { test fun relativeToTest() {
@@ -570,11 +584,10 @@ class FilesTest {
val file1 = File("C:/dir1".separatorsToSystem()) val file1 = File("C:/dir1".separatorsToSystem())
val file2 = File("D:/dir2".separatorsToSystem()) val file2 = File("D:/dir2".separatorsToSystem())
try { try {
val winRelPath = file1.relativeTo(file2) file1.relativeTo(file2)
assert(file1.canonicalPath.charAt(0) == '/') assert(false);
assertEquals("../../C:/dir1", winRelPath)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
assert(Character.isLetter(file1.canonicalPath.charAt(0))) // It's the thing we should get here
} catch (e: IOException) { } catch (e: IOException) {
// The device is not ready (D) ==> DO NOTHING // The device is not ready (D) ==> DO NOTHING
} }
@@ -593,8 +606,8 @@ class FilesTest {
private fun checkFileElements(f: File, root: File?, elements: List<String>) { private fun checkFileElements(f: File, root: File?, elements: List<String>) {
var i = 0 var i = 0
assertEquals(root, f.root) assertEquals(root, f.root)
for (elem in f) { for (elem in f.filePathComponents().fileList) {
assertTrue(i < elements.size()) assertTrue(i < elements.size(), i.toString())
assertEquals(elements[i++], elem.toString()) assertEquals(elements[i++], elem.toString())
} }
assertEquals(elements.size(), i) assertEquals(elements.size(), i)
@@ -602,11 +615,14 @@ class FilesTest {
test fun fileIterator() { test fun fileIterator() {
checkFileElements(File("/foo/bar"), File("/"), listOf("foo", "bar")) checkFileElements(File("/foo/bar"), File("/"), listOf("foo", "bar"))
checkFileElements(File("\\foo\\bar"), File("\\".separatorsToSystem()), listOf("foo", "bar"))
checkFileElements(File("/foo/bar/gav"), File("/"), listOf("foo", "bar", "gav")) checkFileElements(File("/foo/bar/gav"), File("/"), listOf("foo", "bar", "gav"))
checkFileElements(File("/foo/bar/gav/"), File("/"), listOf("foo", "bar", "gav")) checkFileElements(File("/foo/bar/gav/"), File("/"), listOf("foo", "bar", "gav"))
checkFileElements(File("bar/gav"), null, listOf("bar", "gav")) checkFileElements(File("bar/gav"), null, listOf("bar", "gav"))
checkFileElements(File("C:\\bar\\gav"), File("C:\\"), listOf("bar", "gav")) checkFileElements(File("C:\\bar\\gav"), File("C:\\".separatorsToSystem()), listOf("bar", "gav"))
checkFileElements(File("C:\\"), File("C:\\"), listOf()) checkFileElements(File("C:/bar/gav"), File("C:/"), listOf("bar", "gav"))
checkFileElements(File("C:\\"), File("C:\\".separatorsToSystem()), listOf())
checkFileElements(File("C:/"), File("C:/"), listOf())
checkFileElements(File("C:"), File("C:"), listOf()) checkFileElements(File("C:"), File("C:"), listOf())
checkFileElements(File("//host.ru/home/mike"), File("//host.ru/home"), listOf("mike")) checkFileElements(File("//host.ru/home/mike"), File("//host.ru/home"), listOf("mike"))
checkFileElements(File(""), null, listOf("")) checkFileElements(File(""), null, listOf(""))
@@ -631,13 +647,18 @@ class FilesTest {
test fun subPath() { test fun subPath() {
assertEquals(File("mike"), File("//my.host.net/home/mike/temp").subPath(0, 1)) assertEquals(File("mike"), File("//my.host.net/home/mike/temp").subPath(0, 1))
assertEquals(File("mike"), File("\\\\my.host.net\\home\\mike\\temp").subPath(0, 1))
assertEquals(File("bar/gav"), File("/foo/bar/gav/hi").subPath(1, 3)) assertEquals(File("bar/gav"), File("/foo/bar/gav/hi").subPath(1, 3))
} }
test fun normalize() { test fun normalize() {
assertEquals(File("/foo/bar/baaz"), File("/foo/./bar/gav/../baaz").normalize()) assertEquals(File("/foo/bar/baaz"), File("/foo/./bar/gav/../baaz").normalize())
assertEquals(File("/foo/bar/baaz"), File("/foo/bak/../bar/gav/../baaz").normalize())
assertEquals(File("../../bar"), File("../foo/../../bar").normalize()) assertEquals(File("../../bar"), File("../foo/../../bar").normalize())
assertEquals(File("C:\\windows"), File("C:\\home\\..\\documents\\..\\windows").normalize()) // For Unix C:\windows is not correct so it's not the same as C:/windows
assertEquals(File("C:\\windows").separatorsToSystem(),
File("C:\\home\\..\\documents\\..\\windows").normalize().separatorsToSystem())
assertEquals(File("C:/windows"), File("C:/home/../documents/../windows").normalize())
assertEquals(File("foo"), File("gav/bar/../../foo").normalize()) assertEquals(File("foo"), File("gav/bar/../../foo").normalize())
} }
@@ -645,16 +666,22 @@ class FilesTest {
assertEquals(File("/foo/bar/gav"), File("/foo/bar").resolve("gav")) assertEquals(File("/foo/bar/gav"), File("/foo/bar").resolve("gav"))
assertEquals(File("/foo/bar/gav"), File("/foo/bar/").resolve("gav")) assertEquals(File("/foo/bar/gav"), File("/foo/bar/").resolve("gav"))
assertEquals(File("/gav"), File("/foo/bar").resolve("/gav")) assertEquals(File("/gav"), File("/foo/bar").resolve("/gav"))
assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"), // For Unix C:\path is not correct so it's cannot be automatically converted
File("C:\\Users\\Me").resolve("Documents\\important.doc")) assertEquals(File("C:\\Users\\Me\\Documents\\important.doc").separatorsToSystem(),
File("C:\\Users\\Me").resolve("Documents\\important.doc").separatorsToSystem())
assertEquals(File("C:/Users/Me/Documents/important.doc"),
File("C:/Users/Me").resolve("Documents/important.doc"))
} }
test fun resolveSibling() { test fun resolveSibling() {
assertEquals(File("/foo/gav"), File("/foo/bar").resolveSibling("gav")) assertEquals(File("/foo/gav"), File("/foo/bar").resolveSibling("gav"))
assertEquals(File("/foo/gav"), File("/foo/bar/").resolveSibling("gav")) assertEquals(File("/foo/gav"), File("/foo/bar/").resolveSibling("gav"))
assertEquals(File("/gav"), File("/foo/bar").resolveSibling("/gav")) assertEquals(File("/gav"), File("/foo/bar").resolveSibling("/gav"))
assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"), // For Unix C:\path is not correct so it's cannot be automatically converted
File("C:\\Users\\Me\\profile.ini").resolveSibling("Documents\\important.doc")) assertEquals(File("C:\\Users\\Me\\Documents\\important.doc").separatorsToSystem(),
File("C:\\Users\\Me\\profile.ini").resolveSibling("Documents\\important.doc").separatorsToSystem())
assertEquals(File("C:/Users/Me/Documents/important.doc"),
File("C:/Users/Me/profile.ini").resolveSibling("Documents/important.doc"))
} }
test fun extension() { test fun extension() {
@@ -701,7 +728,8 @@ class FilesTest {
try { try {
srcFile.copyTo(dstFile) srcFile.copyTo(dstFile)
assert(false) assert(false)
} catch (e: FileAlreadyExistsException) {} } catch (e: FileAlreadyExistsException) {
}
var len = srcFile.copyTo(dstFile, overwrite = true) var len = srcFile.copyTo(dstFile, overwrite = true)
assertEquals(13L, len) assertEquals(13L, len)
@@ -727,13 +755,15 @@ class FilesTest {
try { try {
srcFile.copyTo(dstFile) srcFile.copyTo(dstFile)
assert(false) assert(false)
} catch (e: NoSuchFileException) {} } catch (e: NoSuchFileException) {
}
srcFile.mkdir() srcFile.mkdir()
try { try {
srcFile.copyTo(dstFile) srcFile.copyTo(dstFile)
assert(false) assert(false)
} catch (e: IllegalArgumentException) {} } catch (e: IllegalArgumentException) {
}
srcFile.delete() srcFile.delete()
} }
@@ -787,7 +817,7 @@ class FilesTest {
var conflicts = 0 var conflicts = 0
src.copyRecursively(dst) { src.copyRecursively(dst) {
(file: File, e: IOException) -> file: File, e: IOException ->
if (e is FileAlreadyExistsException) { if (e is FileAlreadyExistsException) {
conflicts++ conflicts++
OnErrorAction.SKIP OnErrorAction.SKIP
@@ -802,7 +832,7 @@ class FilesTest {
dst.deleteRecursively() dst.deleteRecursively()
var caught = false var caught = false
assert(src.copyRecursively(dst) { assert(src.copyRecursively(dst) {
(file: File, e: IOException) -> file: File, e: IOException ->
if (e is AccessDeniedException) { if (e is AccessDeniedException) {
caught = true caught = true
OnErrorAction.SKIP OnErrorAction.SKIP
@@ -826,7 +856,7 @@ class FilesTest {
} }
assert(!src.copyRecursively(dst) { assert(!src.copyRecursively(dst) {
(file: File, e: IOException) -> file: File, e: IOException ->
OnErrorAction.TERMINATE OnErrorAction.TERMINATE
}) })
} finally { } finally {
@@ -834,4 +864,28 @@ class FilesTest {
dst.deleteRecursively() dst.deleteRecursively()
} }
} }
test fun helpers1() {
val str = "123456789\n"
System.setIn(str.byteInputStream())
val reader = System.`in`.bufferedReader()
assertEquals("123456789", reader.readLine())
val stringReader = str.reader()
assertEquals('1', stringReader.read().toChar())
assertEquals('2', stringReader.read().toChar())
assertEquals('3', stringReader.read().toChar())
}
test fun helpers2() {
val file = createTempFile()
val writer = file.printWriter()
val str1 = "Hello, world!"
val str2 = "Everything is wonderful!"
writer.println(str1)
writer.println(str2)
writer.close()
val reader = file.bufferedReader()
assertEquals(str1, reader.readLine())
assertEquals(str2, reader.readLine())
}
} }
+2 -2
View File
@@ -71,12 +71,12 @@ class ReadWriteTest {
writer.close() writer.close()
//file.replaceText("Hello\nWorld") //file.replaceText("Hello\nWorld")
file.forEachBlock{ (arr: ByteArray, size: Int) -> file.forEachBlock {(arr: ByteArray, size: Int) ->
assertTrue(size >= 11 && size <= 12, size.toString()) assertTrue(size >= 11 && size <= 12, size.toString())
assertTrue(arr.contains('W'.toByte())) assertTrue(arr.contains('W'.toByte()))
} }
val list = ArrayList<String>() val list = ArrayList<String>()
file.forEachLine{ file.forEachLine {
list.add(it) list.add(it)
} }
assertEquals(arrayListOf("Hello", "World"), list) assertEquals(arrayListOf("Hello", "World"), list)