IO second review: empty File.subPath() allowed, OutputStream.bufferedWriter() added,

normalize(), startsWith(), endsWith() rewritten, LinesStream --> LinesSequence,
File.replaceBytes() / replaceText() rolled back to writeBytes() / writeText(),
some exception messages formulated more clearly, refactorings, comments,
relativeTo() slightly optimized, additional tests
This commit is contained in:
Mikhail Glukhikh
2015-03-23 16:16:30 +03:00
parent 88733d34eb
commit 2bc124fa3d
8 changed files with 238 additions and 282 deletions
@@ -1,5 +1,5 @@
val x = LinesStream<caret> val x = LinesSequence<caret>
// INVOCATION_COUNT: 2 // INVOCATION_COUNT: 2
// EXIST: { lookupString:"LinesStream", tailText:" (kotlin.io)" } // EXIST: { lookupString:"LinesSequence", tailText:" (kotlin.io)" }
// NUMBER: 1 // NUMBER: 1
+5 -21
View File
@@ -62,12 +62,6 @@ public fun InputStream.reader(charset: String): InputStreamReader = InputStreamR
/** Creates a buffered reader on this input stream using the specified [charset]. */ /** Creates a buffered reader on this input stream using the specified [charset]. */
public fun InputStream.bufferedReader(charset: String): BufferedReader = reader(charset).buffered() 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. * Creates a buffered output stream wrapping this stream.
* @param bufferSize the buffer size to use. * @param bufferSize the buffer size to use.
@@ -78,11 +72,14 @@ public fun OutputStream.buffered(bufferSize: Int = defaultBufferSize): BufferedO
/** Creates a writer on this output stream using UTF-8 or the specified [charset]. */ /** Creates a writer on this output stream using UTF-8 or the specified [charset]. */
public fun OutputStream.writer(charset: Charset = Charsets.UTF_8): OutputStreamWriter = OutputStreamWriter(this, charset) public fun OutputStream.writer(charset: Charset = Charsets.UTF_8): OutputStreamWriter = OutputStreamWriter(this, charset)
/** Creates a buffered writer on this output stream using UTF-8 or the specified [charset]. */
public fun OutputStream.bufferedWriter(charset: Charset = Charsets.UTF_8): BufferedWriter = writer(charset).buffered()
/** Creates a writer on this output stream using the specified [charset]. */ /** Creates a writer on this output stream using the specified [charset]. */
public fun OutputStream.writer(charset: String): OutputStreamWriter = OutputStreamWriter(this, charset) public fun OutputStream.writer(charset: String): OutputStreamWriter = OutputStreamWriter(this, charset)
/** Creates a writer on this output stream using the specified [encoder]. */ /** Creates a buffered writer on this output stream using the specified [charset]. */
public fun OutputStream.writer(encoder: CharsetEncoder): OutputStreamWriter = OutputStreamWriter(this, encoder) public fun OutputStream.bufferedWriter(charset: String): BufferedWriter = writer(charset).buffered()
/** /**
* Copies this stream to the given output stream, returning the number of bytes copied * Copies this stream to the given output stream, returning the number of bytes copied
@@ -112,16 +109,3 @@ public fun InputStream.readBytes(estimatedSize: Int = defaultBufferSize): ByteAr
return buffer.toByteArray() return buffer.toByteArray()
} }
/**
* Constructs a new FileInputStream of this file and returns it as a result.
*/
public fun File.inputStream(): InputStream {
return FileInputStream(this)
}
/**
* Constructs a new FileOutputStream of this file and returns it as a result.
*/
public fun File.outputStream(): OutputStream {
return FileOutputStream(this)
}
+57 -87
View File
@@ -33,49 +33,34 @@ public fun File.bufferedWriter(bufferSize: Int = defaultBufferSize): BufferedWri
/** /**
* Returns a new [PrintWriter] for writing the content of this file. * Returns a new [PrintWriter] for writing the content of this file.
*/ */
public fun File.printWriter(): PrintWriter = PrintWriter(writer().buffered()) public fun File.printWriter(): PrintWriter = PrintWriter(bufferedWriter())
/** /**
* Reads the entire content of this file as a byte array. * Gets the entire content of this file as a byte array.
* *
* This method is not recommended on huge files. It has an internal limitation of 2 GB byte array size * This method is not recommended on huge files. It has an internal limitation of 2 GB byte array size
* *
* @return the entire content of this file as a byte array * @return the entire content of this file as a byte array
*/ */
public fun File.readBytes(): ByteArray { public fun File.readBytes(): ByteArray = FileInputStream(this).use { it.readBytes(length().toInt()) }
return FileInputStream(this).use { it.readBytes(length().toInt()) }
}
/** /**
* Replaces the content of this file with [data]. * Sets the content of this file as an [array] of bytes.
* If this file already exists, it becomes overwritten.
* *
* @param data byte array to write into this file * @param array byte array to write into this file
*/ */
deprecated("Use replaceBytes instead") public fun File.writeBytes(array: ByteArray): Unit = FileOutputStream(this).use { it.write(array) }
public fun File.writeBytes(data: ByteArray): Unit {
return FileOutputStream(this).use { it.write(data) }
}
/** /**
* Replaces the content of this file with [data]. * Appends an [array] of bytes to the content of this file.
* *
* @param data byte array to write into this file * @param array byte array to append to this file
*/ */
public fun File.replaceBytes(data: ByteArray): Unit { public fun File.appendBytes(array: ByteArray): Unit = FileOutputStream(this, true).use { it.write(array) }
return FileOutputStream(this).use { it.write(data) }
}
/** /**
* Appends [data] to the content of this file. * Gets the entire content of this file as a String using specified [charset].
*
* @param data byte array to append to this file
*/
public fun File.appendBytes(data: ByteArray): Unit {
return FileOutputStream(this, true).use { it.write(data) }
}
/**
* Reads the entire content of this file as a String using specified [charset].
* *
* This method is not recommended on huge files. It has an internal limitation of 2 GB file size * This method is not recommended on huge files. It has an internal limitation of 2 GB file size
* *
@@ -85,7 +70,7 @@ public fun File.appendBytes(data: ByteArray): Unit {
public fun File.readText(charset: String): String = readBytes().toString(charset) public fun File.readText(charset: String): String = readBytes().toString(charset)
/** /**
* Reads the entire content of this file as a String using UTF-8 or specified [charset]. * Gets the entire content of this file as a String using UTF-8 or specified [charset].
* *
* This method is not recommended on huge files. It has an internal limitation of 2 GB file size * This method is not recommended on huge files. It has an internal limitation of 2 GB file size
* *
@@ -95,47 +80,22 @@ 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 content of this file with [text] encoded using the specified [charset]. * Sets the content of this file as [text] encoded using the specified [charset].
* If this file exists, it becomes overwritten.
* *
* @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
*/ */
deprecated("Use replaceText instead") public fun File.writeText(text: String, charset: String): Unit = writeBytes(text.toByteArray(charset))
public fun File.writeText(text: String, charset: String): Unit {
replaceBytes(text.toByteArray(charset))
}
/** /**
* Replaces the content of this file with [text] encoded using the specified [charset]. * Sets the content of this file as [text] encoded using UTF-8 or specified [charset].
* If this file exists, it becomes overwritten.
* *
* @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
*/ */
deprecated("There is a same function with default parameter value") public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset))
public fun File.replaceText(text: String, charset: String): Unit {
replaceBytes(text.toByteArray(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
*/
deprecated("Use replaceText instead")
public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit {
replaceBytes(text.toByteArray(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
*/
public fun File.replaceText(text: String, charset: Charset = Charsets.UTF_8): Unit {
replaceBytes(text.toByteArray(charset))
}
/** /**
* Appends [text] to the content 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].
@@ -143,9 +103,7 @@ public fun File.replaceText(text: String, charset: Charset = Charsets.UTF_8): Un
* @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
*/ */
public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit { public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit = appendBytes(text.toByteArray(charset))
appendBytes(text.toByteArray(charset))
}
/** /**
* Appends [text] to the content of the file using the specified [charset]. * Appends [text] to the content of the file using the specified [charset].
@@ -153,31 +111,29 @@ public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Uni
* @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
*/ */
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. * Reads file by byte blocks and calls [operation] for each block read.
* Block has default size which is implementation-dependent. * 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 [operation] 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 operation function to process file blocks
*/ */
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit = forEachBlock(closure, defaultBlockSize) public fun File.forEachBlock(operation: (ByteArray, Int) -> Unit): Unit = forEachBlock(operation, defaultBlockSize)
/** /**
* Reads file by byte blocks and calls [closure] for each block read. * Reads file by byte blocks and calls [operation] for each block read.
* 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 [operation] 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 operation function to process file blocks
* @param blockSize size of a block, replaced by 512 if it's less, 4096 by default * @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 { public fun File.forEachBlock(operation: (ByteArray, Int) -> Unit, blockSize: Int): Unit {
val arr = ByteArray(if (blockSize < minimumBlockSize) minimumBlockSize else blockSize) val arr = ByteArray(if (blockSize < minimumBlockSize) minimumBlockSize else blockSize)
val fis = FileInputStream(this) val fis = FileInputStream(this)
@@ -187,7 +143,7 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit, blockSize: Int):
if (size <= 0) { if (size <= 0) {
break break
} else { } else {
closure(arr, size) operation(arr, size)
} }
} while (true) } while (true)
} finally { } finally {
@@ -196,28 +152,28 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit, blockSize: Int):
} }
/** /**
* Reads this file line by line using the specified [charset] and calls [closure] for each line. * Reads this file line by line using the specified [charset] and calls [operation] for each line.
* Default charset is UTF-8. * Default charset is UTF-8.
* *
* You may use this function on huge files. * You may use this function on huge files.
* *
* @param charset character set to use * @param charset character set to use
* @param closure function to proceed file lines * @param operation function to process 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, operation: (line: String) -> Unit): Unit {
// Note: close is called at forEachLine // Note: close is called at forEachLine
BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(closure) BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(operation)
} }
/** /**
* Reads this file line by line using the specified [charset] and calls [closure] for each line. * Reads this file line by line using the specified [charset] and calls [operation] for each line.
* *
* You may use this function on huge files. * You may use this function on huge files.
* *
* @param charset character set to use * @param charset character set to use
* @param closure function to proceed file lines * @param operation function to process file lines
*/ */
public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): Unit = forEachLine(Charset.forName(charset), closure) public fun File.forEachLine(charset: String, operation: (line: String) -> Unit): Unit = forEachLine(Charset.forName(charset), operation)
/** /**
* Reads the file content as a list of lines, using the specified [charset]. * Reads the file content as a list of lines, using the specified [charset].
@@ -229,6 +185,20 @@ public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): U
*/ */
public fun File.readLines(charset: String): List<String> = readLines(Charset.forName(charset)) public fun File.readLines(charset: String): List<String> = readLines(Charset.forName(charset))
/**
* Constructs a new FileInputStream of this file and returns it as a result.
*/
public fun File.inputStream(): InputStream {
return FileInputStream(this)
}
/**
* Constructs a new FileOutputStream of this file and returns it as a result.
*/
public fun File.outputStream(): OutputStream {
return FileOutputStream(this)
}
/** /**
* Reads the file content as a list of lines. By default uses UTF-8 charset. * Reads the file content as a list of lines. By default uses UTF-8 charset.
* *
@@ -255,7 +225,7 @@ public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter
* Iterates through each line of this reader, calls [block] for each line read * Iterates through each line of this reader, calls [block] for each line read
* and closes the [Reader] when it's completed * and closes the [Reader] when it's completed
* *
* @param block function to proceed file lines * @param block function to process file lines
*/ */
public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { it.forEach(block) } public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { it.forEach(block) }
@@ -278,12 +248,12 @@ public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
* *
* @return a sequence of corresponding file lines * @return a sequence of corresponding file lines
*/ */
public fun BufferedReader.lines(): Sequence<String> = LinesStream(this) public fun BufferedReader.lines(): Sequence<String> = LinesSequence(this)
deprecated("Use lines() function which returns Sequence<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 LinesSequence(private val reader: BufferedReader) : Sequence<String> {
override public 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
@@ -312,7 +282,7 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
/** /**
* Reads this reader completely as a String * Reads this reader completely as a String
* *
* *Note*: It is the caller's responsibility to close this resource. * *Note*: It is the caller's responsibility to close this reader.
* *
* @return the string with corresponding file content * @return the string with corresponding file content
*/ */
@@ -364,19 +334,19 @@ public fun URL.readText(charset: String): String = readBytes().toString(charset)
public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset) public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)
/** /**
* Reads the entire content of the URL as bytes * Reads the entire content of the URL as byte array
* *
* This method is not recommended on huge files. * This method is not recommended on huge files.
* *
* @return a byte array with this URL entire content * @return a byte array with this URL entire content
*/ */
public fun URL.readBytes(): ByteArray = openStream().use<InputStream, ByteArray>{ it.readBytes() } public fun URL.readBytes(): ByteArray = openStream().use { 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
* *
* @param block a function to proceed this closable resource * @param block a function to process 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 {
@@ -1,8 +1,22 @@
package kotlin.io package kotlin.io
import java.io.File import java.io.File
import java.util.Collections
import java.util.NoSuchElementException import java.util.NoSuchElementException
/**
* Estimation of a root name by a given file name.
*
* This implementation is able to find /, Drive:/, Drive: or
* //network.name/root as possible root names.
* / denotes File.separator here so \ can be used instead.
* All other possible roots cannot be identified by this implementation.
* It's also not guaranteed (but possible) that function will be able to detect a root
* which is incorrect for current OS. For instance, in Unix function cannot detect
* network root names like //network.name/root, but can detect Windows roots like C:/.
*
* @return string representing the root for this file, or empty string is this file name is relative
*/
private fun String.getRootName(): String { private fun String.getRootName(): String {
// Note: separators should be already replaced to system ones // Note: separators should be already replaced to system ones
var first = indexOf(File.separatorChar, 0) var first = indexOf(File.separatorChar, 0)
@@ -36,9 +50,17 @@ private fun String.getRootName(): String {
} }
/** /**
* Returns a string representation of root component of this abstract name, like / from /home/user, or C:\ from C:\file.tmp, * Estimation of a root name for this file.
* or //my.host/home for //my.host/home/user, *
* or empty string if this name is relative, like bar/gav * This implementation is able to find /, Drive:/, Drive: or
* //network.name/root as possible root names.
* / denotes File.separator here so \ can be used instead.
* All other possible roots cannot be identified by this implementation.
* It's also not guaranteed (but possible) that function will be able to detect a root
* which is incorrect for current OS. For instance, in Unix function cannot detect
* network root names like //network.name/root, but can detect Windows roots like C:/.
*
* @return string representing the root for this file, or empty string is this file name is relative
*/ */
public val File.rootName: String public val File.rootName: String
get() = separatorsToSystem().getRootName() get() = separatorsToSystem().getRootName()
@@ -55,23 +77,13 @@ public val File.root: File?
} }
public data class FilePathComponents(public val rootName: String, public val fileList: List<File>) { public data class FilePathComponents(public val rootName: String, public val fileList: List<File>) {
public val size: Int = fileList.size() public fun size(): Int = fileList.size()
public fun subPath(beginIndex: Int, endIndex: Int): File { public fun subPath(beginIndex: Int, endIndex: Int): File {
if (beginIndex < 0 || beginIndex >= endIndex || endIndex > size) if (beginIndex < 0 || beginIndex > endIndex || endIndex > size())
throw IllegalArgumentException() throw IllegalArgumentException()
var res = "" return File(fileList.subList(beginIndex, endIndex).joinToString(File.separator))
var first = true
for (elem in fileList.subList(beginIndex, endIndex)) {
if (!first) {
res += File.separatorChar
} else {
first = false
}
res += elem.toString()
}
return File(res)
} }
} }
@@ -82,6 +94,7 @@ public fun File.filePathComponents(): FilePathComponents {
// if: a special case when we have only root component // if: a special case when we have only root component
// Split not only by / or \, but also by //, ///, \\, \\\, etc. // Split not only by / or \, but also by //, ///, \\, \\\, etc.
val list = if (rootName.length() > 0 && subPath.isEmpty()) listOf() else val list = if (rootName.length() > 0 && subPath.isEmpty()) listOf() else
// Looks awful but we split just by /+ or \+ depending on OS
subPath.split("""\Q${File.separatorChar}\E+""").toList().map { it -> File(it) } subPath.split("""\Q${File.separatorChar}\E+""").toList().map { it -> File(it) }
return FilePathComponents(rootName, list) return FilePathComponents(rootName, list)
} }
@@ -94,6 +107,6 @@ public fun File.filePathComponents(): FilePathComponents {
* number count-1 belongs to a component farthest from the root * number count-1 belongs to a component farthest from the root
* @throws IllegalArgumentException if [beginIndex] is negative, * @throws IllegalArgumentException if [beginIndex] is negative,
* or [endIndex] is greater than existing number of components, * or [endIndex] is greater than existing number of components,
* or [beginIndex] is greater than or equals to [endIndex] * or [beginIndex] is greater than [endIndex]
*/ */
public fun File.subPath(beginIndex: Int, endIndex: Int): File = filePathComponents().subPath(beginIndex, endIndex) public fun File.subPath(beginIndex: Int, endIndex: Int): File = filePathComponents().subPath(beginIndex, endIndex)
@@ -8,9 +8,9 @@ import java.util.NoSuchElementException
import java.util.Stack import java.util.Stack
/** /**
* Created by Mikhail.Glukhikh on 13/03/2015. * Created by Mikhail Glukhikh on 13/03/2015.
* *
* An alternative version of file walker * An implementation of file walker
*/ */
/** /**
@@ -31,6 +31,15 @@ public enum class FileWalkDirection {
* It allows to iterate through all files inside [start] directory. * It allows to iterate through all files inside [start] directory.
* If [start] is just a file, walker iterates only it. * If [start] is just a file, walker iterates only it.
* If [start] does not exist, walker does not do any iterations at all. * If [start] does not exist, walker does not do any iterations at all.
*
* @param start directory to walk into
* @param direction selects top-down or bottom-up order (in other words, parents first or children first).
* @param enter is called on any entered directory before its files are visited and before it is visited itself
* @param leave is called on any left directory after its files are visited and after it is visited itself
* @param fail is called on a directory when it's impossible to get its file list
* @param filter is called just before visiting a file, and if false is returned, file is not visited
* @param maxDepth is maximum walking depth, it must be positive. With a value of 1,
* walker visits [start] and all its children, with a value of 2 also grandchildren, etc.
*/ */
public class FileTreeWalk(private val start: File, public class FileTreeWalk(private val start: File,
private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN, private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
@@ -41,15 +50,18 @@ public class FileTreeWalk(private val start: File,
private val maxDepth: Int = Int.MAX_VALUE private val maxDepth: Int = Int.MAX_VALUE
) : Sequence<File> { ) : Sequence<File> {
/** Abstract class that encapsulates file visiting in some order, beginning from a given [rootDir] */
private abstract class DirectoryState(public 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")
} }
/** Call of this function proceeds to a next file for visiting and returns it */
abstract public fun step(): File? abstract public fun step(): File?
} }
/** Visiting in bottom-up order */
private inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) { private inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) {
private var rootVisited = false private var rootVisited = false
@@ -60,6 +72,7 @@ public class FileTreeWalk(private val start: File,
private var failed = false private var failed = false
/** First all children, then root directory */
override public fun step(): File? { override public fun step(): File? {
if (!failed && fileList == null) { if (!failed && fileList == null) {
enter(rootDir) enter(rootDir)
@@ -84,6 +97,7 @@ public class FileTreeWalk(private val start: File,
} }
} }
/** Visiting in top-down order */
private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) { private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {
private var rootVisited = false private var rootVisited = false
@@ -92,6 +106,7 @@ public class FileTreeWalk(private val start: File,
private var fileIndex = 0 private var fileIndex = 0
/** First root directory, then all children */
override public fun step(): File? { override public fun step(): File? {
if (!rootVisited) { if (!rootVisited) {
// First visit root // First visit root
@@ -167,10 +182,10 @@ public class FileTreeWalk(private val start: File,
if (!filter(file)) if (!filter(file))
return gotoNext() return gotoNext()
if (file == topState.rootDir || !file.isDirectory() || state.size() >= maxDepth) { if (file == topState.rootDir || !file.isDirectory() || state.size() >= maxDepth) {
// Proceed a root directory or a simple file // Proceed to a root directory or a simple file
return file return file
} else { } else {
// Proceed a sub-directory // Proceed to a sub-directory
pushState(file) pushState(file)
return gotoNext() return gotoNext()
} }
@@ -178,42 +193,42 @@ public class FileTreeWalk(private val start: File,
} }
/** /**
* Sets enter directory function. * Sets enter directory [function].
* Enter function is called BEFORE the corresponding directory and its files are visited * Enter [function] is called BEFORE the corresponding directory and its files are visited
*/ */
public fun enter(f: (File) -> Unit): FileTreeWalk { public fun enter(function: (File) -> Unit): FileTreeWalk {
return FileTreeWalk(start, direction, f, leave, fail, filter, maxDepth) return FileTreeWalk(start, direction, function, leave, fail, filter, maxDepth)
} }
/** /**
* Sets leave directory function. * Sets leave directory [function].
* Leave function is called AFTER the corresponding directory and its files are visited * Leave [function] is called AFTER the corresponding directory and its files are visited
*/ */
public fun leave(f: (File) -> Unit): FileTreeWalk { public fun leave(function: (File) -> Unit): FileTreeWalk {
return FileTreeWalk(start, direction, enter, f, fail, filter, maxDepth) return FileTreeWalk(start, direction, enter, function, fail, filter, maxDepth)
} }
/** /**
* Set fail entering directory function. * Set fail entering directory [function].
* Fail function is called when walker is unable to get list of directory files. * Fail [function] is called when walker is unable to get list of directory files.
* Enter and leave functions are called even in this case. * Enter and leave functions are called even in this case.
*/ */
public fun fail(f: (File, IOException) -> Unit): FileTreeWalk { public fun fail(function: (File, IOException) -> Unit): FileTreeWalk {
return FileTreeWalk(start, direction, enter, leave, f, filter, maxDepth) return FileTreeWalk(start, direction, enter, leave, function, filter, maxDepth)
} }
/** /**
* Sets filter function. * Sets filter [predicate].
* Filter function is called before visiting files and entering directories. * Filter [predicate] function is called before visiting files and entering directories.
* If it returns false, file is not visited, directory is not entered and all its content is also not visited. * If it returns false, file is not visited, directory is not entered and all its content is also not visited.
* If it returns true, everything goes in the regular way * If it returns true, everything goes in the regular way
*/ */
public fun filter(f: (File) -> Boolean): FileTreeWalk { public fun filter(predicate: (File) -> Boolean): FileTreeWalk {
return FileTreeWalk(start, direction, enter, leave, fail, f, maxDepth) return FileTreeWalk(start, direction, enter, leave, fail, predicate, maxDepth)
} }
/** /**
* Sets maximum depth of walk. Int.MAX_VALUE is used for unlimited. * Sets maximum [depth] of walk. Int.MAX_VALUE is used for unlimited.
* Negative and zero values are not allowed * Negative and zero values are not allowed
*/ */
public fun maxDepth(depth: Int): FileTreeWalk { public fun maxDepth(depth: Int): FileTreeWalk {
@@ -222,6 +237,7 @@ public class FileTreeWalk(private val start: File,
return FileTreeWalk(start, direction, enter, leave, fail, filter, depth) return FileTreeWalk(start, direction, enter, leave, fail, filter, depth)
} }
/** An iterator associated with this walker */
private val it = object : Iterator<File> { private val it = object : Iterator<File> {
override public fun hasNext(): Boolean { override public fun hasNext(): Boolean {
if (nextFile == null) if (nextFile == null)
@@ -242,6 +258,7 @@ public class FileTreeWalk(private val start: File,
} }
} }
/** Returns an associated file iterator */
override public fun iterator(): Iterator<File> { override public fun iterator(): Iterator<File> {
return it return it
} }
@@ -271,9 +288,9 @@ public fun File.walkBottomUp(): FileTreeWalk = walk(FileWalkDirection.BOTTOM_UP)
* Recursively process this file and all children with the given block. * 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. * 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 * @param function the function to call on each file
*/ */
deprecated("It's recommended to use walkTopDown() / walkBottomUp()") deprecated("It's recommended to use walkTopDown() / walkBottomUp()")
public fun File.recurse(block: (File) -> Unit): Unit { public fun File.recurse(function: (File) -> Unit): Unit {
walkTopDown().forEach { block(it) } walkTopDown().forEach { function(it) }
} }
+83 -116
View File
@@ -22,7 +22,7 @@ public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directo
if (dir.mkdir()) { if (dir.mkdir()) {
return dir return dir
} else { } else {
throw IOException("Unable to create temporary directory") throw IOException("Unable to create temporary directory $dir")
} }
} }
@@ -76,9 +76,7 @@ public val File.path: String
* Returns 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() = name.substringAfterLast('.', "")
return name.substringAfterLast('.', "")
}
/** /**
* Replaces all separators in the string used to separate directories with system ones and returns the resulting string. * Replaces all separators in the string used to separate directories with system ones and returns the resulting string.
@@ -143,28 +141,28 @@ public fun File.relativeTo(base: File): String {
// Check roots // Check roots
val components = filePathComponents() val components = filePathComponents()
val baseComponents = base.filePathComponents() val baseComponents = base.filePathComponents()
if (components.rootName != base.rootName) if (components.rootName != baseComponents.rootName)
throw IllegalArgumentException("this and base files have different roots") throw IllegalArgumentException("this and base files have different roots: ${components.rootName} and ${baseComponents.rootName}")
var i = 0 var i = 0
while (i < components.size && i < baseComponents.size && components.fileList[i] == baseComponents.fileList[i]) while (i < components.size() && i < baseComponents.size() && components.fileList[i] == baseComponents.fileList[i])
i++ i++
val sameCount = i val sameCount = i
val baseCount = baseComponents.size val baseCount = baseComponents.size()
// Add all .. // Add all ..
var res = "" val res = StringBuilder()
for (j in sameCount + 1..baseCount - 1) for (j in sameCount..baseCount - 2)
res += (".." + File.separator) res.append("..").append(File.separator)
// If .. is the last element, no separator should present // If .. is the last element, no separator should present
if (baseCount > sameCount) { if (baseCount > sameCount) {
res += if (sameCount < components.size) ".." + File.separator else ".." res.append(if (sameCount < components.size()) ".." + File.separator else "..")
} }
// Add remaining this components // Add remaining this components
if (sameCount < components.size - 1) if (sameCount < components.size() - 1)
res += (components.subPath(sameCount, components.size - 1).toString() + File.separator) res.append(components.subPath(sameCount, components.size() - 1)).append(File.separator)
// The last one should be without separator // The last one should be without separator
if (sameCount < components.size) if (sameCount < components.size())
res += components.subPath(components.size - 1, components.size).toString() res.append(components.subPath(components.size() - 1, components.size()))
return res return res.toString()
} }
/** /**
@@ -191,8 +189,8 @@ 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.
* the [dst] file is not a non-empty directory. * Otherwise this file overwrites [dst] if it's a file to, or is written into [dst] if it's a directory.
* *
* Note: this function fails if you call it on a directory. * Note: this function fails if you call it on a directory.
* If you want to copy directories, use 'copyRecursively' function instead. * If you want to copy directories, use 'copyRecursively' function instead.
@@ -208,7 +206,7 @@ public fun File.copyTo(dst: File, overwrite: Boolean = false, bufferSize: Int =
if (!exists()) { if (!exists()) {
throw NoSuchFileException(file = this, reason = "The source file doesn't exist") throw NoSuchFileException(file = this, reason = "The source file doesn't exist")
} else if (isDirectory()) { } else if (isDirectory()) {
throw IllegalArgumentException("Use copyRecursively to copy a directory") throw IllegalArgumentException("Use copyRecursively to copy a directory $this")
} else if (dst.exists()) { } else if (dst.exists()) {
if (!overwrite) { if (!overwrite) {
throw FileAlreadyExistsException(file = this, throw FileAlreadyExistsException(file = this,
@@ -243,6 +241,7 @@ public enum class OnErrorAction {
TERMINATE TERMINATE
} }
/** Private exception class, used to terminate recursive copying */
private class TerminateException(file: File) : FileSystemException(file) {} private class TerminateException(file: File) : FileSystemException(file) {}
/** /**
@@ -265,13 +264,14 @@ private class TerminateException(file: File) : FileSystemException(file) {}
*/ */
public fun File.copyRecursively(dst: File, public fun File.copyRecursively(dst: File,
onError: (File, IOException) -> OnErrorAction = onError: (File, IOException) -> OnErrorAction =
{ file, e -> throw e } { file, exception -> throw exception }
): Boolean { ): Boolean {
if (!exists()) { if (!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 OnErrorAction.TERMINATE
} }
try { try {
// We cannot break for loop from inside a lambda, so we have to use an exception here
for (src in walkTopDown().fail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) { for (src in walkTopDown().fail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
if (!src.exists()) { if (!src.exists()) {
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) == if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) ==
@@ -307,86 +307,68 @@ public fun File.copyRecursively(dst: File,
* *
* @return true if the file or directory is successfully deleted, false otherwise. * @return true if the file or directory is successfully deleted, false otherwise.
*/ */
public fun File.deleteRecursively(): Boolean { public fun File.deleteRecursively(): Boolean = walkBottomUp().fold(exists(), { res, it -> res && it.delete() })
var result = exists()
walkBottomUp().forEach { if (!it.delete()) result = false }
return result
}
/** /**
* Returns 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(FileFilter(filter))
object : FileFilter {
override fun accept(file: File) = filter(file)
}
)
/** /**
* Determines whether this file belongs to the same root as [o] * Determines whether this file belongs to the same root as [other]
* and starts with all components of [o] in the same order. * and starts with all components of [other] in the same order.
* So if [o] has N components, first N components of `this` must be the same as in [o]. * So if [other] has N components, first N components of `this` must be the same as in [other].
* For relative [o], `this` can belong to any root. * For relative [other], `this` can belong to any root.
* *
* @return true if this path starts with [o] path, false otherwise * @return true if this path starts with [other] path, false otherwise
*/ */
public fun File.startsWith(o: File): Boolean { public fun File.startsWith(other: File): Boolean {
val components = filePathComponents() val components = filePathComponents()
val otherComponents = o.filePathComponents() val otherComponents = other.filePathComponents()
if (components.rootName != otherComponents.rootName && otherComponents.rootName != "") if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
return false return false
if (components.size < otherComponents.size) return if (components.size() < otherComponents.size()) false
return false else components.fileList.subList(0, otherComponents.size()).equals(otherComponents.fileList)
for (i in 0..otherComponents.size - 1) {
if (components.fileList[i] != otherComponents.fileList[i])
return false
}
return true
} }
/** /**
* Determines whether this file belongs to the same root as [o] * Determines whether this file belongs to the same root as [other]
* and starts with all components of [o] in the same order. * and starts with all components of [other] in the same order.
* So if [o] has N components, first N components of `this` must be the same as in [o]. * So if [other] has N components, first N components of `this` must be the same as in [other].
* For relative [o], `this` can belong to any root. * For relative [other], `this` can belong to any root.
* *
* @return true if this path starts with [o] path, false otherwise * @return true if this path starts with [other] path, false otherwise
*/ */
public fun File.startsWith(o: String): Boolean = startsWith(File(o)) public fun File.startsWith(other: String): Boolean = startsWith(File(other))
/** /**
* Determines whether this file belongs to the same root as [o] * Determines whether this file belongs to the same root as [other]
* and ends with all components of [o] in the same order. * and ends with all components of [other] in the same order.
* So if [o] has N components, last N components of `this` must be the same as in [o]. * So if [other] has N components, last N components of `this` must be the same as in [other].
* For relative [o], `this` can belong to any root. * For relative [other], `this` can belong to any root.
* *
* @return true if this path ends with [o] path, false otherwise * @return true if this path ends with [other] path, false otherwise
*/ */
public fun File.endsWith(o: File): Boolean { public fun File.endsWith(other: File): Boolean {
val components = filePathComponents() val components = filePathComponents()
val otherComponents = o.filePathComponents() val otherComponents = other.filePathComponents()
if (components.rootName != otherComponents.rootName && otherComponents.rootName != "") if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
return false return false
val shift = components.size - otherComponents.size val shift = components.size() - otherComponents.size()
if (shift < 0) return if (shift < 0) false
return false else components.fileList.subList(shift, components.size()).equals(otherComponents.fileList)
for (i in 0..otherComponents.size - 1) {
if (components.fileList[i + shift] != otherComponents.fileList[i])
return false
}
return true
} }
/** /**
* Determines whether this file belongs to the same root as [o] * Determines whether this file belongs to the same root as [other]
* and ends with all components of [o] in the same order. * and ends with all components of [other] in the same order.
* So if [o] has N components, last N components of `this` must be the same as in [o]. * So if [other] has N components, last N components of `this` must be the same as in [other].
* For relative [o], `this` can belong to any root. * For relative [other], `this` can belong to any root.
* *
* @return true if this path ends with [o] path, false otherwise * @return true if this path ends with [other] path, false otherwise
*/ */
public fun File.endsWith(o: String): Boolean = endsWith(File(o)) public fun File.endsWith(other: String): Boolean = endsWith(File(other))
/** /**
* Removes all . and resolves all possible .. in this file name. * Removes all . and resolves all possible .. in this file name.
@@ -397,77 +379,62 @@ public fun File.endsWith(o: String): Boolean = endsWith(File(o))
public fun File.normalize(): File { public fun File.normalize(): File {
val components = filePathComponents() val components = filePathComponents()
val rootName = components.rootName val rootName = components.rootName
val list: MutableList<String> = components.fileList.filter { it.toString() != "." }.map { it.toString() }.toLinkedList() val list: MutableList<String> = ArrayList()
var first = 0 for (file in components.fileList) {
var dots = list.subList(first, list.size()).indexOf("..") val name = file.toString()
while (dots != -1) { when (name) {
if (dots > 0) { "." -> {
list.remove(dots + first) }
list.remove(dots + first - 1) ".." -> if (!list.isEmpty() && list.get(list.size() - 1) != "..") list.remove(list.size() - 1) else list.add(name)
} else { else -> list.add(name)
first++
} }
dots = list.subList(first, list.size()).indexOf("..")
} }
var res = rootName return File(list.joinToString(File.separator, rootName))
var addSeparator = rootName != "" && !rootName.endsWith(File.separatorChar)
for (elem in list) {
if (addSeparator)
res += File.separatorChar
res += elem
addSeparator = true
}
return File(res)
} }
/** /**
* Adds relative [o] to this, considering this as a directory. * Adds [relative] file to this, considering this as a directory.
* If [o] has a root, [o] is returned back. * If [relative] has a root, [relative] is returned back.
* For instance, File("/foo/bar").resolve(File("gav")) is File("/foo/bar/gav"). * For instance, File("/foo/bar").resolve(File("gav")) is File("/foo/bar/gav").
* This function is complementary with (File.relativeTo), * This function is complementary with (File.relativeTo),
* so f.resolve(g.relativeTo(f)) == g should be always true except for different roots case. * so f.resolve(g.relativeTo(f)) == g should be always true except for different roots case.
* *
* @return concatenated this and [o] paths, or just [o] if it's absolute * @return concatenated this and [relative] paths, or just [relative] if it's absolute
*/ */
public fun File.resolve(o: File): File { public fun File.resolve(relative: File): File {
if (o.root != null) if (relative.root != null)
return o return relative
val ourName = toString() val ourName = toString()
return if (ourName.endsWith(File.separatorChar)) File(ourName + o) else File(ourName + File.separatorChar + o) return if (ourName.endsWith(File.separatorChar)) File(ourName + relative) else File(ourName + File.separatorChar + relative)
} }
/** /**
* Adds relative [o] to this, considering this as a directory. * Adds [relative] name to this, considering this as a directory.
* If [o] has a root, [o] is returned back. * If [relative] has a root, [relative] is returned back.
* For instance, File("/foo/bar").resolve("gav") is File("/foo/bar/gav") * For instance, File("/foo/bar").resolve("gav") is File("/foo/bar/gav")
* *
* @return concatenated this and [o] paths, or just [o] if it's absolute * @return concatenated this and [relative] paths, or just [relative] if it's absolute
*/ */
public fun File.resolve(o: String): File = resolve(File(o)) public fun File.resolve(relative: String): File = resolve(File(relative))
/** /**
* Adds relative [o] to this parent directory. * Adds [relative] file to this parent directory.
* If [o] has a root or this has no parent directory, [o] is returned back. * If [relative] has a root or this has no parent directory, [relative] is returned back.
* For instance, File("/foo/bar").resolveSibling(File("gav")) is File("/foo/gav") * For instance, File("/foo/bar").resolveSibling(File("gav")) is File("/foo/gav")
* *
* @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent * @return concatenated this.parent and [relative] paths, or just [relative] if it's absolute or this has no parent
*/ */
public fun File.resolveSibling(o: File): File { public fun File.resolveSibling(relative: File): File {
val components = filePathComponents() val components = filePathComponents()
val rootName = components.rootName val rootName = components.rootName
val parentFile = when (components.size) { return if (components.size() == 0) relative else File(rootName).resolve(components.subPath(0, components.size() - 1)).resolve(relative)
0 -> null
1 -> File(rootName)
else -> File(rootName + components.subPath(0, components.size - 1).path)
}
return if (parentFile != null) parentFile.resolve(o) else o
} }
/** /**
* Adds relative [o] to this parent directory. * Adds [relative] name to this parent directory.
* If [o] has a root or this has no parent directory, [o] is returned back * If [relative] has a root or this has no parent directory, [relative] is returned back
* For instance, File("/foo/bar").resolveSibling("gav") is File("/foo/gav") * For instance, File("/foo/bar").resolveSibling("gav") is File("/foo/gav")
* *
* @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent * @return concatenated this.parent and [relative] paths, or just [relative] if it's absolute or this has no parent
*/ */
public fun File.resolveSibling(o: String): File = resolveSibling(File(o)) public fun File.resolveSibling(relative: String): File = resolveSibling(File(relative))
+6 -5
View File
@@ -733,21 +733,22 @@ class FilesTest {
test fun testCopyTo() { test fun testCopyTo() {
val srcFile = createTempFile() val srcFile = createTempFile()
val dstFile = createTempFile() val dstFile = createTempFile()
srcFile.replaceText("Hello, World!") srcFile.writeText("Hello, World!", "UTF8")
try { try {
srcFile.copyTo(dstFile) srcFile.copyTo(dstFile)
assert(false) assert(false)
} catch (e: FileAlreadyExistsException) { } catch (e: FileAlreadyExistsException) {
println(e.getMessage())
} }
var len = srcFile.copyTo(dstFile, overwrite = true) var len = srcFile.copyTo(dstFile, overwrite = true)
assertEquals(13L, len) assertEquals(13L, len)
assertEquals(srcFile.readText(), dstFile.readText()) assertEquals(srcFile.readText(), dstFile.readText("UTF8"))
assert(dstFile.delete()) assert(dstFile.delete())
len = srcFile.copyTo(dstFile) len = srcFile.copyTo(dstFile)
assertEquals(13L, len) assertEquals(13L, len)
assertEquals(srcFile.readText(), dstFile.readText()) assertEquals(srcFile.readText("UTF8"), dstFile.readText())
assert(dstFile.delete()) assert(dstFile.delete())
dstFile.mkdir() dstFile.mkdir()
@@ -811,8 +812,8 @@ class FilesTest {
createTempDir(prefix = "d1_", directory = subDir1) createTempDir(prefix = "d1_", directory = subDir1)
val file1 = createTempFile(prefix = "f1_", directory = src) val file1 = createTempFile(prefix = "f1_", directory = src)
val file2 = createTempFile(prefix = "f2_", directory = subDir1) val file2 = createTempFile(prefix = "f2_", directory = subDir1)
file1.replaceText("hello") file1.writeText("hello")
file2.replaceText("wazzup") file2.writeText("wazzup")
createTempDir(prefix = "d1_", directory = subDir2) createTempDir(prefix = "d1_", directory = subDir2)
assert(src.copyRecursively(dst)) assert(src.copyRecursively(dst))
+14 -10
View File
@@ -15,10 +15,12 @@ fun sample(): Reader = StringReader("Hello\nWorld");
class ReadWriteTest { class ReadWriteTest {
test fun testAppendText() { test fun testAppendText() {
val file = File.createTempFile("temp", System.nanoTime().toString()) val file = File.createTempFile("temp", System.nanoTime().toString())
file.replaceText("Hello\n") file.writeText("Hello\n", "UTF8")
file.appendText("World") file.appendText("World\n", "UTF8")
file.appendText("Again")
assertEquals("Hello\nWorld", file.readText()) assertEquals("Hello\nWorld\nAgain", file.readText())
assertEquals(listOf("Hello", "World", "Again"), file.readLines("UTF8"))
file.deleteOnExit() file.deleteOnExit()
} }
@@ -71,34 +73,34 @@ 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("UTF8", {
list.add(it) list.add(it)
} })
assertEquals(arrayListOf("Hello", "World"), list) assertEquals(arrayListOf("Hello", "World"), list)
val text = file.inputStream().reader().readText() val text = file.inputStream().reader().readText()
assertTrue(text.contains("Hello")) assertTrue(text.contains("Hello"))
assertTrue(text.contains("World")) assertTrue(text.contains("World"))
file.replaceText("") file.writeText("")
var c = 0 var c = 0
file.forEachLine { c++ } file.forEachLine { c++ }
assertEquals(0, c) assertEquals(0, c)
file.replaceText(" ") file.writeText(" ")
file.forEachLine { c++ } file.forEachLine { c++ }
assertEquals(1, c) assertEquals(1, c)
file.replaceText(" \n") file.writeText(" \n")
c = 0 c = 0
file.forEachLine { c++ } file.forEachLine { c++ }
assertEquals(1, c) assertEquals(1, c)
file.replaceText(" \n ") file.writeText(" \n ")
c = 0 c = 0
file.forEachLine { c++ } file.forEachLine { c++ }
assertEquals(2, c) assertEquals(2, c)
@@ -167,5 +169,7 @@ class ReadWriteTest {
val url = URL("http://kotlinlang.org") val url = URL("http://kotlinlang.org")
val text = url.readText() val text = url.readText()
assertFalse(text.isEmpty()) assertFalse(text.isEmpty())
val text2 = url.readText("UTF8")
assertFalse(text2.isEmpty())
} }
} }