Kotlin Watch Service

The java.nio.file package has a WatchService class that is used to watch for changes in a folder. This is a Kotlin program that demonstrates how to create a watch service that monitors a folder for changes and reports the changes.

package ch9.files

import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchService

private fun prompt(msg : String) : String {
    print("$msg => ")
    return readLine() ?: ""
}

private fun Path.watch() : WatchService {
    //Create a watch service
    val watchService = this.fileSystem.newWatchService()

    //Register the service, specifying which events to watch
    register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW, StandardWatchEventKinds.ENTRY_DELETE)

    //Return the watch service
    return watchService
}

fun main(args : Array<String>){
    val folder = prompt("Enter a folder to watch")
    val path = Paths.get(folder)

    val watcher = path.watch()
    println("Press ctrl+c to exit")

    while(true){
        //The watcher blocks until an event is available
        val key = watcher.take()

        //Now go through each event on the folder
        key.pollEvents().forEach { it ->
            //Print output according to the event
            when(it.kind().name()){
                "ENTRY_CREATE" -> println("${it.context()} was created")
                "ENTRY_MODIFY" -> println("${it.context()} was modified")
                "OVERFLOW" -> println("${it.context()} overflow")
                "ENTRY_DELETE" -> println("${it.context()} was deleted")
            }
        }
        //Call reset() on the key to watch for future events
        key.reset()
    }
}

Here is what it looked like when run on my machine.

Enter a folder to watch => /users/stonesoup/downloads
Press ctrl+c to exit
bob.json was created
bob.json was deleted

While the program was running, I created a bob.json file in my Downloads folder and then deleted it.

Explanation

The first task is to register the Watch Service. The example program has an Path.watch() extension function that encapsulates creating a watch service, registering it, and then returning it to the caller. The Watch Service is obtained from Path.fileSystem.newWatchService() method (line 15). The next step is to register the Watch Service using the Path.register() method (line 18). When registering the Watch Service, we can pass in number of StandWatchEventKinds to tell the Watch Service what to watch.

The main method collects a path from the user (line 25), creates a Path object from the input (line 26), and then registers the Watch Service (line 28). At this point, we enter into an infinite loop and watch the target folder for changes.

The first action in the loop is watcher.take() (line 33). The take() method blocks the thread until an event happens. When a monitored watch event takes place, the take() method will return a WatchKey(). The WatchKey() holds any number of Watch Events that have happened since the last watch cycle.

The example program calls WatchKey.pollEvents().forEach and goes through each watch event (line 36). It uses the WatchEvent.kind().name property (line 38-43) to print output according to each event. Notice how the program combines a when() function to react to each kind of watch event (lines 38-43). When we are done processing all events, we call reset() on the WatchKey() so that the program can wait for the next event. We can also end the WatchService by calling cancel() on the WatchKey.

References

https://docs.oracle.com/javase/8/docs/api/?java/io/File.html

Kotlin Glob

Glob is a pattern that is used to match files to a pattern. For example, suppose we wish to match all Kotlin files on our file system, we would use the syntax “glob:*.kt”. The following demo program walks through a user-supplied start path and matches all files according to the user-supplied glob pattern.

package ch9.files

import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.stream.Collectors.toList

private fun prompt(msg : String) : String {
    print("$msg => ")
    return readLine() ?: ""
}

fun main(args : Array<String>){
    val start = prompt("Enter a start path")
    val glob = prompt("Enter a glob pattern")

    //Object a matcher object from the supplied Glob pattern
    val matcher = FileSystems.getDefault().getPathMatcher(glob)

    val path = Paths.get(start)
    //Walk the file system
    Files.walk(path)
            //Filter out anything that doesn't match the glob
            .filter { it : Path? -> it?.let { matcher.matches(it.fileName) } ?: false }
            //Collect to a list
            .collect(toList())
            //Print to the console
            .forEach({ it -> println("Found ${it.fileName}") })
}

Here is an example run of the program.

Enter a start path => /users/stonesoup
Enter a glob pattern => glob:*.kt
Found CachingTutorialApplicationTests.kt
Found CachingTutorialApplication.kt
Found ExposedTransactionManagerTest.kt
Found SpringTransactionManager.kt
Found SamplesDao.kt
Found SamplesSQL.kt
...continued

