Rename FileTreeWalk public parameters and builder methods.
Directory filtering now is performed in newly introduced onEnter predicate.
This commit is contained in:
@@ -29,27 +29,54 @@ public enum class FileWalkDirection {
|
|||||||
*
|
*
|
||||||
* @param start directory to walk into.
|
* @param start directory to walk into.
|
||||||
* @param direction selects top-down or bottom-up order (in other words, parents first or children first).
|
* @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 onEnter 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.
|
* if `false` is returned, directory is not visited entirely.
|
||||||
* @param fail is called on a directory when it's impossible to get its file list.
|
* @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 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,
|
* @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.
|
* 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 constructor(
|
||||||
private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
|
private val start: File,
|
||||||
private val enter: (File) -> Unit = {},
|
private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
|
||||||
private val leave: (File) -> Unit = {},
|
private val onEnter: ((File) -> Boolean)?,
|
||||||
private val fail: (f: File, e: IOException) -> Unit = { f, e -> Unit },
|
private val onLeave: ((File) -> Unit)?,
|
||||||
private val filter: (File) -> Boolean = { true },
|
private val onFail: ((f: File, e: IOException) -> Unit)?,
|
||||||
private val maxDepth: Int = Int.MAX_VALUE
|
private val filter: (File) -> Boolean = { true },
|
||||||
|
private val maxDepth: Int = Int.MAX_VALUE,
|
||||||
|
dummy: Boolean = false
|
||||||
) : Sequence<File> {
|
) : Sequence<File> {
|
||||||
|
|
||||||
|
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. */
|
/** Returns an iterator walking through files. */
|
||||||
override public fun iterator(): Iterator<File> = FileTreeWalkIterator()
|
override public fun iterator(): Iterator<File> = FileTreeWalkIterator()
|
||||||
|
|
||||||
/** Abstract class that encapsulates file visiting in some order, beginning from a given [rootDir] */
|
/** Abstract class that encapsulates file visiting in some order, beginning from a given [root] */
|
||||||
private abstract class WalkState(val rootDir: File) {
|
private abstract class WalkState(val root: File) {
|
||||||
/** Call of this function proceeds to a next file for visiting and returns it */
|
/** Call of this function proceeds to a next file for visiting and returns it */
|
||||||
abstract public fun step(): File?
|
abstract public fun step(): File?
|
||||||
}
|
}
|
||||||
@@ -112,7 +139,7 @@ public class FileTreeWalk(private val start: File,
|
|||||||
// Check that file/directory matches the filter
|
// Check that file/directory matches the filter
|
||||||
if (!filter(file))
|
if (!filter(file))
|
||||||
return gotoNext()
|
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
|
// Proceed to a root directory or a simple file
|
||||||
return file
|
return file
|
||||||
} else {
|
} else {
|
||||||
@@ -137,10 +164,13 @@ public class FileTreeWalk(private val start: File,
|
|||||||
/** First all children, then root directory */
|
/** 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)
|
if (onEnter?.invoke(root) == false) {
|
||||||
fileList = rootDir.listFiles()
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList = root.listFiles()
|
||||||
if (fileList == null) {
|
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
|
failed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,10 +180,10 @@ public class FileTreeWalk(private val start: File,
|
|||||||
} else if (!rootVisited) {
|
} else if (!rootVisited) {
|
||||||
// Then visit root
|
// Then visit root
|
||||||
rootVisited = true
|
rootVisited = true
|
||||||
return rootDir
|
return root
|
||||||
} else {
|
} else {
|
||||||
// That's all
|
// That's all
|
||||||
leave(rootDir)
|
onLeave?.invoke(root)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,18 +202,21 @@ public class FileTreeWalk(private val start: File,
|
|||||||
override public fun step(): File? {
|
override public fun step(): File? {
|
||||||
if (!rootVisited) {
|
if (!rootVisited) {
|
||||||
// First visit root
|
// First visit root
|
||||||
enter(rootDir)
|
if (onEnter?.invoke(root) == false) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
rootVisited = true
|
rootVisited = true
|
||||||
return rootDir
|
return root
|
||||||
} else if (fileList == null || fileIndex < fileList!!.size) {
|
} else if (fileList == null || fileIndex < fileList!!.size) {
|
||||||
if (fileList == null) {
|
if (fileList == null) {
|
||||||
// Then read an array of files, if any
|
// Then read an array of files, if any
|
||||||
fileList = rootDir.listFiles()
|
fileList = root.listFiles()
|
||||||
if (fileList == null) {
|
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) {
|
if (fileList == null || fileList!!.size == 0) {
|
||||||
leave(rootDir)
|
onLeave?.invoke(root)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,56 +224,66 @@ public class FileTreeWalk(private val start: File,
|
|||||||
return fileList!![fileIndex++]
|
return fileList!![fileIndex++]
|
||||||
} else {
|
} else {
|
||||||
// That's all
|
// That's all
|
||||||
leave(rootDir)
|
onLeave?.invoke(root)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class SingleFileState(root: File) : WalkState(root) {
|
private inner class SingleFileState(rootFile: File) : WalkState(rootFile) {
|
||||||
private var visited: Boolean = false
|
private var visited: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (ASSERTIONS_ENABLED)
|
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? {
|
override fun step(): File? {
|
||||||
if (visited) return null
|
if (visited) return null
|
||||||
visited = true
|
visited = true
|
||||||
if (!filter(rootDir)) return null
|
if (!filter(root)) return null
|
||||||
return rootDir
|
return root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets enter directory [function].
|
* Sets enter directory predicate [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.
|
||||||
|
* 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 {
|
public fun onEnter(function: (File) -> Boolean): FileTreeWalk {
|
||||||
return FileTreeWalk(start, direction, function, leave, fail, filter, maxDepth)
|
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].
|
* 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(function: (File) -> Unit): FileTreeWalk {
|
public fun onLeave(function: (File) -> Unit): FileTreeWalk {
|
||||||
return FileTreeWalk(start, direction, enter, function, fail, filter, maxDepth)
|
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].
|
* 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(function: (File, IOException) -> Unit): FileTreeWalk {
|
public fun onFail(function: (File, IOException) -> Unit): FileTreeWalk {
|
||||||
return FileTreeWalk(start, direction, enter, leave, function, filter, maxDepth)
|
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].
|
* 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 `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.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Filter out directories entirely with onEnter, and all items with filter().")
|
||||||
public fun treeFilter(predicate: (File) -> Boolean): FileTreeWalk {
|
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 {
|
public fun maxDepth(depth: Int): FileTreeWalk {
|
||||||
if (depth <= 0)
|
if (depth <= 0)
|
||||||
throw IllegalArgumentException("Use positive depth value")
|
throw IllegalArgumentException("Use positive depth value")
|
||||||
return FileTreeWalk(start, direction, enter, leave, fail, filter, depth)
|
return FileTreeWalk(start, direction, onEnter, onLeave, onFail, filter, depth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ import kotlin.test.*
|
|||||||
class FileTreeWalkTest {
|
class FileTreeWalkTest {
|
||||||
|
|
||||||
companion object {
|
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 {
|
fun createTestFiles(): File {
|
||||||
val basedir = createTempDir()
|
val basedir = createTempDir()
|
||||||
File(basedir, "1").mkdir()
|
for (name in referenceFilenames) {
|
||||||
File(basedir, "1/2".separatorsToSystem()).mkdir()
|
val file = basedir.resolve(name)
|
||||||
File(basedir, "1/3".separatorsToSystem()).mkdir()
|
if (file.extension.isEmpty())
|
||||||
File(basedir, "1/3/4.txt".separatorsToSystem()).createNewFile()
|
file.mkdir()
|
||||||
File(basedir, "1/3/5.txt".separatorsToSystem()).createNewFile()
|
else
|
||||||
File(basedir, "6").mkdir()
|
file.createNewFile()
|
||||||
File(basedir, "7.txt").createNewFile()
|
}
|
||||||
File(basedir, "8").mkdir()
|
|
||||||
File(basedir, "8/9.txt".separatorsToSystem()).createNewFile()
|
|
||||||
return basedir
|
return basedir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,9 +28,7 @@ class FileTreeWalkTest {
|
|||||||
@Test fun withSimple() {
|
@Test fun withSimple() {
|
||||||
val basedir = createTestFiles()
|
val basedir = createTestFiles()
|
||||||
try {
|
try {
|
||||||
val referenceNames =
|
val referenceNames = setOf("") + referenceFilenames
|
||||||
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 namesTopDown = HashSet<String>()
|
val namesTopDown = HashSet<String>()
|
||||||
for (file in basedir.walkTopDown()) {
|
for (file in basedir.walkTopDown()) {
|
||||||
val name = file.relativeTo(basedir)
|
val name = file.relativeTo(basedir)
|
||||||
@@ -70,20 +69,24 @@ class FileTreeWalkTest {
|
|||||||
val basedir = createTestFiles()
|
val basedir = createTestFiles()
|
||||||
try {
|
try {
|
||||||
val referenceNames =
|
val referenceNames =
|
||||||
listOf("", "1", "1/2", "1/3", "6", "8").map(
|
listOf("", "1", "1/2", "6", "8").map(
|
||||||
{ it -> it.separatorsToSystem() }).toHashSet()
|
{ it -> it.separatorsToSystem() }).toHashSet()
|
||||||
val namesTopDownEnter = HashSet<String>()
|
val namesTopDownEnter = HashSet<String>()
|
||||||
val namesTopDownLeave = HashSet<String>()
|
val namesTopDownLeave = HashSet<String>()
|
||||||
val namesTopDown = HashSet<String>()
|
val namesTopDown = HashSet<String>()
|
||||||
fun enter(file: File) {
|
fun enter(file: File): Boolean {
|
||||||
val name = file.relativeTo(basedir)
|
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")
|
assertFalse(namesTopDownEnter.contains(name), "$name is entered twice")
|
||||||
namesTopDownEnter.add(name)
|
|
||||||
assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance")
|
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) {
|
fun leave(file: File) {
|
||||||
val name = file.relativeTo(basedir)
|
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")
|
assertFalse(namesTopDownLeave.contains(name), "$name is left twice")
|
||||||
namesTopDownLeave.add(name)
|
namesTopDownLeave.add(name)
|
||||||
assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance")
|
assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance")
|
||||||
@@ -91,14 +94,14 @@ class FileTreeWalkTest {
|
|||||||
|
|
||||||
fun visit(file: File) {
|
fun visit(file: File) {
|
||||||
val name = file.relativeTo(basedir)
|
val name = file.relativeTo(basedir)
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory) {
|
||||||
assertTrue(namesTopDownEnter.contains(name), "$name is visited before entrance")
|
assertTrue(namesTopDownEnter.contains(name), "$name is visited before entrance")
|
||||||
namesTopDown.add(name)
|
namesTopDown.add(name)
|
||||||
assertFalse(namesTopDownLeave.contains(name), "$name is visited after leaving")
|
assertFalse(namesTopDownLeave.contains(name), "$name is visited after leaving")
|
||||||
}
|
}
|
||||||
if (file == basedir)
|
if (file == basedir)
|
||||||
return
|
return
|
||||||
val parent = file.getParentFile()
|
val parent = file.parentFile
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
val parentName = parent.relativeTo(basedir)
|
val parentName = parent.relativeTo(basedir)
|
||||||
assertTrue(namesTopDownEnter.contains(parentName),
|
assertTrue(namesTopDownEnter.contains(parentName),
|
||||||
@@ -107,7 +110,7 @@ class FileTreeWalkTest {
|
|||||||
"$name is visited after leaving its parent $parentName")
|
"$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)
|
visit(file)
|
||||||
}
|
}
|
||||||
assertEquals(referenceNames, namesTopDownEnter)
|
assertEquals(referenceNames, namesTopDownEnter)
|
||||||
@@ -115,7 +118,7 @@ class FileTreeWalkTest {
|
|||||||
namesTopDownEnter.clear()
|
namesTopDownEnter.clear()
|
||||||
namesTopDownLeave.clear()
|
namesTopDownLeave.clear()
|
||||||
namesTopDown.clear()
|
namesTopDown.clear()
|
||||||
for (file in basedir.walkBottomUp().enter(::enter).leave(::leave)) {
|
for (file in basedir.walkBottomUp().onEnter(::enter).onLeave(::leave)) {
|
||||||
visit(file)
|
visit(file)
|
||||||
}
|
}
|
||||||
assertEquals(referenceNames, namesTopDownEnter)
|
assertEquals(referenceNames, namesTopDownEnter)
|
||||||
|
|||||||
Reference in New Issue
Block a user