Updating Legacy Code, Part II: NIO.2

May 03, 2018

In a previous post, we updated legacy Java 5 code to use try-with-resources. In this post, we will be updating that same legacy code to use the new to Java 7 NIO.2 enhancements.

Java's NIO.2 is a set of new classes and methods that predominantly live in the java.nio package. It is intended to be a replacement of java.io.File as the abstraction when dealing with code that reads or writes from a filesystem.

The starting point for using NIO.2 is the Path. The Path class contains various methods for interacting with the path and extracting information about it. For the carmix-creator application, we will replace all uses of the File class and replace them with Path and other new classes and methods from the java.nio.file package.

Since carmix-creator copies files, it makes heavy use of several File-related classes. For starters, the loadPlaylistFile method can be refactored into a new method loadPlaylistPath.

Here is the original. It simply creates a File object based on the strFilePath parameter.

private File loadPlaylistFile(String strFilePath) {
    File playlistFile = null;
    if (strFilePath != null && !strFilePath.equals("")) {
        playlistFile = new File(strFilePath);
    }

    return playlistFile;
}

This is the updated version. Here we've replaced the direct File object creation with a call to Paths.get to obtain a reference to the file path and that result is loaded into a java.nio.file.Path object.

private Path loadPlaylistPath(String strPathName) {
    Path playlistPath = null;
    if (strPathName != null && !strPathName.equals("")) {
        playlistPath = Paths.get(strPathName);
    }

    return playlistPath;
}

We next refactor processPlaylistFile into the new processPlaylistPath method. Here is how the method looked originally:

private void processPlaylistFile(File filePlaylist) {

    BufferedReader input = null;
    try {
        input = new BufferedReader(new FileReader(filePlaylist));
        String firstLine = input.readLine();
        String nextLine = null;
        if (!M3U_HEADER.equals(firstLine)) {
            return;
        }
        LOG.log(Level.INFO, "Header Found");
        boolean keepReading = true;
        do {
            nextLine = input.readLine();
            if (nextLine == null) {
                keepReading = false;
            } else if ("".equals(nextLine)) {
                continue;
            } else if (nextLine.startsWith(M3U_INFO)) {
                continue;
            } else { // This should be a File URL
                processTrackURL(nextLine);
            }
        } while (keepReading);
    } catch (FileNotFoundException fnfe) {
        LOG.log(Level.SEVERE, null, fnfe);
    } catch (IOException ioe) {
        LOG.log(Level.SEVERE, null, ioe);
    } finally {
        try {
            input.close();
        } catch (IOException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
    }
}

The refactored method:

private Status processPlaylistPath(Path path) {
    try {
        List<String> lines = Files.readAllLines(path, StandardCharsets.ISO_8859_1);
        String firstLine = lines.remove(0);
        if (!M3U_HEADER.equals(firstLine)) {
            LOGGER.log(Level.WARNING, "M3U Header Not Found. for file: " + path.toString());
            return Status.INVALID_HEADER;
        }
        LOGGER.log(Level.INFO, "M3U Header Found");
        for (String s : lines) {
            if (StringUtils.isBlank(s)) {
                continue;
            } else if (s.startsWith(M3U_INFO)) {
                continue;
            } else {
                processTrackURL(s);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return Status.PROC_SUCCESSFULLY;

}

We first replaced the File parameter with a Path parameter. Next, we read in the lines of the file represented by the Path using the new Files.readAllLines static method. This method also ensures the file is closed once the data is read or if there are any exceptions while the file is being read. We no longer have to read a file line by line using a BufferedReader!

The processTrackURL method can replace the call to the private method copy to the new Files.copy method. We can also replace the checkFileOrDirectoryExists call with Files.exists.

Original:

private void processTrackURL(String strLine) {
    if (checkFileOrDirectoryExists(strLine)) {
        File inFile = new File(strLine);
        String fileName = inFile.getName();
        String newFileName = strMixDirectoryPath + fileName;
        try {
            if (usingArtistName) {
                MP3File mp3File = new MP3File(source.toFile(), false);
                ID3v1 tag = mp3File.getID3v1Tag();
                artistName = tag.getArtist();

                newFileName = this.getStrDestDirectoryName() + artistName + "\\" + fileName;
            } else {
                newFileName = this.getStrDestDirectoryName() + fileName;
            }
            File outFile = new File(newFileName);
            copy(inFile, outFile);
        } catch (IOException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
        LOG.log(Level.INFO, "File {0} located!", strLine);
    } else {
        LOG.log(Level.SEVERE, "File {0} not found!", strLine);
    }
}

New:

private void processTrackURL(String strLine) {
    Path source = Paths.get(strLine);
    if (Files.exists(source)) {
        String artistName = "";
        String fileName = source.getFileName().toString();
        String newFileName = strMixDirectoryPath + fileName;
        try {
            if (usingArtistName) {
                MP3File mp3File = new MP3File(source.toFile(), false);
                ID3v1 tag = mp3File.getID3v1Tag();
                artistName = tag.getArtist();

                newFileName = this.getStrDestDirectoryName() + artistName + "\\" + fileName;
            }
            Path target = Paths.get(newFileName);
            Files.copy(source, target, COPY_ATTRIBUTES, REPLACE_EXISTING);
        } catch (IOException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
        LOG.log(Level.INFO, "File {0} located!", strLine);
    } else {
        LOG.log(Level.SEVERE, "File {0} not found!", strLine);
    }
}

At a glance, it seems like we no longer need the private copy method. If you run the application and try copying without using the artist names, it will copy fine. However, when you select to have the files copied into folders by artist, then the copy fails due to a java.nio.file.NoSuchFileException on the target. Turns out, for the moment at least, we need the copy method after all since it took care of creating the artist folders. We can still make improvements to this private copy method, though.

So, we revert the call to Files.copy back to the private method call, updated to take Paths instead of Files. Within this method, we will call Files.copy. We also make a few updates to make use of additional methods in the Files class.

Before:

public static void copy(File inFile, File outFile) throws IOException {

    if (inFile.getCanonicalPath().equals(outFile.getCanonicalPath())) {
        // inFile and outFile are the same;
        // hence no copying is required.
        LOGGER.log(Level.INFO, "Files are the same, no copy performed");
        return;
    }

    verifyFile(inFile, FILE_TYPE, READ_FILE);

    if (outFile.isDirectory()) {
        outFile = new File(outFile, inFile.getName());
    }

    if (outFile.exists()) {
        if (!outFile.canWrite()) {
            throw new IOException("Cannot write to: " + outFile);
        }
        // This should become a prompt for the GUI
        System.out.print("Overwrite existing file " + outFile.getName() + "? (Y/N): ");
        System.out.flush();
        BufferedReader promptIn = new BufferedReader(new InputStreamReader(System.in));
        String response = promptIn.readLine();
        if (!response.toUpperCase().equals("Y")) {
            throw new IOException("FileCopy: " + "existing file was not overwritten.");
        }
    } else {
        File dirFile = outFile.getParentFile();

        if (dirFile != null && (!dirFile.exists())) {
            if (!dirFile.mkdirs()) {
                if(!dirFile.exists()) {
                    throw new IOException("Cannot create directory: " + dirFile);
                }
            }
        }

        // check for exists, isFile, canWrite
        verifyFile(inFile, FILE_TYPE, WRITE_FILE);
    }

    try(FileInputStream fis = new FileInputStream(inFile);
        FileOutputStream fos = new FileOutputStream(outFile);
        InputStream in = new BufferedInputStream(fis);
        OutputStream out = new BufferedOutputStream(fos)) {
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = in.read(buffer)) != -1){
            out.write(buffer, 0, bytesRead);
        } // write
    }
}

After:

public static void copy(Path inPath, Path outPath) throws IOException {

    if (Files.exists(outPath) && Files.isSameFile(inPath, outPath)) {
        LOGGER.log(Level.INFO, "Files are the same, no copy performed");
        return;
    }

    verifyFile(inPath, FILE_TYPE, READ_FILE);

    if (Files.exists(outPath)) {
        if (!Files.isWritable(outPath)) {
            throw new IOException("Cannot write to: " + outPath);
        }
    } else {
        Path parentDirectory = outPath.getParent();
        if (!Files.exists(parentDirectory)) {
            Files.createDirectories(parentDirectory);
        if (!Files.exists(parentDirectory)) {
            throw new IOException("Cannot create directory: " + parentDirectory);
        }
    }

    // check for exists, isRegularFile, canWrite
    verifyFile(inPath, FILE_TYPE, WRITE_FILE);
    }
    Files.copy(inPath, outPath, COPY_ATTRIBUTES, REPLACE_EXISTING);
}

There is more work that could be done to this method, but I will leave that to future post(s) that discuss refactoring.

Since we're making use of the copy method, we'll have to update the verifyFile method to take Paths instead of Files.

Here's the method pre-Path:

private static boolean verifyFile(File aFile, String indFileDir, String indReadWrite) throws IOException {
    if (!aFile.exists()) {
        throw new IOException("File Verification: " + aFile.getPath() + " does not exist");
    }
    if (FILE_TYPE.equals(indFileDir)) {
        if (!aFile.isFile()) {
            throw new IOException("File Verification: " + aFile.getPath() + " does not exist");
        }
    } else if (DIR_TYPE.equals(indFileDir)) {
        if (!aFile.isDirectory()) {
            throw new IOException("Directory Verification: " + aFile.getPath() + " does not exist");
        }
    }
    if (READ_FILE.equals(indReadWrite)) {
        if (!aFile.canRead()) {
            throw new IOException("File Verification: Cannot read file " + aFile.getPath());
        }
    } else if (WRITE_FILE.equals(indReadWrite)){
        if (!aFile.canWrite()) {
            throw new IOException("File Verification: Cannot write to file " + aFile.getPath());
        }
    }
    return false;
}

And here it is refactored to use the Path and Files classes:

private static boolean verifyFile(Path aPath, String indFileDir, String indReadWrite) throws IOException {
    if (!Files.exists(aPath)) {
        throw new IOException("File Verification: " + aPath.getFileName() + " does not exist");
    }
    if (FILE_TYPE.equals(indFileDir)) {
        if (!Files.isRegularFile(aPath)) {
            throw new IOException("File Verification: " + aPath.getFileName() + " does not exist");
        }
    } else if (DIR_TYPE.equals(indFileDir)) {
        if (!Files.isDirectory(aPath)) {
            throw new IOException("Directory Verification: " + aPath.getFileName() + " does not exist");
        }
    }
    if (READ_FILE.equals(indReadWrite)) {
        if (!Files.isReadable(aPath)) {
            throw new IOException("File Verification: Cannot read file " + aPath.getFileName());
        }
    } else if (WRITE_FILE.equals(indReadWrite)){
        if (!Files.isWritable()) {
            throw new IOException("File Verification: Cannot write to file " + aPath.getFileName());
        }
    }
    return true;
}

The updated verifyFile method helps to show how you check file properties when working with Paths. The Files class has many static methods that can be used, including checking for the existence of a file or checking whether or not a file can be read or written to. Many more methods live in this class to help you work with files.

And this should wrap up updating the carmix-creator to use Java 7 NIO.2, where applicable to the application. There is far more to NIO.2 than has been shown here and I urge you to all go give it a try if you haven't already. NIO.2 changes don't stop here. Thanks to the Streams API in Java 8, there will be new things to try with that as well.