Detailed Explanation

The program asks the user for a start path (line 15) and a glob syntax (line 16). The program supports the glob patterns in the table below.

Pattern Description
* Matches anything
** Matches anything even accross directories
? The ? mark matches any single character
[xyz] Matches any character inside of [ ]. In this example, it’s x, y, or z
[0-5], [a-z] Matches a range. In this case, it’s 0-5 or the letters a-z
{xyz, abc} Matches one of the two patterns. In this case, either xyz or abc

Once the user has supplied a valid path and glob pattern, the program calls Files.walk to walk through the file system. Using Java 8’s Streaming API, we filter all items that do not match the pattern (line 25) using the matcher object that was returned on line 19. The results are collected into a list and printed to the console.

References

https://docs.oracle.com/javase/8/docs/api/?java/io/File.html
https://docs.oracle.com/javase/8/docs/api/?java/io/File.html

Kotlin Files.copy

The Files class found in JDK provides a utility method that allows copying from an input stream into a file. The copy operation is found on lines 22 and 28. Here is an example program followed by a detailed explanation.

import java.io.Console
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

private fun Path.isFile() = !Files.isDirectory(this)

private fun Path.exists() = Files.exists(this)

private fun replace(): Boolean {
        val console = console()
        val replace = console.readLine("File already exists! Enter Y to replace => ")
        return replace.toLowerCase() == "y"
}

private fun Path.doCopy(dest : Path) : Boolean {
    return if(isFile()){
        if(dest.exists()){
            if(replace()){
                //We need to pass StandardCopyOption.REPLACE_EXISTING when overwriting a file
                Files.copy(this, dest, StandardCopyOption.REPLACE_EXISTING)
                true
            } else {
                false
            }
        } else {
            Files.copy(this, dest)
            true
        }
    } else {
        false
    }
}

private fun console() : Console {
    val console = System.console()
    return if(console != null){
        console
    } else {
        println("Please run from the terminal")
        System.exit(-1)

        //Return needed for compiler but we never actually reach this statement
        console!!
    }
}

fun main(args : Array<String>){
    when (args.size){
        2 -> {
            val src = Paths.get(args[0])
            val dest = Paths.get(args[1])

            if(src.doCopy(dest)){
                println("Copied ${src.fileName} to ${dest.fileName}")
            }
        }
        else -> {
            println("Usage: src dest")
        }
    }
}

Extension Functions

The example program uses Kotlin’s extension functions to help simplify the code. For example, it seems more natural to call exists() on a Path object as opposed to Files.exists(path). Likewise, it seems more natual to call copy on a Path object rather than Files.copy(src, dest). For this reason, we define a number of extension functions in the program.

The first extension function is found on line 7.

private fun Path.isFile() = !Files.isDirectory(this)

There isn’t a lot of magic here. All we are doing is wrapping a call to Files.isDirectory inside of the extension function. It allows us to call path.isFile() later on.

The next function is equally as brief.

private fun Path.exists() = Files.exists(this)

Once again, we are writing this function so that we can call path.exists() later on in the program.

The next function is replace().

private fun replace(): Boolean {
        val console = console()
        val replace = console.readLine("File already exists! Enter Y to replace => ")
        return replace.toLowerCase() == "y"
}

This function is used by the program to prompt the user if the file already exists. As we will see, we need to pass StandardCopyOption.REPLACE_EXISTING to overwrite a file or it will throw an exception. The function makes a call to a console() function that returns a non-null Console object or exits the program.

private fun console() : Console {
    val console = System.console()
    return if(console != null){
        console
    } else {
        println("Please run from the terminal")
        System.exit(-1)

        //Return needed for compiler but we never actually reach this statement
        console!!
    }
}

The final extension function is doCopy(). This is the function that actually contains the call to Files.copy(), which is the topic of this post.

private fun Path.doCopy(dest : Path) : Boolean {
    return if(isFile()){
        if(dest.exists()){
            if(replace()){
                //We need to pass StandardCopyOption.REPLACE_EXISTING when overwriting a file
                Files.copy(this, dest, StandardCopyOption.REPLACE_EXISTING)
                true
            } else {
                false
            }
        } else {
            Files.copy(this, dest)
            true
        }
    } else {
        false
    }
}

