Paths in NIO.2

In order to work with file system one must first be able to point to files and directories. The first thing that needs to be understood is the role of java.nio.file.Path class, the way instances are created and its functionality. As mentioned in previous articles, Path is just an abstraction of the file system location. This allows for the situations when directory does not even have to exist. NIO.2 presents more elegant solutions for getting the object representing file system location. This shields programmer from platform specific problems.

In general, Path instances allow two types of operations:

  • syntactic operations
    • any operations related to the Path representation itself – hierarchy traversal, conversion, comparison and so on
  • file operations
    • operations that modify location, state or contents of a file represented by a path instance

Absolute paths

First, the most common type of path that we can create is an absolute path. You may know it by the names complete path or final path. Path contains complete directory hierarchy down to the final leaf node. To define this type of path, developer is free to decide whether to specify it by a single string or by entering every hierarchical level as a separate string. The only difference between them is in the absence of path delimiter in the definition of the path.

// each of the following calls gets given path from the default file system
Path path1 = Paths.get("c:/src/java/nio/file/Path.java");
Path path2 = Paths.get("c:", "src/java/nio", "file", "Path.java");
Path path3 = Paths.get("c:", "src", "java", "nio", "file", "Path.java");

The code snippet above is pretty straightforward – it creates 3 instances of Path. All these paths are equivalent since they point to the same location on the default file system. This allows us to compare these paths with the result of equivalency. This approach to getting path instances brings one big improvement – programmer does not have to specify file system delimiter since it is provided by Java. This would not be possible to do with good old java.io.File (unless you would be willing to nest several calls of File(String parent, String child) to follow hierarchical structure).

// each of the following calls gets given path from the  default file system
Path path4 = Paths.get("/usr/java/jdk1.7.0_25/src/java/nio/file/Path.java");
Path path5 = Paths.get("/", "usr/java/jdk1.7.0_25", "src/java/nio", "file", "Path.java");
Path path6 = Paths.get("/", "usr", "java", "jdk1.7.0_25", "src", "java", "nio", "file", "Path.java");

With regard to the last paragraph please note, that when retrieving path instances on for example Linux file systems, first / symbol is required. In this example it is actually the name of root file system location – not to be confused with file path delimiter.

Relative paths

Relative (or incomplete) path is a type of path that is only a subset of an absolute path. You can easily identify relative path declaration simply by looking at the syntax of the declaration itself. Relative path is always relative to something concrete:

  • path starting with delimiter
    • Paths declared this way are always relative to the root directory of the file system
  • path starting without delimiter
    • Paths declared this way are always relative to the current directory

Lets look at the following example where we create two relative paths for each test platform and using conversion method toAbsolutePath obtain absolute versions of these paths.

// executed on Windows
Path pathRelativeToCurrentDirectoryWin = Paths.get("src/main/java");
Path pathRelativeToRootFSLocationWin = Paths.get("/src/main/java");
System.out.println("Windows:\nPath relative to current directory: " + pathRelativeToCurrentDirectoryWin.toAbsolutePath());
System.out.println("Path relative to root file system location: " + pathRelativeToRootFSLocationWin.toAbsolutePath());

// executed on Fedora
Path pathRelativeToCurrentDirectoryFed = Paths.get("src/main/java");
Path pathRelativeToRootFSLocationFed = Paths.get("/src/main/java");
System.out.println("\nFedora:\nPath relative to current directory: " + pathRelativeToCurrentDirectoryFed.toAbsolutePath());
System.out.println("Path relative to root file system location: " + pathRelativeToRootFSLocationFed.toAbsolutePath());

After running this code snippet you will end up with an output similar to the following:

Windows:
Path relative to current directory:C:\Workspaces\blog-nio2\src\main\java
Path relative to root file system location: C:\src\main\java

Fedora:
Path relative to current directory: /home/jstas/Documents/workspace/blog-nio2/src/main/java
Path relative to root file system location: /src/main/java

* Since I am using Eclipse IDE my current directory is set to my workspace location. When it comes to current directory selection, it follows the location where the process was initiated.

