JavaSwingJTree and TreeModel: FileTreeModel


FileTreeModel

The following is a TreeModel that represents the File structure rooted at a certain directory (or File). The nodes are the File objects themselves.

The structure is completely static and doesn't dynamically update when changes happen.

The GUI representation doesn't actually look right because the default tree cell renderer calls toString() on the nodes, and for Files this is their whole path instead of just their name.

See FileTreeModel.java for the code without interleaved comments.

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.EventListenerList;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.io.File;
import java.io.Serializable;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;



public class FileTreeModel
    implements TreeModel, Serializable, Cloneable
{
    protected EventListenerList listeners;

    private static final Object LEAF = new Serializable() { };
 
    private Map map;

The map is used to map each directory File object to either the leaf marker LEAF if the File is not a directory (in this model: a leaf), or a List of the Files contained in it.

See the method children().

    private File root;


    public FileTreeModel(File root)
    {
        this.root = root;

        if (!root.isDirectory())
            map.put(root, LEAF);

        this.listeners = new EventListenerList();

        this.map = new HashMap();
    }


    public Object getRoot()
    {
        return root;
    }

    public boolean isLeaf(Object node)
    {
        return map.get(node) == LEAF;
    }

Nodes that aren't directories are leaves, because they cannot contain any files. To avoid calling isDirectory() multiple times, it is called once (in children(), or in the constructor for the root), and if it returned false, the File is mapped to the LEAF marker. Thus, even when a non-directory File is removed and a directory with the same name is created by another process,isLeaf() consistently returns the same result.

This method could also call children(), like getChildCount(), but then the actual reading of the directories contents would happen earlier. In the current implementation, it postponed until getChildCount() is actually called.

    public int getChildCount(Object node)
    {
        List children = children(node);

        if (children == null)
            return 0;

        return children.size();
    }

This method calls children() to get information about the node's children. The actual lazy lookup happens in this call of children() because getChildCount() is called before any of the methods below.

children() may return null to indicate that either the File didn't exist (anymore) or that it wasn't a directory. These cases must be handled to return 0.

    public Object getChild(Object parent, int index)
    {
        return children(parent).get(index);
    }

    public int getIndexOfChild(Object parent, Object child)
    {
        return children(parent).indexOf(child);
    }

These methods also call children() to get the contents of the directory. Because they may only be called when parent is a directory and getChildCount() has already been called and return a count > 0, the lookup (listFiles()) has already happened before. If these methods are called with wrong parameters (for example parent a leaf or not contained at all), they will (implicitly) throw NullPointerException because children will return null. Then the calling method is buggy, it is not supposed to give wrong arguments.

    protected List children(Object node)
    {
        File f = (File)node;

        Object value = map.get(f);

        if (value == LEAF)
            return null;

        List children = (List)value;

        if (children == null)
        {
            File[] c = f.listFiles();

            if (c != null)
            {
                children = new ArrayList(c.length);

                for (int len = c.length, i = 0; i < len; i++)
                {
                    children.add(c[i]);
                    if (!c[i].isDirectory())
                        map.put(c[i], LEAF);
                }
            }
            else
                children = new ArrayList(0);

            map.put(f, children);      
        }

        return children;
    }

If the File exists and is a directory (and no IO error occurs during looking up its contents) and it has not been looked up before, the code creates a List of child nodes (It may just use the array itself, but arrays don't generalize to variable-sized Lists, so I used List from the beginning; also it avoids having to write getIndexOfChild() explicitly). It then stores the mapping directory to children in the map, so it can be retrieved the next time this method is called for the same directory. If listFiles() returns null (IO error), I store an empty List as well. Otherwise the lookup would happen again (since the file is still mapped to null or not even contained in the Map), and if then it worked without IO error, the model would suddenly have changed, but a) this is not noticed by the code, and b) there is no support in this simple class for firing events at all.

Non-directories are mapped to LEAF at once. This makes the isLeaf() implementation simpler.

    public void valueForPathChanged(TreePath path, Object value)
    {
    }

This model doesn't (in fact can't) handle editing of File names because that would require replacing a node (File) by another, something TreeModel does not properly support.

    public void addTreeModelListener(TreeModelListener l)
    {
        listeners.add(TreeModelListener.class, l);
    }

    public void removeTreeModelListener(TreeModelListener l)
    {
        listeners.remove(TreeModelListener.class, l);
    }

    public Object clone()
    {
        try
        {
            FileTreeModel clone = (FileTreeModel)super.clone();
   
            clone.listeners = new EventListenerList();
   
            clone.map = new HashMap(map);
   
            return clone;
        }
        catch (CloneNotSupportedException e)
        {
            throw new InternalError();
        }
    }

Setting the clone's map to a new map isn't really needed because it is only internally modified. But in general, mutable data structures should not be shared between the clone and its original.

    public static void main(String[] args)
    {
        if (args.length != 1)
        {
            System.err.println("Usage: java FileTreeModel path");
            System.exit(1);
        }

        File root = new File(args[0]);

        if (!root.exists())
        {
            System.err.println(root+ ": No such file or directory");
            System.exit(2);
        }

        JTree tree = new JTree(new FileTreeModel(root));

        JFrame f = new JFrame(root.toString());

        f.addWindowListener(new WindowAdapter()
        {
            public void windowClosing(WindowEvent e)
            {
                System.exit(0);
            }
        });

        f.getContentPane().add(new JScrollPane(tree));

        f.pack();
        f.setVisible(true);
    }
}

Further notes:

There is a case where a FileTreeModel will contain infinite paths (and infinitely many nodes): circular symbolic links (if the OS has them).

In real-world code, I wouldn't use File objects if (as in the famous file manager) the node (file) names should be editable. TreeModel doesn't supporting replacing properly. Wrapper objects around the Files can be used to replace a File by another File without destroying too much information in TreeModelListener states.


(C) 2001-2009 Christian Kaufhold (swing@chka.de)