The doCopy function uses our isFile() and exist() extension functions on both the src and dest Path objects. When isFile(), exists() and replace() return true, we make a call to Files.copy, passing in this as our source, dest as our path, and StandardCopyOption.REPLACE_EXISTING. The function will make a copy of the source Path (this) into the dest and overwrite the dest should it already exist.

The alternative case is used when the dest file doesn’t already exist. Since the destination file doesn’t exist, there is no need to prompt the user about replacing it and we do not need to pass any StandardCopyOption to the call to Files.copy(). The program will simply copy one file to the other and the function will return true.

The main() function is the final function in the program.

fun main(args : Array<String>){
    when (args.size){
        2 -> {
            val src = Paths.get(args[0])
            val dest = Paths.get(args[1])

            if(src.doCopy(dest)){
                println("Copied ${src.fileName} to ${dest.fileName}")
            }
        }
        else -> {
            println("Usage: src dest")
        }
    }
}

The main() function gets mention because it uses the previously discussed doCopy() extension function. When the function is true, we tell the user that we copied the files. Otherwise the program exits.

References

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#copy-java.io.InputStream-java.nio.file.Path-java.nio.file.CopyOption…-
https://kotlinlang.org/docs/reference/extensions.html

Kotlin Files.readAttributes()

The Files.readAttributes() method comes from JDK and is used to return meta-data about a particular file.

import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.attribute.BasicFileAttributes

fun main(args : Array<String>){
    val path =
            if(args.isEmpty()){
                Paths.get(System.getProperty("user.dir"))
            } else {
                Paths.get(args[0])
            }

    if(Files.isDirectory(path)){

        Files.list(path).forEach({ it ->
            val attrs = Files.readAttributes(it, BasicFileAttributes::class.java)

            println("${it.fileName}")
            println("Size => ${attrs.size()}")
            println("Directory => ${attrs.isDirectory}")
            println("Regular File => ${attrs.isRegularFile}")
            println("Link => ${attrs.isSymbolicLink}")
            println("Last Accessed => ${attrs.lastAccessTime()}")
            println("Last Modified => ${attrs.lastModifiedTime()}")
            println()
        })
    } else {
        println("Enter a path to a directory")
    }
}

The readAttributes() call is made on line 16. The readAttributes() method takes a Path object and then a Java class object of BasicFileAttribures or one of its subinterfaces, DosFileAttributes or PosixFileAttributes. Depending on the Java class specified, the method will return either BasicFileAttributes, DosFileAttributes, or PosixFileAttributes. BasicFileAttributes is used in this example because it is portable across all systems, while the other two interfaces are specific to their respective platforms.

BasicFileAttributes provides many commonly used file attributes in a type safe fashion. We can access common attributes such as when the file was created, modified, or last accessed. The interface has boolean attributes to check if a Path is a directory or symbolic links. We can even check the size of the file with the BasicFileAttributes.

The DosFileAttributes interface has properties specific to DOS based platforms (i.e., Windows). We can check if a file is a system file, hidden file, read only, or archived. The PosixFileAttributes interface is used on unix based platforms such as Mac OS X or Linux. It contains properties file permissions, user groups, and the file owner.

References

https://docs.oracle.com/javase/7/docs/api/java/nio/file/attribute/BasicFileAttributes.html
https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#readAttributes(java.nio.file.Path,%20java.lang.Class,%20java.nio.file.LinkOption…)

Koltin Files.getAttribute()

The Files class in JDK has a getAttribute() that accepts a Path object and returns a specified attribute.

import java.nio.file.Files
import java.nio.file.Paths

fun main(args: Array<String>) {
    val path =
            if (args.isEmpty()) {
                Paths.get(System.getProperty("user.dir"))
            } else {
                Paths.get(args[0])
            }
    if(Files.isDirectory(path)){

        Files.list(path).forEach({ it ->
            //getAttribute() asserts non-null, so we can add compiler null checks by declaring a nullable type if desired
            val creationTime = Files.getAttribute(it, "creationTime")
            val lastModified = Files.getAttribute(it, "lastModifiedTime")
            val size = Files.getAttribute(it, "size")
            val dir = Files.getAttribute(it, "isDirectory")

            println("${it.fileName}")
            println("Creation Time => $creationTime")
            println("Last Modified => $lastModified")
            println("Size => $size")
            println("Directory => $dir")
            println()
        })
    } else {
        println("Please enter a path to a directory")
    }
}

