/*************************************************** * * cismet GmbH, Saarbruecken, Germany * * ... and it just works. * ****************************************************/ package de.cismet.tools.gui.treetable; /* * FileSystemModel2.java * * Copyright (c) 1998 Sun Microsystems, Inc. All Rights Reserved. * * This software is the confidential and proprietary information of Sun * Microsystems, Inc. ("Confidential Information"). You shall not * disclose such Confidential Information and shall use it only in * accordance with the terms of the license agreement you entered into * with Sun. * * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING * THIS SOFTWARE OR ITS DERIVATIVES. * */ 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; /** * 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> * * <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> * * <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> * * <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.</p> * * @author Philip Milne * @author Scott Violet * @version 1.12 05/12/98 */ public class FileSystemModel2 extends AbstractTreeTableModel { //~ Static fields/initializers --------------------------------------------- // Names of the columns. protected static String[] cNames = { "Name", "Size", "Type", "Modified" }; // NOI18N // Types of the columns. protected static 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(); protected static FileNode[] EMPTY_CHILDREN = new FileNode[0]; // Used to sort the file names. private static MergeSort fileMS = new MergeSort() { @Override public int compareElementsAt(final int beginLoc, final int endLoc) { return ((String)toSort[beginLoc]).compareTo((String)toSort[endLoc]); } }; //~ Instance fields -------------------------------------------------------- /** 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; /** Returns true if links are to be descended. */ protected boolean descendLinks; /** > 0 indicates reloading some nodes. */ int reloadCount; //~ Constructors ----------------------------------------------------------- /** * 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. * * @param rootPath DOCUMENT ME! */ public FileSystemModel2(final String rootPath) { super(null); isValid = true; root = new FileNode(new File(rootPath)); } //~ Methods ---------------------------------------------------------------- /** * Returns a MergeSort that can sort on the totalSize of a FileNode. * * @return DOCUMENT ME! */ 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. * * @param sorter DOCUMENT ME! */ protected static void recycleSorter(final MergeSort sorter) { synchronized (sorters) { sorters.push(sorter); } } // // The TreeModel interface // /** * Returns the number of children of <code>node</code>. * * @param node DOCUMENT ME! * * @return DOCUMENT ME! */ @Override public int getChildCount(final Object node) { final Object[] children = getChildren(node); return (children == null) ? 0 : children.length; } /** * Returns the child of <code>node</code> at index <code>i</code>. * * @param node DOCUMENT ME! * @param i DOCUMENT ME! * * @return DOCUMENT ME! */ @Override public Object getChild(final Object node, final int i) { return getChildren(node)[i]; } /** * Returns true if the passed in object represents a leaf, false otherwise. * * @param node DOCUMENT ME! * * @return DOCUMENT ME! */ @Override public boolean isLeaf(final Object node) { return ((FileNode)node).isLeaf(); } // // The TreeTableNode interface. // /** * Returns the number of columns. * * @return DOCUMENT ME! */ @Override public int getColumnCount() { return cNames.length; } /** * Returns the name for a particular column. * * @param column DOCUMENT ME! * * @return DOCUMENT ME! */ @Override public String getColumnName(final int column) { return cNames[column]; } /** * Returns the class for the particular column. * * @param column DOCUMENT ME! * * @return DOCUMENT ME! */ @Override public Class getColumnClass(final int column) { return cTypes[column]; } /** * Returns the value of the particular column. * * @param node DOCUMENT ME! * @param column DOCUMENT ME! * * @return DOCUMENT ME! */ @Override public Object getValueAt(final Object node, final int column) { final 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"; // NOI18N } case 3: { return fn.lastModified(); } } } catch (SecurityException se) { } return null; } // // Some convenience methods. // /** * Reloads the children of the specified node. * * @param node DOCUMENT ME! */ public void reloadChildren(final Object node) { final 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. * * @param newValue DOCUMENT ME! */ public void setDescendsLinks(final boolean newValue) { descendLinks = newValue; } /** * Returns true if links are to be automatically descended. * * @return DOCUMENT ME! */ public boolean getDescendsLinks() { return descendLinks; } /** * Returns the path <code>node</code> represents. * * @param node DOCUMENT ME! * * @return DOCUMENT ME! */ public String getPath(final Object node) { return ((FileNode)node).getFile().getPath(); } /** * Returns the total size of the receiver. * * @param node DOCUMENT ME! * * @return DOCUMENT ME! */ public long getTotalSize(final Object node) { return ((FileNode)node).totalSize(); } /** * Returns true if the receiver is loading any children. * * @return DOCUMENT ME! */ public boolean isReloading() { return (reloadCount > 0); } /** * Returns the path to the node that is being loaded. * * @return DOCUMENT ME! */ public TreePath getPathLoading() { final FileNode rn = reloadNode; if (rn != null) { return new TreePath(rn.getPath()); } return null; } /** * Returns the node being loaded. * * @return DOCUMENT ME! */ public Object getNodeLoading() { return reloadNode; } /** * DOCUMENT ME! * * @param node DOCUMENT ME! * * @return DOCUMENT ME! */ protected File getFile(final Object node) { final FileNode fileNode = ((FileNode)node); return fileNode.getFile(); } /** * DOCUMENT ME! * * @param node DOCUMENT ME! * * @return DOCUMENT ME! */ protected Object[] getChildren(final Object node) { final FileNode fileNode = ((FileNode)node); return fileNode.getChildren(); } //~ Inner Classes ---------------------------------------------------------- /** * 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. * * @version $Revision$, $Date$ */ class FileNode { //~ Instance fields ---------------------------------------------------- /** java.io.File the receiver represents. */ protected File file; /** 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; /** Parent FileNode of the receiver. */ private FileNode parent; //~ Constructors ------------------------------------------------------- /** * Creates a new FileNode object. * * @param file DOCUMENT ME! */ protected FileNode(final File file) { this(null, file); } /** * Creates a new FileNode object. * * @param parent DOCUMENT ME! * @param file DOCUMENT ME! */ protected FileNode(final FileNode parent, final File file) { this.parent = parent; this.file = file; try { canonicalPath = file.getCanonicalPath(); } catch (IOException ioe) { canonicalPath = ""; // NOI18N } if (parent != null) { isLink = !canonicalPath.startsWith(parent.getCanonicalPath()); } else { isLink = false; } if (isLeaf()) { totalSize = file.length(); totalSizeValid = true; } } //~ Methods ------------------------------------------------------------ /** * Returns the date the receiver was last modified. * * @return DOCUMENT ME! */ 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. * * @return DOCUMENT ME! */ @Override public String toString() { return file.getName(); } /** * Returns the java.io.File the receiver represents. * * @return DOCUMENT ME! */ public File getFile() { return file; } /** * Returns size of the receiver and all its children. * * @return DOCUMENT ME! */ public long totalSize() { return totalSize; } /** * Returns the parent of the receiver. * * @return DOCUMENT ME! */ public FileNode getParent() { return parent; } /** * Returns true if the receiver represents a leaf, that is it is isn't a directory. * * @return DOCUMENT ME! */ public boolean isLeaf() { return file.isFile(); } /** * Returns true if the total size is valid. * * @return DOCUMENT ME! */ 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. * * @return DOCUMENT ME! */ protected FileNode[] getChildren() { return children; } /** * Recursively loads all the children of the receiver. * * @param sorter DOCUMENT ME! */ protected void loadChildren(final 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. * * @param sorter DOCUMENT ME! * * @return DOCUMENT ME! */ protected FileNode[] createChildren(final MergeSort sorter) { FileNode[] retArray = null; try { final String[] files = file.list(); if (files != null) { if (sorter != null) { sorter.sort(files); } retArray = new FileNode[files.length]; final String path = file.getPath(); for (int i = 0; i < files.length; i++) { final 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. * * @return DOCUMENT ME! */ protected boolean loadedChildren() { return (file.isFile() || (children != null)); } /** * Gets the path from the root to the receiver. * * @return DOCUMENT ME! */ public FileNode[] getPath() { return getPathToRoot(this, 0); } /** * Returns the canonical path for the receiver. * * @return DOCUMENT ME! */ public String getCanonicalPath() { return canonicalPath; } /** * Returns true if the receiver's path does not begin with the parent's canonical path. * * @return DOCUMENT ME! */ public boolean isLink() { return isLink; } /** * DOCUMENT ME! * * @param aNode DOCUMENT ME! * @param depth DOCUMENT ME! * * @return DOCUMENT ME! */ protected FileNode[] getPathToRoot(final 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. * * @param newChildren DOCUMENT ME! * @param generateEvent DOCUMENT ME! */ protected void setChildren(final FileNode[] newChildren, final boolean generateEvent) { final long oldSize = totalSize; totalSize = file.length(); children = newChildren; for (int counter = children.length - 1; counter >= 0; counter--) { totalSize += children[counter].totalSize(); } if (generateEvent) { final FileNode[] path = getPath(); fireTreeStructureChanged(FileSystemModel2.this, path, null, null); final FileNode parent = getParent(); if (parent != null) { parent.alterTotalSize(totalSize - oldSize); } } } /** * DOCUMENT ME! * * @param sizeDelta DOCUMENT ME! */ protected synchronized void alterTotalSize(final 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. * * @param newValue DOCUMENT ME! */ protected synchronized void setTotalSizeValid(final boolean newValue) { if (totalSizeValid != newValue) { nodeChanged(); totalSizeValid = newValue; final 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. * * @param child DOCUMENT ME! */ protected synchronized void childTotalSizeChanged(final FileNode child) { if (totalSizeValid != child.isTotalSizeValid()) { if (totalSizeValid) { setTotalSizeValid(false); } else { final 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() { final FileNode parent = getParent(); if (parent != null) { final FileNode[] path = parent.getPath(); final int[] index = { getIndexOfChild(parent, this) }; final 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. * * @version $Revision$, $Date$ */ class FileNodeLoader implements Runnable { //~ Instance fields ---------------------------------------------------- /** Node creating children for. */ FileNode node; /** Sorter. */ MergeSort sizeMS; //~ Constructors ------------------------------------------------------- /** * Creates a new FileNodeLoader object. * * @param node DOCUMENT ME! */ FileNodeLoader(final FileNode node) { this.node = node; node.resetLastModified(); node.setChildren(node.createChildren(fileMS), true); node.setTotalSizeValid(false); } //~ Methods ------------------------------------------------------------ @Override public void run() { final 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() { @Override public void run() { final 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(); } } } /** * DOCUMENT ME! * * @param node DOCUMENT ME! */ protected void loadChildren(final 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() { @Override public void run() { final 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. * * @version $Revision$, $Date$ */ static class SizeSorter extends MergeSort { //~ Methods ------------------------------------------------------------ @Override public int compareElementsAt(final int beginLoc, final int endLoc) { final long firstSize = ((FileNode)toSort[beginLoc]).totalSize(); final long secondSize = ((FileNode)toSort[endLoc]).totalSize(); if (firstSize != secondSize) { return (int)(secondSize - firstSize); } return ((FileNode)toSort[beginLoc]).toString().compareTo(((FileNode)toSort[endLoc]).toString()); } } }