When it comes to working with paths, it is also good to know that shortcut notation is at our full disposal. Just a brief reminder here:

  • single dot
    • represents current directory
  • two dots
    • represents parent directory
// relative path with .. shortcut
Path doubleDotPath = FileSystems.getDefault().getPath("../../../../src");
System.out.println("Path with .. shortcut: " + doubleDotPath.toAbsolutePath().normalize());

// relative path with . shortcut
Path singleDotPath = FileSystems.getDefault().getPath("/./src");
System.out.println("Path with . shortcut: " + singleDotPath.toAbsolutePath().normalize());

With an output:

C:\src
C:\src

Paths of non-default file systems

One might ask a question what about file systems other than default file system. To answer this we first need to take a look at how file system instances are created. Based on the nature of path, it is only logical that they are involved in the file system creation process. Lets take a ZIP file system for example.

Returning to the question in the beginning of this paragraph, well, there is an easy solution to retrieve path instances for files and directories in other than default file systems as well. This means that all information provided above are applicable to any given file system, as long as Java has access to required file system implementation. NIO.2 brings a transparent way of handling paths, whether they are representing default file system location or some other file system location.

Lets take a look at some basic ways of getting paths from ZIP file system. First, and let’s be honest, the most simple way is to create a path instance, use it to create file system instance and retrieve paths from this new file system. Whole situation is described in following snippet:

Path zipFilePath = Paths.get("c:", "Program Files", "Java", "jdk1.7.0_40", "src.zip");

try (FileSystem zipFS = FileSystems.newFileSystem(zipFilePath, null)) {
    Path relativePath = zipFS.getPath("java/nio");
    Path absolutePath = zipFS.getPath("/java/nio/file/Path.java");

    System.out.println("Relative ZIP path: " + relativePath.toAbsolutePath().normalize());
    System.out.println("Absolute ZIP path: " + absolutePath);
    System.out.println("ZIP file system path separator: " + zipFS.getSeparator());
} catch (IOException e) {
    throw new RuntimeException(e);
}

With an output:

Relative ZIP path: /java/nio
Absolute ZIP path: /java/nio/file/Path.java
ZIP file system separator: /

Another way of achieving this is to use java.net.URI. The only thing we need to modify in previous example is the attribute in method get from class Paths.

URI uri = URI.create("file:///c:/Program%20Files/Java/jdk1.7.0_40/src.zip");
Path zipFilePath = Paths.get(uri);

try (FileSystem zipFS = FileSystems.newFileSystem(zipFilePath, null)) {
    Path relativePath = zipFS.getPath("java/nio");
    Path absolutePath = zipFS.getPath("/java/nio/file/Path.java");

    System.out.println("Relative ZIP path: " + relativePath.toAbsolutePath().normalize());
    System.out.println("Absolute ZIP path: " + absolutePath);
    System.out.println("ZIP file system path separator: " + zipFS.getSeparator());
} catch (IOException e) {
    throw new RuntimeException(e);
}

These two ways are pretty nice, but imagine the situation when we want to access a path within target file system directly. NIO.2 presents simple solution for this type of situations. Lets say, we want to work with a path, that points to the Path interface source code stored in src.zip file in my Java installation folder. For this type of paths, programmer has to specify also path within target zip file. So, step one is to formulate URI, that incorporates both paths and is correctly prefixed. In order to make such URI, the URI is composed as follows: jar:file:<path to zip file>!<path to file stored in zip file>. Following code displays the situation nicely.

URI fileUri = URI.create("jar:file:/c:/src.zip!/java/nio/file/Path.java");
Path filePath = Paths.get(fileUri);

System.out.println(filePath);

However, the output is not quite satisfying:

Exception in thread "main" java.nio.file.FileSystemNotFoundException
    ...

What has happened? Well, if I take a look back here, chapter FileSystem clearly states, that file system can be in two states – open and closed. File systems are implicitly opened right after the creation and you can access them to retrieve desired path only when they are instantiated and in open state. Since this condition was not fulfilled the first time, lets modify previous example so it works in a desired way.

