package org.freehep.swing.treetable.test; import java.io.File; import java.io.IOException; import java.util.Date; import java.util.Stack; import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; import org.freehep.swing.treetable.AbstractTreeTableModel; import org.freehep.swing.treetable.TreeTableModel; /** * FileSystemModel2 is a TreeTableModel representing a hierarchical file * system.<p> * This will recursively load all the children from the path it is * created with. The loading is done with the method reloadChildren, and * happens in another thread. The method isReloading can be invoked to check * if there are active threads. The total size of all the files are also * accumulated. * <p> * By default links are not descended. java.io.File does not have a way * to distinguish links, so a file is assumed to be a link if its canonical * path does not start with its parent path. This may not cover all cases, * but works for the time being. * <p>Reloading happens such that all the files of the directory are * loaded and immediately available. The background thread then recursively * loads all the files of each of those directories. When each directory has * finished loading all its sub files they are attached and an event is * generated in the event dispatching thread. A more ambitious approach * would be to attach each set of directories as they are loaded and generate * an event. Then, once all the direct descendants of the directory being * reloaded have finished loading, it is resorted based on total size. * <p> * While you can invoke reloadChildren as many times as you want, care * should be taken in doing this. You should not invoke reloadChildren * on a node that is already being reloaded, or going to be reloaded * (meaning its parent is reloading but it hasn't started reloading * that directory yet). If this is done odd results may * happen. FileSystemModel2 does not enforce any policy in this manner, * and it is up to the user of FileSystemModel2 to ensure it doesn't * happen. * * @version 1.12 05/12/98 * @author Philip Milne * @author Scott Violet */ public class FileSystemModel2 extends AbstractTreeTableModel { // Names of the columns. static protected String[] cNames = {"Name", "Size", "Type", "Modified"}; // Types of the columns. static protected Class[] cTypes = { TreeTableModel.class, Integer.class, String.class, Date.class}; // The the returned file length for directories. public static final Integer ZERO = new Integer(0); /** An array of MergeSorter sorters, that will sort based on size. */ static Stack sorters = new Stack(); /** True if the receiver is valid, once set to false all Threads * loading files will stop. */ protected boolean isValid; /** Node currently being reloaded, this becomes somewhat muddy if * reloading is happening in multiple threads. */ protected FileNode reloadNode; /** > 0 indicates reloading some nodes. */ int reloadCount; /** Returns true if links are to be descended. */ protected boolean descendLinks; /** * Returns a MergeSort that can sort on the totalSize of a FileNode. */ protected static MergeSort getSizeSorter() { synchronized(sorters) { if (sorters.size() == 0) { return new SizeSorter(); } return (MergeSort)sorters.pop(); } } /** * Should be invoked when a MergeSort is no longer needed. */ protected static void recycleSorter(MergeSort sorter) { synchronized(sorters) { sorters.push(sorter); } } /** * Creates a FileSystemModel2 rooted at File.separator, which is usually * the root of the file system. This does not load it, you should invoke * <code>reloadChildren</code> with the root to start loading. */ public FileSystemModel2() { this(File.separator); } /** * Creates a FileSystemModel2 with the root being <code>rootPath</code>. * This does not load it, you should invoke * <code>reloadChildren</code> with the root to start loading. */ public FileSystemModel2(String rootPath) { super(null); isValid = true; root = new FileNode(new File(rootPath)); } // // The TreeModel interface // /** * Returns the number of children of <code>node</code>. */ public int getChildCount(Object node) { Object[] children = getChildren(node); return (children == null) ? 0 : children.length; } /** * Returns the child of <code>node</code> at index <code>i</code>. */ public Object getChild(Object node, int i) { return getChildren(node)[i]; } /** * Returns true if the passed in object represents a leaf, false * otherwise. */ public boolean isLeaf(Object node) { return ((FileNode)node).isLeaf(); } // // The TreeTableNode interface. // /** * Returns the number of columns. */ public int getColumnCount() { return cNames.length; } /** * Returns the name for a particular column. */ public String getColumnName(int column) { return cNames[column]; } /** * Returns the class for the particular column. */ public Class getColumnClass(int column) { return cTypes[column]; } /** * Returns the value of the particular column. */ public Object getValueAt(Object node, int column) { FileNode fn = (FileNode)node; try { switch(column) { case 0: return fn.getFile().getName(); case 1: if (fn.isTotalSizeValid()) { return new Integer((int)((FileNode)node).totalSize()); } return null; case 2: return fn.isLeaf() ? "File" : "Directory"; case 3: return fn.lastModified(); } } catch (SecurityException se) { } return null; } // // Some convenience methods. // /** * Reloads the children of the specified node. */ public void reloadChildren(Object node) { FileNode fn = (FileNode)node; synchronized(this) { reloadCount++; } fn.resetSize(); new Thread(new FileNodeLoader((FileNode)node)).start(); } /** * Stops and waits for all threads to finish loading. */ public void stopLoading() { isValid = false; synchronized(this) { while (reloadCount > 0) { try { wait(); } catch (InterruptedException ie) {} } } isValid = true; } /** * If <code>newValue</code> is true, links are descended. Odd results * may happen if you set this while other threads are loading. */ public void setDescendsLinks(boolean newValue) { descendLinks = newValue; } /** * Returns true if links are to be automatically descended. */ public boolean getDescendsLinks() { return descendLinks; } /** * Returns the path <code>node</code> represents. */ public String getPath(Object node) { return ((FileNode)node).getFile().getPath(); } /** * Returns the total size of the receiver. */ public long getTotalSize(Object node) { return ((FileNode)node).totalSize(); } /** * Returns true if the receiver is loading any children. */ public boolean isReloading() { return (reloadCount > 0); } /** * Returns the path to the node that is being loaded. */ public TreePath getPathLoading() { FileNode rn = reloadNode; if (rn != null) { return new TreePath(rn.getPath()); } return null; } /** * Returns the node being loaded. */ public Object getNodeLoading() { return reloadNode; } protected File getFile(Object node) { FileNode fileNode = ((FileNode)node); return fileNode.getFile(); } protected Object[] getChildren(Object node) { FileNode fileNode = ((FileNode)node); return fileNode.getChildren(); } protected static FileNode[] EMPTY_CHILDREN = new FileNode[0]; // Used to sort the file names. static private MergeSort fileMS = new MergeSort() { public int compareElementsAt(int beginLoc, int endLoc) { return ((String)toSort[beginLoc]).compareTo ((String)toSort[endLoc]); } }; /** * A FileNode is a derivative of the File class - though we delegate to * the File object rather than subclassing it. It is used to maintain a * cache of a directory's children and therefore avoid repeated access * to the underlying file system during rendering. */ class FileNode { /** java.io.File the receiver represents. */ protected File file; /** Parent FileNode of the receiver. */ private FileNode parent; /** Children of the receiver. */ protected FileNode[] children; /** Size of the receiver and all its children. */ protected long totalSize; /** Valid if the totalSize has finished being calced. */ protected boolean totalSizeValid; /** Path of the receiver. */ protected String canonicalPath; /** True if the canonicalPath of this instance does not start with * the canonical path of the parent. */ protected boolean isLink; /** Date last modified. */ protected Date lastModified; protected FileNode(File file) { this(null, file); } protected FileNode(FileNode parent, File file) { this.parent = parent; this.file = file; try { canonicalPath = file.getCanonicalPath(); } catch (IOException ioe) { canonicalPath = ""; } if (parent != null) { isLink = !canonicalPath.startsWith(parent.getCanonicalPath()); } else { isLink = false; } if (isLeaf()) { totalSize = file.length(); totalSizeValid = true; } } /** * Returns the date the receiver was last modified. */ public Date lastModified() { if (lastModified == null && file != null) { lastModified = new Date(file.lastModified()); } return lastModified; } /** * Returns the the string to be used to display this leaf in the JTree. */ public String toString() { return file.getName(); } /** * Returns the java.io.File the receiver represents. */ public File getFile() { return file; } /** * Returns size of the receiver and all its children. */ public long totalSize() { return totalSize; } /** * Returns the parent of the receiver. */ public FileNode getParent() { return parent; } /** * Returns true if the receiver represents a leaf, that is it is * isn't a directory. */ public boolean isLeaf() { return file.isFile(); } /** * Returns true if the total size is valid. */ public boolean isTotalSizeValid() { return totalSizeValid; } /** * Clears the date. */ protected void resetLastModified() { lastModified = null; } /** * Sets the size of the receiver to be 0. */ protected void resetSize() { alterTotalSize(-totalSize); } /** * Loads the children, caching the results in the children * instance variable. */ protected FileNode[] getChildren() { return children; } /** * Recursively loads all the children of the receiver. */ protected void loadChildren(MergeSort sorter) { totalSize = file.length(); children = createChildren(null); for (int counter = children.length - 1; counter >= 0; counter--) { Thread.yield(); // Give the GUI CPU time to draw itself. if (!children[counter].isLeaf() && (descendLinks || !children[counter].isLink())) { children[counter].loadChildren(sorter); } totalSize += children[counter].totalSize(); if (!isValid) { counter = 0; } } if (isValid) { if (sorter != null) { sorter.sort(children); } totalSizeValid = true; } } /** * Loads the children of of the receiver. */ protected FileNode[] createChildren(MergeSort sorter) { FileNode[] retArray = null; try { String[] files = file.list(); if(files != null) { if (sorter != null) { sorter.sort(files); } retArray = new FileNode[files.length]; String path = file.getPath(); for(int i = 0; i < files.length; i++) { File childFile = new File(path, files[i]); retArray[i] = new FileNode(this, childFile); } } } catch (SecurityException se) {} if (retArray == null) { retArray = EMPTY_CHILDREN; } return retArray; } /** * Returns true if the children have been loaded. */ protected boolean loadedChildren() { return (file.isFile() || (children != null)); } /** * Gets the path from the root to the receiver. */ public FileNode[] getPath() { return getPathToRoot(this, 0); } /** * Returns the canonical path for the receiver. */ public String getCanonicalPath() { return canonicalPath; } /** * Returns true if the receiver's path does not begin with the * parent's canonical path. */ public boolean isLink() { return isLink; } protected FileNode[] getPathToRoot(FileNode aNode, int depth) { FileNode[] retNodes; if(aNode == null) { if(depth == 0) return null; else retNodes = new FileNode[depth]; } else { depth++; retNodes = getPathToRoot(aNode.getParent(), depth); retNodes[retNodes.length - depth] = aNode; } return retNodes; } /** * Sets the children of the receiver, updates the total size, * and if generateEvent is true a tree structure changed event * is created. */ protected void setChildren(FileNode[] newChildren, boolean generateEvent) { long oldSize = totalSize; totalSize = file.length(); children = newChildren; for (int counter = children.length - 1; counter >= 0; counter--) { totalSize += children[counter].totalSize(); } if (generateEvent) { TreePath path = new TreePath(getPath()); fireTreeStructureChanged(FileSystemModel2.this, path, null, null); FileNode parent = getParent(); if (parent != null) { parent.alterTotalSize(totalSize - oldSize); } } } protected synchronized void alterTotalSize(long sizeDelta) { if (sizeDelta != 0 && (parent = getParent()) != null) { totalSize += sizeDelta; nodeChanged(); parent.alterTotalSize(sizeDelta); } else { // Need a way to specify the root. totalSize += sizeDelta; } } /** * This should only be invoked on the event dispatching thread. */ protected synchronized void setTotalSizeValid(boolean newValue) { if (totalSizeValid != newValue) { nodeChanged(); totalSizeValid = newValue; FileNode parent = getParent(); if (parent != null) { parent.childTotalSizeChanged(this); } } } /** * Marks the receivers total size as valid, but does not invoke * node changed, nor message the parent. */ protected synchronized void forceTotalSizeValid() { totalSizeValid = true; } /** * Invoked when a childs total size has changed. */ protected synchronized void childTotalSizeChanged(FileNode child) { if (totalSizeValid != child.isTotalSizeValid()) { if (totalSizeValid) { setTotalSizeValid(false); } else { FileNode[] children = getChildren(); for (int counter = children.length - 1; counter >= 0; counter--) { if (!children[counter].isTotalSizeValid()) { return; } } setTotalSizeValid(true); } } } /** * Can be invoked when a node has changed, will create the * appropriate event. */ protected void nodeChanged() { FileNode parent = getParent(); if (parent != null) { TreePath path = new TreePath(parent.getPath()); int[] index = { getIndexOfChild(parent, this) }; Object[] children = { this }; fireTreeNodesChanged(FileSystemModel2.this, path, index, children); } } } /** * FileNodeLoader can be used to reload all the children of a * particular node. It first resets the children of the FileNode * it is created with, and in its run method will reload all of * that nodes children. FileNodeLoader may not be running in the event * dispatching thread. As swing is not thread safe it is important * that we don't generate events in this thread. SwingUtilities.invokeLater * is used so that events are generated in the event dispatching thread. */ class FileNodeLoader implements Runnable { /** Node creating children for. */ FileNode node; /** Sorter. */ MergeSort sizeMS; FileNodeLoader(FileNode node) { this.node = node; node.resetLastModified(); node.setChildren(node.createChildren(fileMS), true); node.setTotalSizeValid(false); } public void run() { FileNode[] children = node.getChildren(); sizeMS = getSizeSorter(); for (int counter = children.length - 1; counter >= 0; counter--) { if (!children[counter].isLeaf()) { reloadNode = children[counter]; loadChildren(children[counter]); reloadNode = null; } if (!isValid) { counter = 0; } } recycleSorter(sizeMS); if (isValid) { SwingUtilities.invokeLater(new Runnable() { public void run() { MergeSort sorter = getSizeSorter(); sorter.sort(node.getChildren()); recycleSorter(sorter); node.setChildren(node.getChildren(), true); synchronized(FileSystemModel2.this) { reloadCount--; FileSystemModel2.this.notifyAll(); } } }); } else { synchronized(FileSystemModel2.this) { reloadCount--; FileSystemModel2.this.notifyAll(); } } } protected void loadChildren(FileNode node) { if (!node.isLeaf() && (descendLinks || !node.isLink())) { final FileNode[] children = node.createChildren(null); for (int counter = children.length - 1; counter >= 0; counter--) { if (!children[counter].isLeaf()) { if (descendLinks || !children[counter].isLink()) { children[counter].loadChildren(sizeMS); } else { children[counter].forceTotalSizeValid(); } } if (!isValid) { counter = 0; } } if (isValid) { final FileNode fn = node; // Reset the children SwingUtilities.invokeLater(new Runnable() { public void run() { MergeSort sorter = getSizeSorter(); sorter.sort(children); recycleSorter(sorter); fn.setChildren(children, true); fn.setTotalSizeValid(true); fn.nodeChanged(); } }); } } else { node.forceTotalSizeValid(); } } } /** * Sorts the contents, which must be instances of FileNode based on * totalSize. */ static class SizeSorter extends MergeSort { public int compareElementsAt(int beginLoc, int endLoc) { long firstSize = ((FileNode)toSort[beginLoc]).totalSize(); long secondSize = ((FileNode)toSort[endLoc]).totalSize(); if (firstSize != secondSize) { return (int)(secondSize - firstSize); } return ((FileNode)toSort[beginLoc]).toString().compareTo (((FileNode)toSort[endLoc]).toString()); } } }