Chapter 8. The Virtual File System

Duplication of resource-handling code is a common problem for developers. In most cases, the code deals with determining information about a particular resource, which might be a file, a directory, or, in the case of a JAR, a remote URL. Another duplication problem is code for the processing of nested archives. Example 8.1, “Resource Duplication Problem” illustrates the problem.

Example 8.1. Resource Duplication Problem

public static URL[] search(ClassLoader cl, String prefix, String suffix) throws IOException {
    Enumeration[] e = new Enumeration[]{
	cl.getResources(prefix),
	cl.getResources(prefix + "MANIFEST.MF")
    };
    Set all = new LinkedHashSet();
    URL url;
    URLConnection conn;
    JarFile jarFile;
    for (int i = 0, s = e.length; i < s; ++i)
	{
	    while (e[i].hasMoreElements())
		{
		    url = (URL)e[i].nextElement();
		    conn = url.openConnection();
		    conn.setUseCaches(false);
		    conn.setDefaultUseCaches(false);
		    if (conn instanceof JarURLConnection)
			{
			    jarFile = ((JarURLConnection)conn).getJarFile();
			}
		    else
			{
			    jarFile = getAlternativeJarFile(url);
			}
		    if (jarFile != null)
			{
			    searchJar(cl, all, jarFile, prefix, suffix);
			}
		    else
			{
			    boolean searchDone = searchDir(all, new File(URLDecoder.decode(url.getFile(), "UTF-8")), suffix);
			    if (searchDone == false)
				{
				    searchFromURL(all, prefix, suffix, url);
				}
			}
		}
	}
    return (URL[])all.toArray(new URL[all.size()]);
}

private static boolean searchDir(Set result, File file, String suffix) throws IOException
{
    if (file.exists() && file.isDirectory())
	{
	    File[] fc = file.listFiles();
	    String path;
	    for (int i = 0; i < fc.length; i++)
		{
		    path = fc[i].getAbsolutePath();
		    if (fc[i].isDirectory())
			{
			    searchDir(result, fc[i], suffix);
			}
		    else if (path.endsWith(suffix))
			{
			    result.add(fc[i].toURL());
			}
		}
	    return true;
	}
    return false;
}
There are also many problems with file locking on Windows systems, forcing developers to copy all hot-deployable archives to another location to prevent locking those in deploy folders (which would prevent their deletion and file-system based undeploy). File locking is a major problem whose only solution used to be centralizing all the resource loading code in one place.
The VFS project was created solve all of these issues. VFS stands for Virtual File System.

8.1. VFS Public API

VFS is used for two main purposes, as shown in Uses for VFS.

Uses for VFS

  • simple resource navigation
  • visitor pattern API (Application Programmer Interface)
As mentioned, in plain JDK, handling and navigating resources are complex. You must always check the resource type, and these checks can be cumbersome. VFS abstracts resources into a single resource type, VirtualFile.

Example 8.2. The VirtualFile Resource Type

public class VirtualFile implements Serializable {
    /**
     * Get certificates.
     *
     * @return the certificates associated with this virtual file
     */
    Certificate[] getCertificates()

    /**
     * Get the simple VF name (X.java)
     *
     * @return the simple file name
     * @throws IllegalStateException if the file is closed
     */
	String getName()

    /**
     * Get the VFS relative path name (org/jboss/X.java)
     *
     * @return the VFS relative path name
     * @throws IllegalStateException if the file is closed
     */
	String getPathName()

    /**
     * Get the VF URL (file://root/org/jboss/X.java)
     *
     * @return the full URL to the VF in the VFS.
     * @throws MalformedURLException if a url cannot be parsed
     * @throws URISyntaxException if a uri cannot be parsed
     * @throws IllegalStateException if the file is closed
     */
	URL toURL() throws MalformedURLException, URISyntaxException

    /**
     * Get the VF URI (file://root/org/jboss/X.java)
     *
     * @return the full URI to the VF in the VFS.
     * @throws URISyntaxException if a uri cannot be parsed
     * @throws IllegalStateException if the file is closed
     * @throws MalformedURLException for a bad url
     */
			   URI toURI() throws MalformedURLException, URISyntaxException

    /**
     * When the file was last modified
     *
     * @return the last modified time
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   long getLastModified() throws IOException

    /**
     * Returns true if the file has been modified since this method was last called
     * Last modified time is initialized at handler instantiation.
     *
     * @return true if modifed, false otherwise
     * @throws IOException for any error
     */
			   boolean hasBeenModified() throws IOException

    /**
     * Get the size
     *
     * @return the size
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   long getSize() throws IOException

    /**
     * Tests whether the underlying implementation file still exists.
     * @return true if the file exists, false otherwise.
     * @throws IOException - thrown on failure to detect existence.
     */
			   boolean exists() throws IOException

    /**
     * Whether it is a simple leaf of the VFS,
     * i.e. whether it can contain other files
     *
     * @return true if a simple file.
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   boolean isLeaf() throws IOException

    /**
     * Is the file archive.
     *
     * @return true if archive, false otherwise
     * @throws IOException for any error
     */
			   boolean isArchive() throws IOException

    /**
     * Whether it is hidden
     *
     * @return true when hidden
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   boolean isHidden() throws IOException

    /**
     * Access the file contents.
     *
     * @return an InputStream for the file contents.
     * @throws IOException for any error accessing the file system
     * @throws IllegalStateException if the file is closed
     */
			   InputStream openStream() throws IOException

    /**
     * Do file cleanup.
     *
     * e.g. delete temp files
     */
			   void cleanup()

    /**
     * Close the file resources (stream, etc.)
     */
			   void close()

    /**
     * Delete this virtual file
     *
     * @return true if file was deleted
     * @throws IOException if an error occurs
     */
			   boolean delete() throws IOException

