All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kotlin.io.files.FileTreeWalk.kt Maven / Gradle / Ivy

@file:JvmVersion
@file:JvmMultifileClass
@file:JvmName("FilesKt")
package kotlin.io

import java.io.File
import java.io.IOException
import java.util.Stack

/**
 * 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 */
    BOTTOM_UP
    // Do we want also breadth-first search?
}

/**
 * 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.
 *
 * @param start directory to walk into.
 * @param direction selects top-down or bottom-up order (in other words, parents first or children first).
 * @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 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 maxDepth: Int = Int.MAX_VALUE
) : Sequence {

    internal constructor(start: File, direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): this(start, direction, null, null, null)


    /** 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 [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?
    }

    /** Abstract class that encapsulates directory visiting in some order, beginning from a given [rootDir] */
    private abstract class DirectoryState(rootDir: File): WalkState(rootDir) {
        init {
            if (_Assertions.ENABLED)
                assert(rootDir.isDirectory) { "rootDir must be verified to be directory beforehand." }
        }
    }

    private inner class FileTreeWalkIterator : AbstractIterator() {

        // Stack of directory states, beginning from the start directory
        private val state = Stack()

        init {
            if (start.isDirectory) {
                state.push(directoryState(start))
            } else if (start.isFile) {
                state.push(SingleFileState(start))
            } else {
                done()
            }

        }

        override fun computeNext() {
            val nextFile = gotoNext()
            if (nextFile != null)
                setNext(nextFile)
            else
                done()
        }


        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 (file == topState.root || !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? = null

            private var fileIndex = 0

            private var failed = false

            /** First all children, then root directory */
            override public fun step(): File? {
                if (!failed && fileList == null) {
                    if (onEnter?.invoke(root) == false) {
                        return null
                    }

                    fileList = root.listFiles()
                    if (fileList == null) {
                        onFail?.invoke(root, AccessDeniedException(file = root, 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 root
                } else {
                    // That's all
                    onLeave?.invoke(root)
                    return null
                }
            }
        }

        /** Visiting in top-down order */
        private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {

            private var rootVisited = false

            private var fileList: Array? = null

            private var fileIndex = 0

            /** First root directory, then all children */
            override public fun step(): File? {
                if (!rootVisited) {
                    // First visit root
                    if (onEnter?.invoke(root) == false) {
                        return null
                    }

                    rootVisited = true
                    return root
                } else if (fileList == null || fileIndex < fileList!!.size) {
                    if (fileList == null) {
                        // Then read an array of files, if any
                        fileList = root.listFiles()
                        if (fileList == null) {
                            onFail?.invoke(root, AccessDeniedException(file = root, reason = "Cannot list files in a directory"))
                        }
                        if (fileList == null || fileList!!.size == 0) {
                            onLeave?.invoke(root)
                            return null
                        }
                    }
                    // Then visit all files
                    return fileList!![fileIndex++]
                } else {
                    // That's all
                    onLeave?.invoke(root)
                    return null
                }
            }
        }

        private inner class SingleFileState(rootFile: File) : WalkState(rootFile) {
            private var visited: Boolean = false

            init {
                if (_Assertions.ENABLED)
                    assert(rootFile.isFile) { "rootFile must be verified to be file beforehand." }
            }

            override fun step(): File? {
                if (visited) return null
                visited = true
                return root
            }
        }

    }

    /**
     * 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 onEnter(function: (File) -> Boolean): FileTreeWalk {
        return FileTreeWalk(start, direction, onEnter = function, onLeave = onLeave, onFail = onFail, maxDepth = maxDepth)
    }

    /**
     * Sets leave directory [function].
     * Leave [function] is called AFTER the corresponding directory and its files are visited.
     */
    public fun onLeave(function: (File) -> Unit): FileTreeWalk {
        return FileTreeWalk(start, direction, onEnter, function, onFail, maxDepth)
    }

    /**
     * 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 onFail(function: (File, IOException) -> Unit): FileTreeWalk {
        return FileTreeWalk(start, direction, onEnter, onLeave, function, maxDepth)
    }

    /**
     * Sets maximum [depth] of walk. Int.MAX_VALUE is used for unlimited.
     * Negative and zero values are not allowed.
     */
    public fun maxDepth(depth: Int): FileTreeWalk {
        if (depth <= 0)
            throw IllegalArgumentException("Use positive depth value")
        return FileTreeWalk(start, direction, onEnter, onLeave, onFail, depth)
    }
}

/**
 * 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 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 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)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy