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
/**
* 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. */
public fun print(message: Any?) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Int) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Long) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Byte) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Short) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Char) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Boolean) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Float) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: Double) {
System.out.print(message)
}
/** Prints the given message to the standard output stream. */
public fun print(message: CharArray) {
System.out.print(message)
@@ -54,42 +73,52 @@ public fun print(message: CharArray) {
public fun println(message: Any?) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Int) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Long) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Byte) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Short) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Char) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Boolean) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Float) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: Double) {
System.out.println(message)
}
/** Prints the given message and newline to the standard output stream. */
public fun println(message: CharArray) {
System.out.println(message)
}
/** Prints a newline to the standard output stream. */
public fun 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,
public val other: File? = 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
+39 -5
View File
@@ -4,15 +4,40 @@ import java.io.*
import java.nio.charset.Charset
import java.nio.charset.CharsetDecoder
import java.nio.charset.CharsetEncoder
// Note: hasNext() is incorrect because available() means "number of available bytes WITHOUT BLOCKING"
import java.util.NoSuchElementException
/** 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 =
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]. */
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]. */
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]. */
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.
* @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 {
val buffer = ByteArrayOutputStream(estimatedSize)
this.copyTo(buffer)
copyTo(buffer)
return buffer.toByteArray()
}
+72 -44
View File
@@ -7,10 +7,34 @@ import java.util.ArrayList
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)
/**
* 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.
*
@@ -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")
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 {
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 {
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
* @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)
/**
@@ -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)
/**
* 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 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 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 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 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 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 charset character set to use
*/
deprecated("There is a same function with default parameter value")
public fun File.appendText(text: String, charset: String): Unit {
appendBytes(text.toByteArray(charset))
}
/**
* Reads file by byte blocks and calls [closure] for each block read. Block size depends on implementation
* but is never less than 512 bytes.
* Reads file by byte blocks and calls [closure] for each block read.
* 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.
*
* You can use this function for huge files.
*
* @param closure function to proceed file blocks
*/
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit {
val arr = ByteArray(4096)
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit = forEachBlock(closure, defaultBlockSize)
/**
* 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)
try {
do {
val size = fis.read(arr)
if (size == -1) {
if (size <= 0) {
break
} else if (size > 0) {
} else {
closure(arr, size)
}
} while (true)
@@ -172,12 +205,8 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit {
* @param closure function to proceed file lines
*/
public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: String) -> Unit): Unit {
val reader = BufferedReader(InputStreamReader(FileInputStream(this), charset))
try {
reader.forEachLine(closure)
} finally {
reader.close()
}
// Note: close is called at forEachLine
BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(closure)
}
/**
@@ -188,7 +217,6 @@ public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: S
* @param charset character set to use
* @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)
/**
@@ -199,7 +227,6 @@ public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): U
* @param charset character set to use
* @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))
/**
@@ -216,11 +243,11 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
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
= 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
= 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
*/
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
@@ -238,29 +265,31 @@ public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines
* @return the value returned by [block].
*/
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`
* 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.
*
* 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)
deprecated("Use lines() function which returns Stream<String>")
deprecated("Use lines() function which returns Sequence<String>")
public fun BufferedReader.lineIterator(): Iterator<String> = lines().iterator()
private class LinesStream(private val reader: BufferedReader) : Sequence<String> {
override fun iterator(): Iterator<String> {
override public fun iterator(): Iterator<String> {
return object : Iterator<String> {
private var nextValue: String? = null
private var done = false
override fun hasNext(): Boolean {
override public fun hasNext(): Boolean {
if (nextValue == null && !done) {
nextValue = reader.readLine()
if (nextValue == null) done = true
@@ -268,7 +297,7 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
return nextValue != null
}
public override fun next(): String {
override public fun next(): String {
if (!hasNext()) {
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.
*
* @return the string with corresponding file contents
* @return the string with corresponding file content
*/
public fun Reader.readText(): String {
val buffer = StringWriter()
@@ -322,7 +351,6 @@ public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long
* @param charset a character set to use
* @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)
/**
@@ -342,13 +370,13 @@ public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes()
*
* @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
* 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
*/
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) {
closed = true
try {
this.close()
close()
} catch (closeException: Exception) {
// eat the closeException as we are already throwing the original cause
// 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
} finally {
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.FileNotFoundException
@@ -13,7 +13,12 @@ import java.util.Stack
* 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 */
TOP_DOWN
/** Depth-first search, directory is visited AFTER its files */
@@ -21,24 +26,31 @@ enum class FileWalkDirection {
// Do we want also breadth-first search?
}
class FileTreeWalk(private val start: 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 = { it -> true },
private val maxDepth: Int = Int.MAX_VALUE
): Stream<File> {
/**
* This class is intended to implement different file walk methods.
* It allows to iterate through all files inside [start] directory.
* If [start] is just a file, walker iterates only it.
* If [start] does not exist, walker does not do any iterations at all.
*/
public class FileTreeWalk(private val start: 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 {
if (!rootDir.isDirectory())
throw IllegalArgumentException("Directory is needed")
}
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
@@ -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
@@ -119,9 +131,8 @@ class FileTreeWalk(private val start: File,
init {
if (!start.exists()) {
throw FileNotFoundException("The start file doesn't exist: $start")
}
if (start.isDirectory() && filter(start)) {
end = true
} else if (start.isDirectory() && filter(start)) {
pushState(start)
}
}
@@ -133,6 +144,7 @@ class FileTreeWalk(private val start: File,
})
}
tailRecursive
private fun gotoNext(): File? {
if (end) {
// We are already at the end
@@ -141,7 +153,7 @@ class FileTreeWalk(private val start: File,
// There is nothing in the state
// We must visit "start" if it's a file and matches the filter
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
val topState = state.peek()
@@ -210,13 +222,14 @@ class FileTreeWalk(private val start: File,
return FileTreeWalk(start, direction, enter, leave, fail, filter, depth)
}
private val it = object: Iterator<File> {
override fun hasNext(): Boolean {
private val it = object : Iterator<File> {
override public fun hasNext(): Boolean {
if (nextFile == null)
nextFile = gotoNext()
return (nextFile != null)
}
override fun next(): File {
override public fun next(): File {
if (nextFile == null)
nextFile = gotoNext()
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
}
}
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)
/**
* 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
*/
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
*/
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
import java.io.*
import java.nio.charset.Charset
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.
*
* @return a file object corresponding to a newly-created directory.
*
*
* @throws IOException in case of input/output error
* @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.
*
* @return a file object corresponding to a newly-created file.
*
*
* @throws IOException in case of input/output error
* @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
*
* Note: this property looks strange
* Returns this if this file is a directory, or the parent if it is a file inside a directory
*/
public val File.directory: File
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?
get() = getParentFile()
/**
* @return the canonical path of this file.
* Returns the canonical path of this file.
*/
public val File.canonicalPath: String
get() = getCanonicalPath()
/**
* @return the file name or "" for an empty name
* Returns the file name
*/
public val File.name: String
get() = getName()
/**
* @return the file path or "" for an empty name
* Returns the file path
*/
public val File.path: String
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
get() {
@@ -110,6 +109,12 @@ public fun String.allSeparatorsToSystem(): String {
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
*
@@ -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
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.
*
* @return relative path from [base] to this
*
*
* @throws IllegalArgumentException if child and parent have different roots.
*/
public fun File.relativeTo(base: File): String {
// 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")
var it = iterator()
var baseIt = base.iterator()
var same = true
var sameComponents = 0
var baseComponents = 0
// 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
}
}
var i = 0
while (i < components.size && i < baseComponents.size && components.fileList[i] == baseComponents.fileList[i])
i++
val sameCount = i
val baseCount = baseComponents.size
// Add all ..
var res = ""
for (i in sameComponents+1..baseComponents-1)
for (j in sameCount + 1..baseCount - 1)
res += (".." + File.separator)
val components = nameCount
// If .. is the last element, no separator should present
if (baseComponents > sameComponents) {
res += if (sameComponents < components) ".." + File.separator else ".."
if (baseCount > sameCount) {
res += if (sameCount < components.size) ".." + File.separator else ".."
}
// Add remaining this components
if (sameComponents < components-1)
res += (subPath(sameComponents, components-1).toString() + File.separator)
if (sameCount < components.size - 1)
res += (components.subPath(sameCount, components.size - 1).toString() + File.separator)
// The last one should be without separator
if (sameComponents < components)
res += subPath(components-1, components).toString()
if (sameCount < components.size)
res += components.subPath(components.size - 1, components.size).toString()
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")
public fun File.relativePath(descendant: File): String {
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.
*
*
* 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
* 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()) {
if (!overwrite) {
throw FileAlreadyExistsException(file = this,
other = dst,
reason = "The destination file already exists")
other = dst,
reason = "The destination file already exists")
} else if (dst.isDirectory() && dst.listFiles().any()) {
// In this case file should be copied *into* this directory,
// no matter whether it is empty or not
@@ -246,6 +243,8 @@ public enum class OnErrorAction {
TERMINATE
}
private class TerminateException(file: File) : FileSystemException(file) {}
/**
* 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.
@@ -261,83 +260,68 @@ public enum class OnErrorAction {
* IOException - if some problems occur when copying.
*
* @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,
onError: (File, IOException) -> OnErrorAction =
{(file: File, e: IOException) -> throw e }
): Boolean {
fun copy(src: File): OnErrorAction? {
if (!src.exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist"))
}
val relPath = src.relativeTo(this@copyRecursively)
val dstFile = File(dst, relPath)
if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) {
return onError(dstFile, FileAlreadyExistsException(file = src,
other = dstFile,
reason = "The destination file already exists"))
}
try {
if (src.isDirectory()) {
dstFile.mkdirs()
val children = src.listFiles()
if (children == null) {
return onError(src, AccessDeniedException(file = src,
reason = "Cannot list files in a directory"))
}
for (child in children) {
val result = copy(child)
if (result == OnErrorAction.TERMINATE) {
return result
{ file, e -> throw e }
): Boolean {
if (!exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist")) !=
OnErrorAction.TERMINATE
}
try {
for (src in walkTopDown().fail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
if (!src.exists()) {
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) ==
OnErrorAction.TERMINATE)
return false
} else {
val relPath = src.relativeTo(this)
val dstFile = File(dst, relPath)
if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) {
if (onError(dstFile, FileAlreadyExistsException(file = src,
other = dstFile,
reason = "The destination file already exists")) == OnErrorAction.TERMINATE)
return false
} else if (src.isDirectory()) {
dstFile.mkdirs()
} else {
if (src.copyTo(dstFile, true) != src.length()) {
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.
* 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.
*
* Note that if this operation fails then partial deletion may have taken place.
*/
public fun File.deleteRecursively(): Boolean {
if (isDirectory()) {
listFiles()?.forEach { it.deleteRecursively() }
}
return delete()
var result = exists()
walkBottomUp().forEach { if (!it.delete()) result = false }
return result
}
/**
* @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.
*/
public fun File.listFiles(filter: (file: File) -> Boolean): Array<File>? = listFiles(
object : FileFilter {
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]
@@ -348,22 +332,14 @@ public fun File.iterator(): Iterator<File> = FileIterator(this)
* @return true if this path starts with [o] path, false otherwise
*/
public fun File.startsWith(o: File): Boolean {
val it = FileIterator(this)
val otherIt = FileIterator(o)
// Roots must be same OR other path can have no root
if (it.root != otherIt.root && otherIt.root != "")
val components = filePathComponents()
val otherComponents = o.filePathComponents()
if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
return false
// Other path is empty
if (!otherIt.hasNext())
return true
val count = this.nameCount
val otherCount = o.nameCount
// This path is shorted than the other
if (count < otherCount)
if (components.size < otherComponents.size)
return false
// Compare first elements until other ends
while (otherIt.hasNext()) {
if (it.next() != otherIt.next())
for (i in 0..otherComponents.size - 1) {
if (components.fileList[i] != otherComponents.fileList[i])
return false
}
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
*/
public fun File.endsWith(o: File): Boolean {
val it = FileIterator(this)
val otherIt = FileIterator(o)
// Roots must be same OR other path can have no root
if (it.root != otherIt.root && otherIt.root != "")
val components = filePathComponents()
val otherComponents = o.filePathComponents()
if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
return false
// Other path is empty
if (!otherIt.hasNext())
return true
var theirs = otherIt.next()
val count = this.nameCount
val otherCount = o.nameCount
// This path is shorted than the other
if (count < otherCount)
val shift = components.size - otherComponents.size
if (shift < 0)
return false
var ours = it.next()
// Move forward to have same number of elements remaining
for (i in 0..count-otherCount-1) {
ours = it.next()
for (i in 0..otherComponents.size - 1) {
if (components.fileList[i + shift] != otherComponents.fileList[i])
return false
}
// 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
}
@@ -434,17 +395,15 @@ public fun File.endsWith(o: String): Boolean = endsWith(File(o))
* @return normalized pathname with . and possibly .. removed
*/
public fun File.normalize(): File {
val rootName = rootName
val list: MutableList<String> = ArrayList()
for (elem in this)
if (elem.toString() != ".")
list.add(elem.toString())
val components = filePathComponents()
val rootName = components.rootName
val list: MutableList<String> = components.fileList.filter { it.toString() != "." }.map { it.toString() }.toLinkedList()
var first = 0
var dots = list.subList(first, list.size()).indexOf("..")
while (dots != -1) {
if (dots > 0) {
list.remove(dots+first)
list.remove(dots+first-1)
list.remove(dots + first)
list.remove(dots + first - 1)
} else {
first++
}
@@ -474,10 +433,7 @@ public fun File.resolve(o: File): File {
if (o.root != null)
return o
val ourName = toString()
if (ourName.endsWith(File.separatorChar))
return File(ourName + o)
else
return File(ourName + File.separatorChar + o)
return if (ourName.endsWith(File.separatorChar)) File(ourName + o) else 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
*/
public fun File.resolveSibling(o: File): File {
val parentFile = parent
if (parentFile != null)
return parentFile.resolve(o)
else
return o
val components = filePathComponents()
val rootName = components.rootName
val parentFile = when (components.size) {
0 -> null
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
import java.io.*
import org.junit.Test as test
import kotlin.test.assertEquals
import java.io.File
import java.io.IOException
import java.io.FileNotFoundException
import java.util.NoSuchElementException
import java.util.HashSet
import java.util.ArrayList
import kotlin.io.files.walkBottomUp
import kotlin.io.files.walkTopDown
import kotlin.io.walkBottomUp
import kotlin.io.walkTopDown
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -29,7 +27,8 @@ class FilesTest {
try {
createTempDir("a")
assert(false)
} catch(e: IllegalArgumentException) {}
} catch(e: IllegalArgumentException) {
}
val dir2 = createTempDir("temp")
assert(dir2.exists() && dir2.isDirectory() && dir2.name.endsWith(".tmp"))
@@ -49,7 +48,8 @@ class FilesTest {
try {
createTempFile("a")
assert(false)
} catch(e: IllegalArgumentException) {}
} catch(e: IllegalArgumentException) {
}
val file2 = createTempFile("temp")
assert(file2.exists() && file2.name.endsWith(".tmp"))
@@ -81,7 +81,7 @@ class FilesTest {
try {
val referenceNames =
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>()
for (file in basedir.walkTopDown()) {
val name = file.relativeTo(basedir)
@@ -106,7 +106,7 @@ class FilesTest {
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
{ it -> it.separatorsToSystem() }).toHashSet()
val namesTopDownEnter = HashSet<String>()
val namesTopDownLeave = HashSet<String>()
val namesTopDown = HashSet<String>()
@@ -116,12 +116,14 @@ class FilesTest {
namesTopDownEnter.add(name)
assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance")
}
fun leave(file: File) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDownLeave.contains(name), "$name is left twice")
namesTopDownLeave.add(name)
assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance")
}
fun visit(file: File) {
val name = file.relativeTo(basedir)
if (file.isDirectory()) {
@@ -163,9 +165,10 @@ class FilesTest {
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
assertEquals(referenceNames, basedir.walkTopDown().filter{ it.isDirectory() }.map{
it.relativeTo(basedir) }.toHashSet())
{ it -> it.separatorsToSystem() }).toHashSet()
assertEquals(referenceNames, basedir.walkTopDown().filter { it.isDirectory() }.map {
it.relativeTo(basedir)
}.toHashSet())
} finally {
basedir.deleteRecursively()
}
@@ -177,7 +180,7 @@ class FilesTest {
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
{ it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>()
fun enter(file: File) {
assertTrue(file.isDirectory())
@@ -202,7 +205,7 @@ class FilesTest {
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
{ it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>()
fun enter(file: File) {
assertTrue(file.isDirectory())
@@ -229,9 +232,10 @@ class FilesTest {
// Everything ended with 3 is filtered
return (!file.name.endsWith("3"));
}
val referenceNames =
listOf("", "1", "1/2", "6", "7.txt", "8", "8/9.txt").map(
{it -> it.separatorsToSystem()}).toHashSet()
{ it -> it.separatorsToSystem() }).toHashSet()
val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown().filter(::filter)) {
val name = file.relativeTo(basedir)
@@ -254,18 +258,18 @@ class FilesTest {
test fun withTotalFilter() {
val basedir = createTestFiles()
try {
// Everything is filtered
fun filter(file: File) = false
val referenceNames: Set<String> = setOf()
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)
assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name)
}
assertEquals(referenceNames, namesTopDown)
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)
assertFalse(namesBottomUp.contains(name), "$name is visited twice")
namesBottomUp.add(name)
@@ -303,7 +307,7 @@ class FilesTest {
test fun withReduce() {
val basedir = createTestFiles()
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}")
} finally {
basedir.deleteRecursively()
@@ -321,27 +325,30 @@ class FilesTest {
stack.add(dir)
dirs.add(dir.relativeTo(basedir))
}
fun afterVisitDirectory(dir: File) {
assertEquals(stack.last(), dir)
stack.remove(stack.lastIndex)
}
fun visitFile(file: File) {
assert(stack.last().listFiles().contains(file), file)
files.add(file.relativeTo(basedir))
}
fun visitDirectoryFailed(dir: File, e: IOException) {
assertEquals(stack.last(), dir)
stack.remove(stack.lastIndex)
failed.add(dir.name)
}
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())
val sep = File.separator
for (fileName in array("", "1", "1${sep}2", "1${sep}3", "6", "8")) {
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)
}
@@ -349,7 +356,7 @@ class FilesTest {
files.clear()
dirs.clear()
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(dirs.size() == 1 && dirs.contains(""), dirs.size())
for (file in array("1", "6", "7.txt", "8")) {
@@ -362,7 +369,7 @@ class FilesTest {
files.clear()
dirs.clear()
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(failed.size() == 1 && failed.contains("1"), failed.size())
assert(dirs.size() == 4, dirs.size())
@@ -393,7 +400,7 @@ class FilesTest {
assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it)
visited.add(it)
}
basedir.walkTopDown().forEach( block )
basedir.walkTopDown().forEach(block)
assert(visited.size() == 10, visited.size())
} finally {
@@ -412,7 +419,7 @@ class FilesTest {
assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it)
visited.add(it)
}
basedir.walkTopDown().forEach( block )
basedir.walkTopDown().forEach(block)
assert(visited.size() == 6, visited.size())
}
} finally {
@@ -460,7 +467,7 @@ class FilesTest {
try {
File(basedir, "8/4.txt".separatorsToSystem()).createNewFile()
var count = 0
basedir.walkTopDown().takeWhile{ it -> count == 0 }.forEach {
basedir.walkTopDown().takeWhile { it -> count == 0 }.forEach {
if (it.name == "4.txt") {
count++
}
@@ -511,10 +518,13 @@ class FilesTest {
} finally {
dir.delete()
}
try {
dir.walkTopDown()
assert(false)
} catch(e: FileNotFoundException) {}
}
test fun recurse() {
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("temp3", ".kt", dir)
// This line works only with Kotlin File.listFiles(filter)
val result = dir.listFiles { it.getName().endsWith(".kt") }
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() {
@@ -570,11 +584,10 @@ class FilesTest {
val file1 = File("C:/dir1".separatorsToSystem())
val file2 = File("D:/dir2".separatorsToSystem())
try {
val winRelPath = file1.relativeTo(file2)
assert(file1.canonicalPath.charAt(0) == '/')
assertEquals("../../C:/dir1", winRelPath)
file1.relativeTo(file2)
assert(false);
} catch (e: IllegalArgumentException) {
assert(Character.isLetter(file1.canonicalPath.charAt(0)))
// It's the thing we should get here
} catch (e: IOException) {
// The device is not ready (D) ==> DO NOTHING
}
@@ -593,8 +606,8 @@ class FilesTest {
private fun checkFileElements(f: File, root: File?, elements: List<String>) {
var i = 0
assertEquals(root, f.root)
for (elem in f) {
assertTrue(i < elements.size())
for (elem in f.filePathComponents().fileList) {
assertTrue(i < elements.size(), i.toString())
assertEquals(elements[i++], elem.toString())
}
assertEquals(elements.size(), i)
@@ -602,11 +615,14 @@ class FilesTest {
test fun fileIterator() {
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("bar/gav"), null, listOf("bar", "gav"))
checkFileElements(File("C:\\bar\\gav"), File("C:\\"), listOf("bar", "gav"))
checkFileElements(File("C:\\"), File("C:\\"), listOf())
checkFileElements(File("C:\\bar\\gav"), File("C:\\".separatorsToSystem()), listOf("bar", "gav"))
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("//host.ru/home/mike"), File("//host.ru/home"), listOf("mike"))
checkFileElements(File(""), null, listOf(""))
@@ -631,13 +647,18 @@ class FilesTest {
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("bar/gav"), File("/foo/bar/gav/hi").subPath(1, 3))
}
test fun 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("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())
}
@@ -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("/gav"), File("/foo/bar").resolve("/gav"))
assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"),
File("C:\\Users\\Me").resolve("Documents\\important.doc"))
// For Unix C:\path is not correct so it's cannot be automatically converted
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() {
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("C:\\Users\\Me\\Documents\\important.doc"),
File("C:\\Users\\Me\\profile.ini").resolveSibling("Documents\\important.doc"))
// For Unix C:\path is not correct so it's cannot be automatically converted
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() {
@@ -701,7 +728,8 @@ class FilesTest {
try {
srcFile.copyTo(dstFile)
assert(false)
} catch (e: FileAlreadyExistsException) {}
} catch (e: FileAlreadyExistsException) {
}
var len = srcFile.copyTo(dstFile, overwrite = true)
assertEquals(13L, len)
@@ -727,13 +755,15 @@ class FilesTest {
try {
srcFile.copyTo(dstFile)
assert(false)
} catch (e: NoSuchFileException) {}
} catch (e: NoSuchFileException) {
}
srcFile.mkdir()
try {
srcFile.copyTo(dstFile)
assert(false)
} catch (e: IllegalArgumentException) {}
} catch (e: IllegalArgumentException) {
}
srcFile.delete()
}
@@ -787,7 +817,7 @@ class FilesTest {
var conflicts = 0
src.copyRecursively(dst) {
(file: File, e: IOException) ->
file: File, e: IOException ->
if (e is FileAlreadyExistsException) {
conflicts++
OnErrorAction.SKIP
@@ -802,7 +832,7 @@ class FilesTest {
dst.deleteRecursively()
var caught = false
assert(src.copyRecursively(dst) {
(file: File, e: IOException) ->
file: File, e: IOException ->
if (e is AccessDeniedException) {
caught = true
OnErrorAction.SKIP
@@ -826,7 +856,7 @@ class FilesTest {
}
assert(!src.copyRecursively(dst) {
(file: File, e: IOException) ->
file: File, e: IOException ->
OnErrorAction.TERMINATE
})
} finally {
@@ -834,4 +864,28 @@ class FilesTest {
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()
//file.replaceText("Hello\nWorld")
file.forEachBlock{ (arr: ByteArray, size: Int) ->
file.forEachBlock {(arr: ByteArray, size: Int) ->
assertTrue(size >= 11 && size <= 12, size.toString())
assertTrue(arr.contains('W'.toByte()))
}
val list = ArrayList<String>()
file.forEachLine{
file.forEachLine {
list.add(it)
}
assertEquals(arrayListOf("Hello", "World"), list)