The demonstration of Files.getAttribute() is found on lines 15 – 18. In each case, the getAttribute() method accepts a Path object, a string name of the attribute, and optional LinkOptions. The value returned is of type any, and the return type is assert not null (!!) operator. Since the names of the attributes is a string, there is the possibility that an exception could get thrown also.

The getAttribute() method can be convient, but it has short comings. Since it is a Java class, Kotlin interprets it as returning assert non-null. This may or may not be a problem. The java-doc makes no mention of a null return type, so it’s most likely safe to assume non-null types. However, it compiler checks are wanted or desirable, then we can declare nullable types.

More troubling is the fact that Strings are used for the attribute types. We have no protections against a typo such as “sizes” when we meant “size”. We will get a runtime exception in the event that attribute doesn’t exist. Alway check the java document prior to using the getAttribute() method to make sure the attribute is available first, keeping in mind that some attributes are platform specific.

Note that there is also a readAttributes() method, found in an upcoming post, that offers much better type safety than getAttribute(). However, the getAttribute() is useful in the case that we wish to check only on specific attribute (such as size), without having to use additional objects.

References

https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#getAttribute(java.nio.file.Path,%20java.lang.String,%20java.nio.file.LinkOption…)

Kotlin Files Attributes and List Files/Folders in Directory

JDK8 has a Files class that provides utility methods to list all files and folders in a directory and list attributes of a file.

import java.nio.file.Files
import java.nio.file.Paths
import java.util.stream.Collectors

fun main(args : Array<String>){
    //Create a Path object
    val path = Paths.get(if(args.isEmpty()){
        System.getProperty("user.dir")
    } else {
        args[0]
    })

    //Check if the Path is a directory
    if (Files.isDirectory(path)){
        //List all items in the directory. Note that we are using Java 8 streaming API to group the entries by
        //directory and files
        val fileDirMap = Files.list(path).collect(Collectors.partitioningBy( {it -> Files.isDirectory(it)}))

        println("Directories")
        //Print out all of the directories
        fileDirMap[true]?.forEach { it -> println(it.fileName) }

        println("\nFiles")
        println("%-20s\tRead\tWrite\tExecute".format("Name"))
        //Print out all files and attributes
        fileDirMap[false]?.forEach( {it ->
            println("%-20s\t%-5b\t%-5b\t%b".format(
                    it.fileName,
                    Files.isReadable(it), //Read attribute
                    Files.isWritable(it), //Write attribute
                    Files.isExecutable(it))) //Execute attribute
        })
    } else {
        println("Enter a directory")
    }
}

Output

Directories
target
.idea
src

Files
Name                	Read	Write	Execute
belchers.txt        	true 	true 	false
belchers.burgers    	true 	true 	false
pom.xml             	true 	true 	false
bob.ser             	true 	true 	false
OCJAP.iml           	true 	true 	false
bob.csv             	true 	true 	false
data.burgers        	true 	true 	false

Explanation

The program begins by testing for command line arguments. If a command line argument is available, the command line argument is used for the Path. Otherwise, we use the current working directory. The result is passed to Paths.get() and a Path object is returned.

Line 14 checks if the Path is a directory by using Files.isDirectory(). If true, the program proceeds to line 17 otherwise exits with an error message. Line 17 obtains a map of all directories and files in the path. We use the Files.list() method which returns a Java 8 Stream object. The next part transforms the Stream into a Map<Boolean, List> by using Collectors.partioningBy(). In our case, we are patitioning by true or false.

We pass the it variable (which is a Path object), to Files.isDirectory(). When the result is true, all Paths are grouped with the true key on the resulting map. All false results are grouped into a list and placed into the false key in the resulting map. When the operation is complete, we will have all files and directories sorted.

Line 21 prints out all directories in the folder. In our case, we are simply using forEach() to print out all of the directories. For demonstration purposes, we include additional information with each of the files. Lines 26-32 prints out the file name, read attribute, write attribute, and executable attribute.