    /**
     * Delete this virtual file
     *
     * @param gracePeriod max time to wait for any locks (in milliseconds)
     * @return true if file was deleted
     * @throws IOException if an error occurs
     */
			   boolean delete(int gracePeriod) throws IOException

    /**
     * Get the VFS instance for this virtual file
     *
     * @return the VFS
     * @throws IllegalStateException if the file is closed
     */
			   VFS getVFS()

    /**
     * Get the parent
     *
     * @return the parent or null if there is no parent
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   VirtualFile getParent() throws IOException

    /**
     * Get a child
     *
     * @param path the path
     * @return the child or <code>null</code> if not found
     * @throws IOException for any problem accessing the VFS
     * @throws IllegalArgumentException if the path is null
     * @throws IllegalStateException if the file is closed or it is a leaf node
     */
			   VirtualFile getChild(String path) throws IOException

    /**
     * Get the children
     *
     * @return the children
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   List<VirtualFile> getChildren() throws IOException

    /**
     * Get the children
     *
     * @param filter to filter the children
     * @return the children
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed or it is a leaf node
     */
			   List<VirtualFile> getChildren(VirtualFileFilter filter) throws IOException

    /**
     * Get all the children recursively<p>
     *
     * This always uses {@link VisitorAttributes#RECURSE}
     *
     * @return the children
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed
     */
			   List<VirtualFile> getChildrenRecursively() throws IOException

    /**
     * Get all the children recursively<p>
     *
     * This always uses {@link VisitorAttributes#RECURSE}
     *
     * @param filter to filter the children
     * @return the children
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalStateException if the file is closed or it is a leaf node
     */
			   List<VirtualFile> getChildrenRecursively(VirtualFileFilter filter) throws IOException

    /**
     * Visit the virtual file system
     *
     * @param visitor the visitor
     * @throws IOException for any problem accessing the virtual file system
     * @throws IllegalArgumentException if the visitor is null
     * @throws IllegalStateException if the file is closed
     */
			   void visit(VirtualFileVisitor visitor) throws IOException
			   }
All of the usual read-only File System operations are available, plus a few options to cleanup or delete the resource. Cleanup or deletion handling is needed when dealing with some internal temporary files, such as files created to handle nested jars.
To switch from the JDK's File or URL resource handling to new VirtualFile you need a root VirtualFile, which is provided by the VFS class, with the help of URL or URI parameter.

Example 8.3. Using the VFS Class

public class VFS {
    /**
     * Get the virtual file system for a root uri
     *
     * @param rootURI the root URI
     * @return the virtual file system
     * @throws IOException if there is a problem accessing the VFS
     * @throws IllegalArgumentException if the rootURL is null
     */
    static VFS getVFS(URI rootURI) throws IOException

    /**
     * Create new root
     *
     * @param rootURI the root url
     * @return the virtual file
     * @throws IOException if there is a problem accessing the VFS
     * @throws IllegalArgumentException if the rootURL
     */
    static VirtualFile createNewRoot(URI rootURI) throws IOException

    /**
     * Get the root virtual file
     *
     * @param rootURI the root uri
     * @return the virtual file
     * @throws IOException if there is a problem accessing the VFS
     * @throws IllegalArgumentException if the rootURL is null
     */
    static VirtualFile getRoot(URI rootURI) throws IOException

    /**
     * Get the virtual file system for a root url
     *
     * @param rootURL the root url
     * @return the virtual file system
     * @throws IOException if there is a problem accessing the VFS
     * @throws IllegalArgumentException if the rootURL is null
     */
    static VFS getVFS(URL rootURL) throws IOException

    /**
     * Create new root
     *
     * @param rootURL the root url
     * @return the virtual file
     * @throws IOException if there is a problem accessing the VFS
     * @throws IllegalArgumentException if the rootURL
     */
    static VirtualFile createNewRoot(URL rootURL) throws IOException

    /**
     * Get the root virtual file
     *
     * @param rootURL the root url
     * @return the virtual file
     * @throws IOException if there is a problem accessing the VFS
     * @throws IllegalArgumentException if the rootURL
     */
    static VirtualFile getRoot(URL rootURL) throws IOException

    /**
     * Get the root file of this VFS
     *
     * @return the root
     * @throws IOException for any problem accessing the VFS
     */
    VirtualFile getRoot() throws IOException
}
The three different methods look similar.
  • getVFS
  • createNewRoot
  • getRoot
getVFS returns a VFS instance but does not yet create a VirtualFile instance. This is important because there are methods which help with configuring a VFS instance (see VFS class API javadocs), before instructing it to create a VirtualFile root.
The other two methods, on the other hand, use default settings for root creation. The difference between createNewRoot and getRoot is in caching details, which are covered later.

Example 8.4. Using getVFS

URL rootURL = ...; // get root url
VFS vfs = VFS.getVFS(rootURL);
// configure vfs instance
VirtualFile root1 = vfs.getRoot();
// or you can get root directly
VirtualFile root2 = VFS.crateNewRoot(rootURL);
VirtualFile root3 = VFS.getRoot(rootURL);
Another useful thing about VFS API is its implementation of a proper visitor pattern. It is very simple to recursively gather different resources, a task which is difficult to do with plain JDK resource loading.

Example 8.5. Recursively Gathering Resources

public interface VirtualFileVisitor {
    /**
     * Get the search attribues for this visitor
     *
     * @return the attributes
     */
    VisitorAttributes getAttributes();

    /**
     * Visit a virtual file
     *
     * @param virtualFile the virtual file being visited
     */
    void visit(VirtualFile virtualFile);
}

VirtualFile root = ...; // get root
VirtualFileVisitor visitor = new SuffixVisitor(".class"); // get all classes
root.visit(visitor);