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

Advertisements

Kotlin Buffered Text Files

The BufferedReader and BufferedWriter classes improve the performance of reading and writing operations by adding an in-memory buffer to the streams. By using a memory buffer, the program and reduce the number of calls required to the underlying read and write streams and thus improve performance. Here is an example program that makes use of both BufferedReader and BufferedWriter.

fun main(args : Array<String>){
    when (args.size){
        //Check for two command line arguments
        2 -> {
            //Grab source and destination files
            val src = args[0]
            val dest = args[1]

            //Check if the destination file exists. We can create it
            //if needed
            with (File(dest)){
                if(!exists()){
                    createNewFile()
                }
            }

            //Now, open the source file in read mode. The BufferedReader
            //provides buffering to improve performance
            BufferedReader(FileReader(src)).use { reader ->

                //Likewise, open the destination file in write mode
                //The BufferedWriter class provides buffering for performance
                BufferedWriter(FileWriter(dest)).use { writer ->

                    //Read through the source file one character at a time
                    var character = reader.read()
                    while(character != -1){

                        //Write the character to the destination file
                        writer.write(character)

                        //Read the next character.
                        character = reader.read()
                    }
                }
            }
        }
        else -> {
            println("Source file followed by destination file required")
            System.exit(-1)
        }
    }
}

The example program copies the source file to the destination file. We begin by using the when() function to check if we have two and only two command line arguments. If we have a source and destination file, the program continues starting on line 6 otherwise it jumps down to line 39 and exits after printing an error message.

On lines 6 and 7, we grab our source and destination files from the command line parameters. On line 11, we create a new File object and pass it to the with() function to see if we need to make a new file for the destination. Line 12 uses the exits() property to see if the file exists, and if it doesn’t exist, line 13 creates the new file.

Starting at line 19, we open the source file and begin our copy operation. The file is opened by creating a new FileReader object and passing in the name of the source file. The FileReader object is then passed to the constructor of BufferedReader. We utilize the use() function to ensure that all resources are properly closed when we are finished with the read operation. It’s also worth noting that we call the lambda parameter reader rather than it to improve code readability.

Line 23 opens the destination file for writing. We create a FileWriter (the companion object to FileReader) and pass the name of the destination file to the FileWriter’s constructor. The FileWriter object is passed to the BufferedWriter constructor to provide buffering support. Once again, we utilize the use() function to ensure that all resources are closed when finished.

The copy operation is fairly anti-climatic. We read the first character on line 26 and then enter into a while loop that terminates when character == -1. Inside of the while loop, we write the character to the destination file (line 30) and then read the next character (line 33). The use() function that was applied to both the BufferedReader and BufferedWriter objects closes the files when finished.

The program can be run by using the following commands at the command line.

kotlinc BufferedCopy.kt -include-runtime -d BufferedCopy.jar
ava -jar BufferedCopy.jar  [dest file]

When finished, the dest file will be an exact copy of source file.