URI zipUri = URI.create("jar:file:/c:/src.zip");
URI fileUri = URI.create("jar:file:/c:/src.zip!/java/nio/file/Path.java");

try (FileSystem zipFs = FileSystems.newFileSystem(zipUri, Collections.EMPTY_MAP)) {
    Path filePath = Paths.get(fileUri);
    System.out.println(filePath);
} catch (IOException e) {
    throw new RuntimeException(e);
}

* Please note that ZIP file system has only one root location represented by / – ZIP file system will be described in one of the future articles

It’s now, that we finally get desired output from previous example:

/java/nio/file/Path.java

Path conversion

It is necessary for any practical application to be able to convert path instances to other object representations. Whether we want to work with pre Java 7 code, or allow other developers to use our APIs, we need to be able to integrate NIO.2 constructs with the outside world. Path conversion works with all basic types of paths right out of the box. But I have not mentioned one particular type of path yet.

This special path is so-called real path. I have not mentioned it yet, because it is quite similar to the absolute path with only one distinction, that directly affects path conversion. In general, real path is derived from an absolute path. Real path consists of hierarchy elements names that are case-sensitive, while locating of files on the file system is not case-sensitive. Its definition is both implementation and platform dependent.

Path instances can be converted to six basic representations:

  • Absolute path
    • You can convert a path to its absolute form using toAbsolutePath method. By doing so the resulting path contains whole hierarchical structure and may contain shortcut notation.
  • Normalized path
    • In order to remove shortcut notation from an absolute path you can convert it to its normalized form using normalize method.
  • File
    • To ensure backwards compatibility with pre Java 7 releases programmers are armed with toFile method allowing tighter integration between NIO.2 and other libraries.
  • Real path
    • When you apply method toRealPath to an absolute path you will end up with real path.
  • String
    • Most common conversion method to create string representation of paths – method toString.
  • URI
    • Using method toUri creates URI representation of given path providing detailed view of file location (and in case of ZIP file system also the view of ZIP-containing file system)

Designers of NIO.2 library were aware of the need to integrate this library with an existing code. In order to preserve code compatibility with programs working with java.io.File, designers introduced toPath method, so the conversion from File to Path is also possible.

However, there is also another interesting way to create and convert path instances in one simple step – path combination. Combining paths is a technique allowing programmer to use already defined path and append it with partial path. This way proves to be very efficient during the declaration of paths with fixed parent hierarchy. Paths created this way are absolute paths. Combining is controlled by two methods:

  • resolve
    • Method appends provided partial path to the end of base path. If the partial path does not contain root file system location, both paths are simply concatenated. In case of root file system location being present in partial path we have a problem. In this case the whole operation is strongly implementation dependent and hence is not specified.
  • resolveSibling
    • Based on parent element, method retrieves parent location and concatenates it with partial path from method attribute. If fixed path does not have a parent element (empty path) or partial path turns out to be an absolute path, then partial path is returned.

To better understand path combination please take a look at following code snippet:

try (FileSystem zipFS = FileSystems.newFileSystem(zipPath, null)) {
    Path fixedPathZip = zipFS.getPath("/java/nio");
    Path resolvedPathZip1 = fixedPathZip.resolve("file/Path.java");
    Path resolvedPathZip2 = fixedPathZip.resolve("/file/Path.java");

    System.out.println("Fixed ZIP path: " + fixedPathZip.toAbsolutePath());
    System.out.println("Resolved ZIP path 1: " + resolvedPathZip1.toAbsolutePath());
    System.out.println("Resolved ZIP path 2: " + resolvedPathZip2.toAbsolutePath());

    Path fixedPath = Paths.get("c:/Program Files/Java/jdk1.7.0_40/src/java/nio/file/Path.java");
    Path resolvedPath = fixedPath.resolveSibling("Paths.java");

    System.out.println("\nFixed path: " + fixedPath.toAbsolutePath());
    System.out.println("Resolved path: " + resolvedPath.toAbsolutePath());
}

