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

Advertisements

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-