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:
Ilya Gorbunov
2015-12-04 20:48:06 +03:00
parent 061803d7b1
commit e24dbcefb6
2 changed files with 161 additions and 140 deletions
@@ -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
}
} }
/** /**
+17
View File
@@ -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 {