With an output:

Fixed ZIP path: /java/nio
Resolved ZIP path 1: /java/nio/file/Path.java
Resolved ZIP path 2: /file/Path.java

Fixed path: c:\Program Files\Java\jdk1.7.0_40\src\java\nio\file\Path.java
Resolved path: c:\Program Files\Java\jdk1.7.0_40\src\java\nio\file\Paths.java

Path comparison

The last of notable path properties is the ability to compare them. This is pretty clear from the declaration of Path interface, since it implements Comparable<T> interface. In general, there are three basic ways of comparing paths each with its own unique processing and area of application:

  • equals
    • Method fulfilling equals contract from java.lang.Object
  • compareTo
    • Method performing lexicographical comparison of given paths. Ordering based on this method is highly implementation dependent on file system provider and in case of default file system provider even platform dependent. It is important to bear in mind that this is purely lexicographical comparison, hence no file system access is required (nor performed) and files located by provided paths do not need to exist.
  • Files.isSameFile
    • Method that verifies whether two files from provided paths are identical or not. Even though, this method does not compare paths, I decided to include it here, since it provides very useful functionality within this topic. Method works in two steps:
      1. Step: Compare two provided paths using equals method – if true is returned than comparison ends with true return value. If paths are from two different file systems than method returns false. Otherwise step 2 is executed.
      2. Step: Actual comparison of files located by compared paths. Behavior of this step is highly implementation dependent and might require file system access.

Lets take a look at these methods in action:

final Path zipPath = Paths.get("c:/Program Files/Java/jdk1.7.0_40/src.zip");

try (FileSystem zipFS = FileSystems.newFileSystem(zipPath, null)) {
    Path thisPath = Paths.get("c:/Program Files/Java/jdk1.7.0_40/src/java/nio/file/Path.java");
    Path thatPath = Paths.get("c:/src/java/nio/file/Path.java");
    Path otherPath = Paths.get("/src/java/nio/file/Path.java");
    Path zipFilePath = zipFS.getPath("/java/nio/file/Path.java");

    System.out.println("Method compareTo  - This and that paths are: " + (thisPath.compareTo(thatPath) == 0 ? "equivalent" : "different"));
    System.out.println("Method equals     - This and that paths are: " + (thisPath.equals(thatPath) ? "equal" : "not equal"));
    System.out.println("Method isSameFile - This and that paths point to the same file: " + (Files.isSameFile(thisPath, thatPath) ? "true" : "false"));
    System.out.println("Method isSameFile - That and other paths point to the same file: " + (Files.isSameFile(thatPath, otherPath) ? "true" : "false"));

    System.out.println("\nMethod equals     - This and ZIP paths are: " + (thisPath.equals(zipFilePath) ? "equal" : "not equal"));
    System.out.println("Method isSameFile - This and ZIP paths point to the same file: " + (Files.isSameFile(thisPath, zipFilePath) ? "true" : "false"));

    // throws java.lang.ClassCastException: com.sun.nio.zipfs.ZipPath cannot be cast to sun.nio.fs.WindowsPath
    System.out.println("Method compareTo  - This and ZIP paths are: " + (thisPath.compareTo(zipFilePath) == 0 ? "equivalent" : "different"));
} catch (ClassCastException e) {
    System.err.println("\nMethod compareTo  - This and ZIP paths are: not suitable for comparison (java.lang.ClassCastException was thrown) !");
}

With an output:

Method compareTo  - This and that paths are: different
Method equals     - This and that paths are: not equal
Method isSameFile - This and that paths point to the same file: false
Method isSameFile - That and other paths point to the same file: true

Method equals     - This and ZIP paths are: not equal
Method isSameFile - This and ZIP paths point to the same file: false

Method compareTo  - This and ZIP paths are: not suitable for comparison (java.lang.ClassCastException was thrown) !

Leave a Reply

Your email address will not be published. Required fields are marked *