The file name is obtained from the fileName property on the Path (line 28). Next we test if the file is readable by using the Files.isReadable() method (line 29. The Files class also has isWritable and isExecutable which work the same as isReadable (lines 30, 31 respectively). All methods return either true or false.

Methods Used

isDirectory(path : Path) : Boolean

Returns true when the Path object points at a directory, otherwise false.

val dir = Files.isDirectory(Paths.get(System.getProperty("user.dir")))

list(dir : Path) : Stream

Returns a Java 8 Stream object of type Path. All of the elements of the stream are lazily populated and contain the entries in a directory.

Files.list(Paths.get(System.getProperty("user.dir"))).forEach{it -> println(it.name) })

isReadable(path : Path) : Boolean

True when the file is readable, otherwise false.

val r = Files.isReadable(Paths.get(System.getProperty("user.dir")))

isWritable(path : Path) : Boolean

True when the file is writable

val w = Files.isWritable(Paths.get(System.getProperty("user.dir")))

isExecutable(path : Path) : Boolean

Tre when the file is an executable.

val e = Files.isExecutable(Paths.get(System.getProperty("user.dir")))

References

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html

Kotlin Files.exists() and Files.isSameFile()

The Files class provides utility methods that are useful for working with the file system.

import java.io.BufferedWriter
import java.io.FileWriter
import java.nio.file.Files
import java.nio.file.Paths


private val belchers = "belchers.txt"

private fun makeBelcherFile(){
    val names = listOf("Bob", "Linda", "Tina", "Gene", "Louise")
    BufferedWriter(FileWriter(ch9.paths.belchers)).use { writer ->
        names.forEach { name ->
            with(writer){
                write(name)
                newLine()
            }
        }
    }
}

fun main(args : Array<String>){
    //Check if the file exists first, and create it if needed
    if (!Files.exists(Paths.get(belchers))){
        makeBelcherFile()
    }
    val relativeBeclhers = Paths.get(belchers)
    val absoluteBelchers = Paths.get(System.getProperty("user.dir"), belchers)

    //Check if both Paths point to the same file
    println("Using Files.isSameFile() => " + (Files.isSameFile(relativeBeclhers, absoluteBelchers)))
}

Output

Using Files.isSameFile() => true

The program uses Files.exists to see if we have a belchers.txt file on the underlying os. If the method returns false, we call makeBelchersFile() on line 24 to create the file. Lines 26 and 27 create two different Path objects to point at the belchers.txt file.

The relativeBelchers is a Path object created using a relative path to the file. The absoluateBelchers object is created with an aboslute path by combining the current working directory with the name of the file. One line 38, we use the Files.isSameFile and pass both of the Path objects to it. Since both of these Paths point at the same file, it returns True.

Methods

exists(path : Path, varages options : LinkOptions) : Boolean

The exists method is used to test if a file exists or not. We can also pass optional LinkOptions that instructs the method on how to handle symbolic links in the file system. For example, if we don’t want to follow links, then pass NOFOLLOE_LNKS.

isSameFile(path : Path, path2 : Path)

Tests if the both path objects point to the same file. It’s the same as checking if path == path2.

References

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#isSameFile-java.nio.file.Path-java.nio.file.Path-

Kotlin Compare Paths

The Paths interface overrides equals() and compareTo(), allowing us to compare paths in the file system.

import java.nio.file.Paths

fun main(args : Array<String>){
    //Get two references to the home directory
    val home = Paths.get(System.getProperty("user.home"))
    val otherHome = Paths.get(System.getProperty("user.home"))
    
    //Get the current working directory
    val cwd = Paths.get(System.getProperty("user.dir"))

    println("$home == $otherHome is " + (home == otherHome))
    println("$home == $cwd is " + (home == cwd))
    println("$home < $cwd is " + (home < cwd))
    println("$cwd < $home is " + (cwd < home))
    println("$home >= $otherHome is " + (home >= otherHome))
}

Output

/Users/stonesoup == /Users/stonesoup is true
/Users/stonesoup == /Users/stonesoup/IdeaProjects/OCJAP is false
/Users/stonesoup < /Users/stonesoup/IdeaProjects/OCJAP is true
/Users/stonesoup/IdeaProjects/OCJAP < /Users/stonesoup is false
/Users/stonesoup >= /Users/stonesoup is true

