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

Advertisements

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

Kotlin Serializable Classes

We are free to write our own serializable classes. All we need to do is have our class implement the java.io.Serializable interface to mark it as a candidate for serialization. All non-transient fields are written to the output stream and restored by the input stream. Here is a program that demonstrates using serialization with our own classes.

import java.io.*

data class Cook(val name : String = "Bob",
                val job : String = "Cook",
                @Transient val age : Int = 44) : Serializable

fun main(args : Array<String>){
    val file = "bob.ser"

    val tiredBob = Cook()
    println("Before Serialization")
    println(tiredBob)

    ObjectOutputStream(FileOutputStream(file)).use{ it -> it.writeObject(tiredBob)}

    println("Bob has been serialized")
    println()
    println("Time to wake Bob up")

    ObjectInputStream(FileInputStream(file)).use { it ->
        val restedBob = it.readObject()

        when (restedBob){
            is Cook -> {
                println("Does Bob remember his age?")
                println(restedBob)
            }
            else -> println("Failed to restore Bob")
        }
    }
}

Our program defines a Cook data class on lines 3-5. The class implements Serializable so it will work with the JVM serialization mechanism. The age property is annotated with @Transient, but more on that later.

The main method creates a tiredBob variable on line 10. On line 14, we open the file bob.ser by passing the name of the file to the FileOutputStream object. The FileOutputStream object is then passed to the constructor of the ObjectOutputStream object. We apply the use() function to make sure the file is closed when finished. The tiredBob object is written to disk by invoking the writeObject() method on the ObjectOutputStream object.

Restoring Bob is done on lines 20-30. Once again, we start by opening the bob.ser file by creating a FileInputStream object and passing the name of the file the constructor. The FileInputStream object is passed to the constructor of the ObjectInputStream object. We chain this operation with the use() function again to ensure the file is closed.

Line 21 restores Bob to memory by calling readObject(). The return type of readObject() is Any, so it’s up to us to down cast the object back into Cook. We do this on lines 23-28. On line 26, we print Bob to the console, but his age is 0. That’s because the age property is transient and therefore excluded from the serialization mechanism.

Transient Properties

Transient properties are defined by adding the @Transient annotation in front of the property (line 5). It’s used because the JVM only serializes primitives and Serializable objects. I once ran into a program where a class was mixed with Swing UI components and the state of the object needed to get sent over network sockets. The UI components were not serializable, so they were marked transient. Otherwise, the JVM would have failed to serialize the object and would have raised an exception.

This was a rather poor design, by the way. The class should have been designed with composition and have the data class separated from the UI components. However, that’s not how this program had been written. Either way, there are certain times where we would want to exclude serializable fields. A password field is a prime example of what should be marked transient.

However, it’s up to the developer to decide what should be included and not included in the serialized object. Kotlin provides the @Transient annotation to accomplish the task. Any object that isn’t @Transient will get included in the serialized object.

Kotlin Object Serialization

Whenever a class implements Serializable, it’s a candidate for object serialization. The serialization mechanism converts an object into bytes and then writes the object to the output stream. We use the class ObjectOutputStream to serialize a file and then ObjectInputStream to restore an object.

import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream

fun main(args : Array<String>){
    //Destination File
    val file = "belchers.burgers"

    //A map of family
    val family = mapOf(
            "Bob" to "Father",
            "Linda" to "Mother",
            "Tina" to "Oldest",
            "Gene" to "Middle",
            "Louise" to "Youngest")

    //Write the family map object to a file
    ObjectOutputStream(FileOutputStream(file)).use{ it -> it.writeObject(family)}

    println("Wrote $file")
    println()
    println("Time to read $file back")

    //Now time to read the family back into memory
    ObjectInputStream(FileInputStream(file)).use { it ->
        //Read the family back from the file
        val restedFamily = it.readObject()

        //Cast it back into a Map
        when (restedFamily) {
            //We can't use <String, String> because of type erasure
            is Map<*, *> -> println(restedFamily)
            else -> println("Deserialization failed")
        }
    }
}

The example program writes a map of strings to a file using object serialization. It begins by creating a map of test data on lines 11-16. Line 19 opens the file by creating a FileOutputStream object and passing in the file name to the constructor. The FileOutputStream object gets passed to the newly created ObjectOutputStream. We apply the use() function to make sure all resources are closed when finished.

Writing the map to the file is painless. All we need to do is use the writeObject() method found on ObjectOutputStream, shown on line 19. The class does all of the work of flattening the family Map object into bytes and writing the bytes to the file. The use() function closes the file and the serialization process is complete.

Reading the object back into memory is almost as simple. We open the file by creating a new FileInputStream object and supplying the constructor with the file name. The FileInputStream object is supplied to the constructor of the ObjectInputStream and we chain it to the use() function to make sure the file gets closed when finished.

