From 8deefd56dbe2ca6f992a68d3ed5830850d9f3a5a Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Fri, 4 Dec 2015 21:23:22 +0300 Subject: [PATCH] Rename FileTreeWalk public parameters and builder methods. Directory filtering now is performed in newly introduced onEnter predicate. --- .../src/kotlin/io/files/FileTreeWalk.kt | 118 ++++++++++++------ libraries/stdlib/test/io/FileTreeWalks.kt | 41 +++--- 2 files changed, 103 insertions(+), 56 deletions(-) diff --git a/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt b/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt index a80cadec5a3..dc4cdfcfc45 100644 --- a/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt +++ b/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt @@ -29,27 +29,54 @@ public enum class FileWalkDirection { * * @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 onEnter is called on any entered directory before its files are visited and before it is visited itself, + * if `false` is returned, directory is not visited entirely. + * @param onLeave is called on any left directory after its files are visited and after it is visited itself. + * @param onFail 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, - 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 +public class FileTreeWalk private constructor( + private val start: File, + private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN, + private val onEnter: ((File) -> Boolean)?, + private val onLeave: ((File) -> Unit)?, + private val onFail: ((f: File, e: IOException) -> Unit)?, + private val filter: (File) -> Boolean = { true }, + private val maxDepth: Int = Int.MAX_VALUE, + dummy: Boolean = false ) : Sequence { + internal constructor(start: File, direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): this(start, direction, null, null, null, dummy = false) + +/* + private constructor( + start: File, + direction: FileWalkDirection = FileWalkDirection.TOP_DOWN, + onEnter: ((File) -> Boolean)? = null, + onLeave: ((File) -> Unit)? = null, + onFail: ((f: File, e: IOException) -> Unit)? = null, + maxDepth: Int = Int.MAX_VALUE + ) : this(start, direction, onEnter, onLeave, onFail, maxDepth = maxDepth, dummy = false) +*/ + + @Deprecated("Use builder methods on an instance obtained from File.walk/walkTopDown/walkBottomUp instead of directly calling constructor.") + public constructor( + start: File, + direction: FileWalkDirection = FileWalkDirection.TOP_DOWN, + enter: (File) -> Unit = {}, + leave: (File) -> Unit = {}, + fail: (f: File, e: IOException) -> Unit = { f, e -> Unit }, + filter: (File) -> Boolean = { true }, + maxDepth: Int = Int.MAX_VALUE + ) : this(start, direction, onEnter = { enter(it); true }, onLeave = leave, onFail = fail, filter = filter, maxDepth = maxDepth) + /** Returns an iterator walking through files. */ override public fun iterator(): Iterator = FileTreeWalkIterator() - /** Abstract class that encapsulates file visiting in some order, beginning from a given [rootDir] */ - private abstract class WalkState(val rootDir: File) { + /** Abstract class that encapsulates file visiting in some order, beginning from a given [root] */ + private abstract class WalkState(val root: File) { /** Call of this function proceeds to a next file for visiting and returns it */ abstract public fun step(): File? } @@ -112,7 +139,7 @@ public class FileTreeWalk(private val start: File, // Check that file/directory matches the filter if (!filter(file)) return gotoNext() - if (file == topState.rootDir || !file.isDirectory || state.size >= maxDepth) { + if (file == topState.root || !file.isDirectory || state.size >= maxDepth) { // Proceed to a root directory or a simple file return file } else { @@ -137,10 +164,13 @@ public class FileTreeWalk(private val start: File, /** First all children, then root directory */ override public fun step(): File? { if (!failed && fileList == null) { - enter(rootDir) - fileList = rootDir.listFiles() + if (onEnter?.invoke(root) == false) { + return null + } + + fileList = root.listFiles() if (fileList == null) { - fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory")) + onFail?.invoke(root, AccessDeniedException(file = root, reason = "Cannot list files in a directory")) failed = true } } @@ -150,10 +180,10 @@ public class FileTreeWalk(private val start: File, } else if (!rootVisited) { // Then visit root rootVisited = true - return rootDir + return root } else { // That's all - leave(rootDir) + onLeave?.invoke(root) return null } } @@ -172,18 +202,21 @@ public class FileTreeWalk(private val start: File, override public fun step(): File? { if (!rootVisited) { // First visit root - enter(rootDir) + if (onEnter?.invoke(root) == false) { + return null + } + rootVisited = true - return rootDir + return root } else if (fileList == null || fileIndex < fileList!!.size) { if (fileList == null) { // Then read an array of files, if any - fileList = rootDir.listFiles() + fileList = root.listFiles() if (fileList == null) { - fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory")) + onFail?.invoke(root, AccessDeniedException(file = root, reason = "Cannot list files in a directory")) } if (fileList == null || fileList!!.size == 0) { - leave(rootDir) + onLeave?.invoke(root) return null } } @@ -191,56 +224,66 @@ public class FileTreeWalk(private val start: File, return fileList!![fileIndex++] } else { // That's all - leave(rootDir) + onLeave?.invoke(root) return null } } } - private inner class SingleFileState(root: File) : WalkState(root) { + private inner class SingleFileState(rootFile: File) : WalkState(rootFile) { private var visited: Boolean = false init { @Suppress("DEPRECATION") if (ASSERTIONS_ENABLED) - assert(root.isFile) { "root must be verified to be file beforehand." } + assert(rootFile.isFile) { "rootFile must be verified to be file beforehand." } } override fun step(): File? { if (visited) return null visited = true - if (!filter(rootDir)) return null - return rootDir + if (!filter(root)) return null + return root } } } /** - * Sets enter directory [function]. + * Sets enter directory predicate [function]. * Enter [function] is called BEFORE the corresponding directory and its files are visited. + * If the [function] returns `false` the directory is not entered, and neither it nor its files are not visited. */ - public fun enter(function: (File) -> Unit): FileTreeWalk { - return FileTreeWalk(start, direction, function, leave, fail, filter, maxDepth) + public fun onEnter(function: (File) -> Boolean): FileTreeWalk { + return FileTreeWalk(start, direction, onEnter = function, onLeave = onLeave, onFail = onFail, filter = filter, maxDepth = maxDepth) } + @Deprecated("Use onEnter instead.") + public fun enter(function: (File) -> Unit): FileTreeWalk = onEnter { function(it); true } + /** * Sets leave directory [function]. * Leave [function] is called AFTER the corresponding directory and its files are visited. */ - public fun leave(function: (File) -> Unit): FileTreeWalk { - return FileTreeWalk(start, direction, enter, function, fail, filter, maxDepth) + public fun onLeave(function: (File) -> Unit): FileTreeWalk { + return FileTreeWalk(start, direction, onEnter, function, onFail, filter, maxDepth, false) } + @Deprecated("Use onLeave instead.", ReplaceWith("onLeave(function)")) + public fun leave(function: (File) -> Unit): FileTreeWalk = onLeave(function) + /** * Set fail entering directory [function]. * Fail [function] is called when walker is unable to get list of directory files. * Enter and leave functions are called even in this case. */ - public fun fail(function: (File, IOException) -> Unit): FileTreeWalk { - return FileTreeWalk(start, direction, enter, leave, function, filter, maxDepth) + public fun onFail(function: (File, IOException) -> Unit): FileTreeWalk { + return FileTreeWalk(start, direction, onEnter, onLeave, function, filter, maxDepth) } + @Deprecated("Use onFail instead.", ReplaceWith("onFail(function)")) + public fun fail(function: (File, IOException) -> Unit): FileTreeWalk = onFail(function) + /** * Sets tree filter [predicate]. @@ -248,8 +291,9 @@ public class FileTreeWalk(private val start: File, * 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. */ + @Deprecated("Filter out directories entirely with onEnter, and all items with filter().") public fun treeFilter(predicate: (File) -> Boolean): FileTreeWalk { - return FileTreeWalk(start, direction, enter, leave, fail, predicate, maxDepth) + return FileTreeWalk(start, direction, onEnter, onLeave, onFail, predicate, maxDepth) } /** @@ -259,7 +303,7 @@ public class FileTreeWalk(private val start: File, public fun maxDepth(depth: Int): FileTreeWalk { if (depth <= 0) throw IllegalArgumentException("Use positive depth value") - return FileTreeWalk(start, direction, enter, leave, fail, filter, depth) + return FileTreeWalk(start, direction, onEnter, onLeave, onFail, filter, depth) } } diff --git a/libraries/stdlib/test/io/FileTreeWalks.kt b/libraries/stdlib/test/io/FileTreeWalks.kt index 9ecf2ca608a..8f3ffa037cf 100644 --- a/libraries/stdlib/test/io/FileTreeWalks.kt +++ b/libraries/stdlib/test/io/FileTreeWalks.kt @@ -9,17 +9,18 @@ import kotlin.test.* class FileTreeWalkTest { companion object { + val referenceFilenames = + listOf("1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6", "7.txt", "8", "8/9.txt") + .map { it.separatorsToSystem() } fun createTestFiles(): File { val basedir = createTempDir() - File(basedir, "1").mkdir() - File(basedir, "1/2".separatorsToSystem()).mkdir() - File(basedir, "1/3".separatorsToSystem()).mkdir() - File(basedir, "1/3/4.txt".separatorsToSystem()).createNewFile() - File(basedir, "1/3/5.txt".separatorsToSystem()).createNewFile() - File(basedir, "6").mkdir() - File(basedir, "7.txt").createNewFile() - File(basedir, "8").mkdir() - File(basedir, "8/9.txt".separatorsToSystem()).createNewFile() + for (name in referenceFilenames) { + val file = basedir.resolve(name) + if (file.extension.isEmpty()) + file.mkdir() + else + file.createNewFile() + } return basedir } } @@ -27,9 +28,7 @@ class FileTreeWalkTest { @Test fun withSimple() { val basedir = createTestFiles() 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() + val referenceNames = setOf("") + referenceFilenames val namesTopDown = HashSet() for (file in basedir.walkTopDown()) { val name = file.relativeTo(basedir) @@ -70,20 +69,24 @@ class FileTreeWalkTest { val basedir = createTestFiles() try { val referenceNames = - listOf("", "1", "1/2", "1/3", "6", "8").map( + listOf("", "1", "1/2", "6", "8").map( { it -> it.separatorsToSystem() }).toHashSet() val namesTopDownEnter = HashSet() val namesTopDownLeave = HashSet() val namesTopDown = HashSet() - fun enter(file: File) { + fun enter(file: File): Boolean { val name = file.relativeTo(basedir) + assertTrue(file.isDirectory, "$name is not directory, only directories should be entered") assertFalse(namesTopDownEnter.contains(name), "$name is entered twice") - namesTopDownEnter.add(name) assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance") + if (file.name == "3") return false // filter out 3 + namesTopDownEnter.add(name) + return true } fun leave(file: File) { val name = file.relativeTo(basedir) + assertTrue(file.isDirectory, "$name is not directory, only directories should be left") assertFalse(namesTopDownLeave.contains(name), "$name is left twice") namesTopDownLeave.add(name) assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance") @@ -91,14 +94,14 @@ class FileTreeWalkTest { fun visit(file: File) { val name = file.relativeTo(basedir) - if (file.isDirectory()) { + if (file.isDirectory) { assertTrue(namesTopDownEnter.contains(name), "$name is visited before entrance") namesTopDown.add(name) assertFalse(namesTopDownLeave.contains(name), "$name is visited after leaving") } if (file == basedir) return - val parent = file.getParentFile() + val parent = file.parentFile if (parent != null) { val parentName = parent.relativeTo(basedir) assertTrue(namesTopDownEnter.contains(parentName), @@ -107,7 +110,7 @@ class FileTreeWalkTest { "$name is visited after leaving its parent $parentName") } } - for (file in basedir.walkTopDown().enter(::enter).leave(::leave)) { + for (file in basedir.walkTopDown().onEnter(::enter).onLeave(::leave)) { visit(file) } assertEquals(referenceNames, namesTopDownEnter) @@ -115,7 +118,7 @@ class FileTreeWalkTest { namesTopDownEnter.clear() namesTopDownLeave.clear() namesTopDown.clear() - for (file in basedir.walkBottomUp().enter(::enter).leave(::leave)) { + for (file in basedir.walkBottomUp().onEnter(::enter).onLeave(::leave)) { visit(file) } assertEquals(referenceNames, namesTopDownEnter)