Move all mutable state from FileTreeWalk to its iterator implementation, allowing this sequence to be iterated several times.
Introduce SingleFileState for single file walks. Do not check isDirectory in DirectoryState constructor unless assertions are enabled.
This commit is contained in:
@@ -5,8 +5,8 @@ package kotlin.io
|
|||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.NoSuchElementException
|
|
||||||
import java.util.Stack
|
import java.util.Stack
|
||||||
|
import kotlin.support.AbstractIterator
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An enumeration to describe possible walk directions.
|
* An enumeration to describe possible walk directions.
|
||||||
@@ -45,145 +45,175 @@ public class FileTreeWalk(private val start: File,
|
|||||||
private val maxDepth: Int = Int.MAX_VALUE
|
private val maxDepth: Int = Int.MAX_VALUE
|
||||||
) : Sequence<File> {
|
) : Sequence<File> {
|
||||||
|
|
||||||
/** Abstract class that encapsulates file visiting in some order, beginning from a given [rootDir] */
|
/** Returns an iterator walking through files. */
|
||||||
private abstract class DirectoryState(public val rootDir: File) {
|
override public fun iterator(): Iterator<File> = FileTreeWalkIterator()
|
||||||
init {
|
|
||||||
if (!rootDir.isDirectory())
|
|
||||||
throw IllegalArgumentException("Directory is needed")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Abstract class that encapsulates file visiting in some order, beginning from a given [rootDir] */
|
||||||
|
private abstract class WalkState(val rootDir: 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?
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Visiting in bottom-up order */
|
/** Abstract class that encapsulates directory visiting in some order, beginning from a given [rootDir] */
|
||||||
private inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) {
|
private abstract class DirectoryState(rootDir: File): WalkState(rootDir) {
|
||||||
|
init {
|
||||||
private var rootVisited = false
|
@Suppress("DEPRECATION")
|
||||||
|
if (ASSERTIONS_ENABLED)
|
||||||
private var fileList: Array<File>? = null
|
assert(rootDir.isDirectory) { "rootDir must be verified to be directory beforehand." }
|
||||||
|
|
||||||
private var fileIndex = 0
|
|
||||||
|
|
||||||
private var failed = false
|
|
||||||
|
|
||||||
/** First all children, then root directory */
|
|
||||||
override public fun step(): File? {
|
|
||||||
if (!failed && fileList == null) {
|
|
||||||
enter(rootDir)
|
|
||||||
fileList = rootDir.listFiles()
|
|
||||||
if (fileList == null) {
|
|
||||||
fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory"))
|
|
||||||
failed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fileList != null && fileIndex < fileList!!.size) {
|
|
||||||
// First visit all files
|
|
||||||
return fileList!![fileIndex++]
|
|
||||||
} else if (!rootVisited) {
|
|
||||||
// Then visit root
|
|
||||||
rootVisited = true
|
|
||||||
return rootDir
|
|
||||||
} else {
|
|
||||||
// That's all
|
|
||||||
leave(rootDir)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Visiting in top-down order */
|
private inner class FileTreeWalkIterator : AbstractIterator<File>() {
|
||||||
private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {
|
|
||||||
|
|
||||||
private var rootVisited = false
|
// Stack of directory states, beginning from the start directory
|
||||||
|
private val state = Stack<WalkState>()
|
||||||
|
|
||||||
private var fileList: Array<File>? = null
|
init {
|
||||||
|
if (start.isDirectory && filter(start)) {
|
||||||
|
state.push(directoryState(start))
|
||||||
|
} else if (start.isFile) {
|
||||||
|
state.push(SingleFileState(start))
|
||||||
|
} else {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
private var fileIndex = 0
|
}
|
||||||
|
|
||||||
/** First root directory, then all children */
|
override fun computeNext() {
|
||||||
override public fun step(): File? {
|
val nextFile = gotoNext()
|
||||||
if (!rootVisited) {
|
if (nextFile != null)
|
||||||
// First visit root
|
setNext(nextFile)
|
||||||
enter(rootDir)
|
else
|
||||||
rootVisited = true
|
done()
|
||||||
return rootDir
|
}
|
||||||
} else if (fileList == null || fileIndex < fileList!!.size) {
|
|
||||||
if (fileList == null) {
|
|
||||||
// Then read an array of files, if any
|
private fun directoryState(root: File): DirectoryState {
|
||||||
|
return when (direction) {
|
||||||
|
FileWalkDirection.TOP_DOWN -> TopDownDirectoryState(root)
|
||||||
|
FileWalkDirection.BOTTOM_UP -> BottomUpDirectoryState(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tailrec private fun gotoNext(): File? {
|
||||||
|
|
||||||
|
if (state.empty()) {
|
||||||
|
// There is nothing in the state
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Take next file from the top of the stack
|
||||||
|
val topState = state.peek()!!
|
||||||
|
val file = topState.step()
|
||||||
|
if (file == null) {
|
||||||
|
// There is nothing more on the top of the stack, go back
|
||||||
|
state.pop()
|
||||||
|
return gotoNext()
|
||||||
|
} else {
|
||||||
|
// Check that file/directory matches the filter
|
||||||
|
if (!filter(file))
|
||||||
|
return gotoNext()
|
||||||
|
if (file == topState.rootDir || !file.isDirectory || state.size >= maxDepth) {
|
||||||
|
// Proceed to a root directory or a simple file
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
// Proceed to a sub-directory
|
||||||
|
state.push(directoryState(file))
|
||||||
|
return gotoNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Visiting in bottom-up order */
|
||||||
|
private inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) {
|
||||||
|
|
||||||
|
private var rootVisited = false
|
||||||
|
|
||||||
|
private var fileList: Array<File>? = null
|
||||||
|
|
||||||
|
private var fileIndex = 0
|
||||||
|
|
||||||
|
private var failed = false
|
||||||
|
|
||||||
|
/** First all children, then root directory */
|
||||||
|
override public fun step(): File? {
|
||||||
|
if (!failed && fileList == null) {
|
||||||
|
enter(rootDir)
|
||||||
fileList = rootDir.listFiles()
|
fileList = rootDir.listFiles()
|
||||||
if (fileList == null) {
|
if (fileList == null) {
|
||||||
fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory"))
|
fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory"))
|
||||||
}
|
failed = true
|
||||||
if (fileList == null || fileList!!.size == 0) {
|
|
||||||
leave(rootDir)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Then visit all files
|
if (fileList != null && fileIndex < fileList!!.size) {
|
||||||
return fileList!![fileIndex++]
|
// First visit all files
|
||||||
} else {
|
return fileList!![fileIndex++]
|
||||||
// That's all
|
} else if (!rootVisited) {
|
||||||
leave(rootDir)
|
// Then visit root
|
||||||
return null
|
rootVisited = true
|
||||||
|
return rootDir
|
||||||
|
} else {
|
||||||
|
// That's all
|
||||||
|
leave(rootDir)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Stack of directory states, beginning from the start directory
|
/** Visiting in top-down order */
|
||||||
private val state = Stack<DirectoryState>()
|
private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {
|
||||||
|
|
||||||
// We are already at the end or not?
|
private var rootVisited = false
|
||||||
private var end = false
|
|
||||||
|
|
||||||
// A future result of next() call
|
private var fileList: Array<File>? = null
|
||||||
private var nextFile: File? = null
|
|
||||||
|
|
||||||
init {
|
private var fileIndex = 0
|
||||||
if (!start.exists()) {
|
|
||||||
end = true
|
|
||||||
} else if (start.isDirectory() && filter(start)) {
|
|
||||||
pushState(start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pushState(root: File) {
|
/** First root directory, then all children */
|
||||||
state.push(when (direction) {
|
override public fun step(): File? {
|
||||||
FileWalkDirection.TOP_DOWN -> TopDownDirectoryState(root)
|
if (!rootVisited) {
|
||||||
FileWalkDirection.BOTTOM_UP -> BottomUpDirectoryState(root)
|
// First visit root
|
||||||
})
|
enter(rootDir)
|
||||||
}
|
rootVisited = true
|
||||||
|
return rootDir
|
||||||
tailrec private fun gotoNext(): File? {
|
} else if (fileList == null || fileIndex < fileList!!.size) {
|
||||||
if (end) {
|
if (fileList == null) {
|
||||||
// We are already at the end
|
// Then read an array of files, if any
|
||||||
return null
|
fileList = rootDir.listFiles()
|
||||||
} else if (state.empty()) {
|
if (fileList == null) {
|
||||||
// There is nothing in the state
|
fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory"))
|
||||||
// We must visit "start" if it's a file and matches the filter
|
}
|
||||||
end = true
|
if (fileList == null || fileList!!.size == 0) {
|
||||||
return if (start.exists() && !start.isDirectory() && filter(start)) start else null
|
leave(rootDir)
|
||||||
}
|
return null
|
||||||
// Take next file from the top of the stack
|
}
|
||||||
val topState = state.peek()
|
}
|
||||||
val file = topState.step()
|
// Then visit all files
|
||||||
if (file == null) {
|
return fileList!![fileIndex++]
|
||||||
// There is nothing more on the top of the stack, go back
|
} else {
|
||||||
state.pop()
|
// That's all
|
||||||
return gotoNext()
|
leave(rootDir)
|
||||||
} else {
|
return null
|
||||||
// Check that file/directory matches the filter
|
}
|
||||||
if (!filter(file))
|
|
||||||
return gotoNext()
|
|
||||||
if (file == topState.rootDir || !file.isDirectory() || state.size >= maxDepth) {
|
|
||||||
// Proceed to a root directory or a simple file
|
|
||||||
return file
|
|
||||||
} else {
|
|
||||||
// Proceed to a sub-directory
|
|
||||||
pushState(file)
|
|
||||||
return gotoNext()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class SingleFileState(root: File) : WalkState(root) {
|
||||||
|
private var visited: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (ASSERTIONS_ENABLED)
|
||||||
|
assert(root.isFile) { "root must be verified to be file beforehand." }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun step(): File? {
|
||||||
|
if (visited) return null
|
||||||
|
visited = true
|
||||||
|
if (!filter(rootDir)) return null
|
||||||
|
return rootDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,32 +261,6 @@ public class FileTreeWalk(private val start: File,
|
|||||||
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, enter, leave, fail, filter, depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An iterator associated with this walker */
|
|
||||||
private val it = object : Iterator<File> {
|
|
||||||
override public fun hasNext(): Boolean {
|
|
||||||
if (nextFile == null)
|
|
||||||
nextFile = gotoNext()
|
|
||||||
return (nextFile != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override public fun next(): File {
|
|
||||||
if (nextFile == null)
|
|
||||||
nextFile = gotoNext()
|
|
||||||
val res = nextFile
|
|
||||||
if (res == null)
|
|
||||||
throw NoSuchElementException()
|
|
||||||
// With nextFile = gotoNext() here some enter() / leave() can be called BEFORE visiting res
|
|
||||||
nextFile = null
|
|
||||||
// Visit this directory or file
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an associated file iterator. */
|
|
||||||
override public fun iterator(): Iterator<File> {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,6 +49,23 @@ class FileTreeWalkTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun singleFile() {
|
||||||
|
val testFile = createTempFile()
|
||||||
|
val nonExistantFile = testFile.resolve("foo")
|
||||||
|
try {
|
||||||
|
for (walk in listOf(File::walkTopDown, File::walkBottomUp)) {
|
||||||
|
assertEquals(testFile, walk(testFile).single(), "${walk.name}")
|
||||||
|
assertTrue(walk(testFile).treeFilter { false }.none(), "${walk.name}")
|
||||||
|
assertEquals(testFile, testFile.walk().onEnter { false }.single(), "${walk.name} - enter should not be called for single file")
|
||||||
|
|
||||||
|
assertTrue(walk(nonExistantFile).none(), "${walk.name} - enter should not be called for single file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
testFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun withEnterLeave() {
|
@Test fun withEnterLeave() {
|
||||||
val basedir = createTestFiles()
|
val basedir = createTestFiles()
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user