diff --git a/libraries/stdlib/src/kotlin/io/Console.kt b/libraries/stdlib/src/kotlin/io/Console.kt index 35f49022422..099daab0b6f 100644 --- a/libraries/stdlib/src/kotlin/io/Console.kt +++ b/libraries/stdlib/src/kotlin/io/Console.kt @@ -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() diff --git a/libraries/stdlib/src/kotlin/io/Exceptions.kt b/libraries/stdlib/src/kotlin/io/Exceptions.kt index f8533d868f4..ebb089a273f 100644 --- a/libraries/stdlib/src/kotlin/io/Exceptions.kt +++ b/libraries/stdlib/src/kotlin/io/Exceptions.kt @@ -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 diff --git a/libraries/stdlib/src/kotlin/io/IOStreams.kt b/libraries/stdlib/src/kotlin/io/IOStreams.kt index e65114fecfb..89ea6900817 100644 --- a/libraries/stdlib/src/kotlin/io/IOStreams.kt +++ b/libraries/stdlib/src/kotlin/io/IOStreams.kt @@ -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() } diff --git a/libraries/stdlib/src/kotlin/io/ReadWrite.kt b/libraries/stdlib/src/kotlin/io/ReadWrite.kt index e2acf39122c..1c4bbe5a1af 100644 --- a/libraries/stdlib/src/kotlin/io/ReadWrite.kt +++ b/libraries/stdlib/src/kotlin/io/ReadWrite.kt @@ -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 = readLines(Charset.forName(charset)) /** @@ -216,11 +243,11 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List { 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 Reader.useLines(block: (Sequence) -> 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 = LinesStream(this) -deprecated("Use lines() function which returns Stream") +deprecated("Use lines() function which returns Sequence") public fun BufferedReader.lineIterator(): Iterator = lines().iterator() private class LinesStream(private val reader: BufferedReader) : Sequence { - override fun iterator(): Iterator { + override public fun iterator(): Iterator { return object : Iterator { 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 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 * * *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{ 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 * 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.use(block: (T) -> R): R { @@ -358,7 +386,7 @@ public inline fun 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.use(block: (T) -> R): R { throw e } finally { if (!closed) { - this.close() + close() } } } diff --git a/libraries/stdlib/src/kotlin/io/files/FileIterator.kt b/libraries/stdlib/src/kotlin/io/files/FileIterator.kt deleted file mode 100644 index b05ff14d788..00000000000 --- a/libraries/stdlib/src/kotlin/io/files/FileIterator.kt +++ /dev/null @@ -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 { - - 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) - -} diff --git a/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt b/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt new file mode 100644 index 00000000000..27268f16a29 --- /dev/null +++ b/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt @@ -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) { + 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) diff --git a/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt b/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt index 717157e6e59..0a7d1a9013e 100644 --- a/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt +++ b/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt @@ -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 { +/** + * 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 { - 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 { - override fun hasNext(): Boolean { + private val it = object : Iterator { + 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 { + override public fun iterator(): Iterator { 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) } +} diff --git a/libraries/stdlib/src/kotlin/io/files/Utils.kt b/libraries/stdlib/src/kotlin/io/files/Utils.kt index bdfd69fcd2d..40c7483a019 100644 --- a/libraries/stdlib/src/kotlin/io/files/Utils.kt +++ b/libraries/stdlib/src/kotlin/io/files/Utils.kt @@ -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? = 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 = FileIterator(this) +) /** * Determines whether this file belongs to the same root as [o] @@ -348,22 +332,14 @@ public fun File.iterator(): Iterator = 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 = ArrayList() - for (elem in this) - if (elem.toString() != ".") - list.add(elem.toString()) + val components = filePathComponents() + val rootName = components.rootName + val list: MutableList = 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 } /** diff --git a/libraries/stdlib/test/io/Files.kt b/libraries/stdlib/test/io/Files.kt index 5b1c09cdb78..e8f0bee444c 100644 --- a/libraries/stdlib/test/io/Files.kt +++ b/libraries/stdlib/test/io/Files.kt @@ -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() 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() val namesTopDownLeave = HashSet() val namesTopDown = HashSet() @@ -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() 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() 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() 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 = setOf() val namesTopDown = HashSet() - 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() - 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 = 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) { 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()) + } } diff --git a/libraries/stdlib/test/io/ReadWrite.kt b/libraries/stdlib/test/io/ReadWrite.kt index be5f2595ba4..da19e384eda 100644 --- a/libraries/stdlib/test/io/ReadWrite.kt +++ b/libraries/stdlib/test/io/ReadWrite.kt @@ -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() - file.forEachLine{ + file.forEachLine { list.add(it) } assertEquals(arrayListOf("Hello", "World"), list)