compareTo(other : Path) : Int

The javadoc states that two paths are compared lexicographically as defined by the file system provider. The default provider uses a platform specific means of comparing file paths.

equals(other : Object) : Boolean

Assuming the other is a Path object and is not associated with a different file system, the paths are equal if they are the same path. Keep in mind that case sensitivity may apply. Mac OS X is case insensitive so “belchers.txt” is the same as “Belchers.TXT”, but on Linux, this would be false.

References

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.htm

Kotlin Path Interface

The Path interface is provided to Kotlin by the java.nio.file.Paths package. It represents Paths on the underlying file system and provides a number of useful utility methods for working with paths. Here is an example program followed by output and an explanation.

import java.io.BufferedWriter
import java.io.FileWriter
import java.nio.file.Paths

val belchers = "belchers.txt"

fun makeBelcherFile(){
    val names = listOf("Bob", "Linda", "Tina", "Gene", "Louise")
    BufferedWriter(FileWriter(belchers)).use { writer ->
        names.forEach { name ->
            with(writer){
                write(name)
                newLine()
            }
        }
    }
}

fun main(args : Array<String>){
    //Just make a file on the file system for demonstration purposes
    makeBelcherFile()

    //Get a reference to our example path on the disk.
    //In this case, we are using Paths.get() to get a reference to the current working directory
    //and then using the resolve() method to add the belchers.txt file to the path
    val belcherPath = Paths.get(System.getProperty("user.dir")).resolve(belchers)

    val template = "\t%-30s => %s"

    with(belcherPath){
        println("File Information")
        //The fileName property returns the name of the file
        println(template.format("File Name", fileName))

        //The root property return the the root folder of the path
        println(template.format("File Path Root", root))

        //The parent property returns the parent folder of the file
        println(template.format("File Path Parent", parent))

        //The nameCount returns how many items are in the path
        println(template.format("Name Count", nameCount))

        //The subpath() method returns a portion of the path
        println(template.format("Subpath (0, 1)", subpath(0, 1)))

        //The normalize method returns items such as . or .. from the path
        println(template.format("Normalizing", normalize()))

        //True if this is an absolute path otherwise false
        println(template.format("Is Absolute Path?", isAbsolute))

        //Convert to an absolute path if needed
        println(template.format("Absolute Path", toAbsolutePath()))

        //Check if the path starts with a path. In this example, we are using the home folder
        println(template.format("Starts with ${System.getProperty("user.home")}?", startsWith(System.getProperty("user.home"))))
        println()

        println("Elements of the Path")
        //We can print each portion of the path individually also!
        forEach { it -> println("\tPortion => $it") }
    }
}

Here is the output when run on my machine.

File Information
	File Name                      => belchers.txt
	File Path Root                 => /
	File Path Parent               => /Users/stonesoup/IdeaProjects/OCJAP
	Name Count                     => 5
	Subpath (0, 1)                 => Users
	Normalizing                    => /Users/stonesoup/IdeaProjects/OCJAP/belchers.txt
	Is Absolute Path?              => true
	Absolute Path                  => /Users/stonesoup/IdeaProjects/OCJAP/belchers.txt
	Starts with /Users/stonesoup?  => true

Elements of the Path
	Portion => Users
	Portion => stonesoup
	Portion => IdeaProjects
	Portion => OCJAP
	Portion => belchers.txt

Explanation

The program writes out a basic text file to the file system for demonstration purposes. We are going to focus on the main function. Our first task is to get a Path object that points to our belchers.txt file. We use the Paths.get() factory method and path in a path on the file system. In our example, we use the current working directory by using the System property “user.dir”.

We could have also added the belchers.txt to the end of the current working directory. However, I wanted to demonstrate the resolve method that combines two paths into a single path. So we chain the resolve method to the returned Path object and add belchers.txt. The returned Path object points to the path of belchers.txt on the file system.

The next part of the program demonstrates commonly used methods found on the Path interface. Line 33 prints the name of the file by using the fileName property. Next we print out the root of the path by using the root property (line 36). When we want to know the parent of a path, we can use the parent property (line 39).