The object is restored with the readObject() method, but there is a catch. The readObject() method returns Any. It’s our job to downcast to the proper type. On line 31, we use the when() function and on line 33, we check that it is a Map. Since map is a generic interface and serialization doesn’t save type, we use *, * for the type arguments. At this point, we can work on the restedFamily object normally.

Kotlin Data Streams

Data streams are used to write binary data. The DataOutputStream writes binary data of primitive types, while DataInputStream reads data back from the binary stream and converts it to primitive types. Here is an example program that writes data to a file and then reads it back into memory.

import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream

fun main(args : Array<String>){
    val burgers = "data.burgers"

    //Open the file in binary mode
    DataOutputStream(FileOutputStream(burgers)).use { dos ->
        with(dos){
            //Notice we have to write our data types
            writeInt("Bob is Great\n".length) //Record length of the array
            writeChars("Bob is Great\n") //Write the array
            writeBoolean(true) //Write a boolean

            writeInt("How many burgers can Bob cook?\n".length) //Record length of array
            writeBytes("How many burgers can Bob cook?\n") //Write the array
            writeInt(Int.MAX_VALUE) //Write an int

            for (i in 0..5){
                writeByte(i) //Write a byte
                writeDouble(i.toDouble()) //Write a double
                writeFloat(i.toFloat()) //Write a float
                writeInt(i) //Write an int
                writeLong(i.toLong()) //Write a long
            }
        }
    }

    //Open a binary file in read mode. It has to be read in the same order
    //in which it was written
    DataInputStream(FileInputStream(burgers)).use {dis ->
        with (dis){
            val bobSize = readInt() //Read back the size of the array
            for (i in 0 until bobSize){
                print(readChar()) //Print the array one character at a time
            }
            println(readBoolean()) //Read a boolean

            val burgerSize = readInt() //Length of the next array
            for (i in 0 until burgerSize){
                print(readByte().toChar()) //Print array one character at a time
            }
            println(readInt()) //Read an int

            for (i in 0..5){
                println(readByte()) //Read a byte
                println(readDouble()) //Read a double
                println(readFloat()) //Read a float
                println(readInt()) //Read an int
                println(readLong()) //Read a long
            }
        }

    }
}

The program creates a FileOutputStream object and passes the name of the file to its constructor. The FileOutputStream object is then passed to the constructor of DataOutputStream. We apply the use() function to ensure all resources are freed properly when we have finished. The file is now open for writing in binary mode.

When we wish to use the same object repeatedly, we can pass it to the with() function. In our case, we intend to keep using our DataOutputStream object, so on line 11, we pass it to the with() function. Inside of the with() function, all method calls will target the dos object because it was supplied to with().

Since we intend to write a string to the file, we need to record the length of the string. We do this using the writeInt function and passing the length of our string to it. Then we can use writeChars() to write a character array to the file. The String argument is converted to a character array and written to the file. Finally, we call writeBoolean to write true/false values to the file.

The next section is a repeat of the first. We intend to write another string to the file, but do so, we need to record the length of the file. Once again, we turn to writeInt() to record an int value. The next line, we use writeBytes() rather than writeChars() to demonstrate how we can write a byte array rather than a String. The DataOutputStream class sees to the details of turning a String into a byte array. Finally, we write another int value to the stream.

Next, we enter a for loop on line 21. Inside of the for loop, we demonstrate writing different primitive types to the file. We can use writeByte() for a byte, writeDouble() for a double, and so on for each primitive type. The DataOutputStream class knows the size of each primitive type and writes the correct number of bytes for each primitive.

When we are done writing the object, we open it again to read it. Line 33 creates a FileInputStream object that accepts the path to the file in its constructor. The FileInputStream object is chained to DataInputStream by passing it to the constructor of DataInputStream. We apply the use() function to ensure all resources are properly closed.

Reading the file requires the file to be read in the same order in which it is written. Our first order of business is to grab the size of the character array we wrote to the file earlier. We use readInt() on line 35 followed by a for loop that terminates at the size of the array on line 36. Each iteration of the for loop calls readChar() and the String is printed to the console. When we are finished, we read a boolean on line 39.

Our next array was a byte array. Once again, we need it’s final size so we call readInt() on line 41. Lines 42-44 run through the array and call readByte() until the loop terminates. Each byte is converted to a character object using toChar(). On line 45, we read an int using readInt().

The final portion of the program repeats the for loop found earlier. In this case, we enter a for loop that terminates after five iterations (line 47). Inside of the for loop, we call readByte(), readDouble(), readFloat(), and so on. Each call prints the restored variable to the console.