The Path interface has a nameCount property (line 42) that returns the number of items in a path. So if a path is /Users/stonesoup/IdeaProjects/OCJAP/belchers.txt, nameCount returns 5, one for each item between each slash (/) character. The nameCount is useful when working with the Subpath function (line 45), which accepts a start index (inclusive) and an end index (exclusive) and returns a Path object based on the indexes.

Sometimes paths are abnormal paths and may have “.” or “..” characters in the path. When we want to remove such characters, we use the normalize() function (line 48) which strips out abnormal characters from the path. Depending on the work we may be doing, we may want to test if the Path is a relative path or an abosulte path. The Path interface has an isAbsolute property (line 51) for such purposes. It returns true if the path is an absolute path otherwise false.

Should we wish to convert a relative path into an absolute path, we only need to call the toAbsolutePath() function (line 54) and we will get an absolute path. We can also check if a path starts with a certain path. In our example, line 57, we check if our path starts with the users home directory (user.home). It returns true or false based on the outcome.

Path supports the forEach() function. Line 62 shows an example of how we can iterate through each part of the Path. The it variable holds each portion of the path and the program prints each part of the path.

Common Methods

We spoke about each method as it relates the program above. Here are each of the commonly used methods broken down.

Paths.get(first : String, varages more : String) : Path

The get() converts a String (or URI in the overloaded version) into a Path object. When we use use the varags part, the Path will use the OS name seperator. So Unix paths will have a forward slash, while Windows ones will have a backslash.

val home = Paths.get(System.getProperty("user.home"))

parent : Path

The parent property returns a Path object that points to the parent of the current Path object.

val parent = home.parent

nameCount : Int

The nameCount returns the number of items in the path.

val count = home.nameCount

subPath(beginIndex : Int, endIndex : Int) : Path

The subPath method is used to return a portion of the path object. The beginIndex is inclusive while the endIndex is exclusive.

val part = home.subPath(0, 2)

normalize() : Path

The normalize() method returns a Path object without unneeded characters.

val norm = home.normalize()

resolve(other : Path) : Path, resolve(other : String): Path

Returns a Path object that is a combined path between the current path and the other parameter.

val belchers = home.resolve("belchers.txt")

isAbsolute : Boolean

True if the Path is an absolute path otherwise it’s false.

val absolute = home.isAbsolute

startsWith(path : String) : Boolean, startsWith(path : Path) : Boolean

True if the current path starts with the supplied path argument.

val hasRoot = home.startsWith("/")

toAbsolutePath() : Path

Returns a Path object that is the aboslute path of the current Path.

val abs = home.toAbsolutePath()

References

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html

Recursion Example — Walking a file tree

Many developers use Python as a platform independent scripting language to perform file system operations. Sometimes it’s necessary to walk through a file system. Here is one way to navigate a file system recusively. (Of course, Python has libaries that do this!)

import os

def walk_fs(start_dir):
    # Get a list of everything in start_dir
    contents = os.listdir(start_dir)

    # This stores the output
    output = []

    # Loop through every item in contents
    for f in contents:
        # Use os.path.join to reassmble the path
        f_path = os.path.join(start_dir, f)

    # check if f_path is directory (or folder)
    if os.path.isdir(f_path):
        # Make recusive call to walk_fs
        output = output + walk_fs(f_path)
    else:
        # Add the file to output
        output.append(f_path)

    # Return a list of files in the directory
    return output

if __name__ == '__main__':
    try:
        result = walk_fs(input('Enter starting folder => '))
        for r in result:
            print(r)
    except FileNotFoundError:
    print('Not a valid folder! Try again!')

The key to this is to begin by using os.listdir, which returns a list of every item in a directory. Then we can loop through each item in contents. As we loop through contents, we need to reassemble the full path because f is only the name of the file or directory. We use os.path.join because it will insert either / (unix-like systems) or \ (windows) between each part of the path.

The next statement checks if f_path is a file or directory. The os.path.isdir function is True if the item is a directory, false otherwise. If f_path is a folder, we can make a recursive call to walk_fs starting with f_path. It will return a list of files that we can concat to output.

If f_path is a file, we just add it to output. When we have finished iterating through contents, we can return output. The output file will hold all of the files in start_dir and it’s subdirectorys.