
de.schlichtherle.io.swing.tree.FileTreeModel Maven / Gradle / Ivy
/*
* Copyright (C) 2006-2010 Schlichtherle IT Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.schlichtherle.io.swing.tree;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.swing.event.*;
import javax.swing.tree.*;
/**
* A {@link TreeModel} which traverses {@link java.io.File java.io.File}
* instances.
* If the root of this tree model is actually an instance of
* {@link de.schlichtherle.io.File de.schlichtherle.io.File}, its archive
* detector is used to detect any archive files in the directory tree.
* This allows you to traverse archive files just like directories.
*
* @author Christian Schlichtherle
* @version $Id: FileTreeModel.java,v 1.4 2010/08/20 13:09:49 christian_schlichtherle Exp $
* @since TrueZIP 5.1
*/
public class FileTreeModel implements TreeModel {
/**
* A collator for file names which considers case according to the
* platform's standard.
*/
private static final Collator collator = Collator.getInstance();
static {
// Set minimum requirements for maximum performance.
collator.setDecomposition(Collator.NO_DECOMPOSITION);
collator.setStrength(java.io.File.separatorChar == '\\'
? Collator.SECONDARY
: Collator.TERTIARY);
}
/** A comparator which sorts directory entries to the beginning. */
public static final Comparator FILE_NAME_COMPARATOR = new Comparator() {
public final int compare(Object o1, Object o2) {
return compare((java.io.File) o1, (java.io.File) o2);
}
public int compare(java.io.File f1, java.io.File f2) {
if (f1.isDirectory())
return f2.isDirectory()
? collator.compare(f1.getName(), f2.getName())
: -1;
else
return f2.isDirectory()
? 1
: collator.compare(f1.getName(), f2.getName());
}
};
/**
* Used to cache the contents of directories.
* Maps {@link java.io.File} -> {@link java.io.File}[] instances.
*/
// Tactical note: Working with a WeakHashMap shows strange results.
private final Map cache = new HashMap();
private final java.io.File root;
private final FileFilter filter;
/** A comparator for {@code java.io.File} or super classes. */
private final Comparator comparator;
private final EventListenerList listeners = new EventListenerList();
/**
* Equivalent to {@link #FileTreeModel(java.io.File, FileFilter, Comparator)
* FileTreeModel(null, null, FILE_NAME_COMPARATOR)}.
* This constructor isn't particularly useful - it's only provided to
* implement the JavaBean pattern.
*/
public FileTreeModel() {
this(null, null, FILE_NAME_COMPARATOR);
}
/**
* Equivalent to {@link #FileTreeModel(java.io.File, FileFilter, Comparator)
* FileTreeModel(root, null, FILE_NAME_COMPARATOR)}.
*/
public FileTreeModel(final java.io.File root) {
this(root, null, FILE_NAME_COMPARATOR);
}
/**
* Equivalent to {@link #FileTreeModel(java.io.File, FileFilter, Comparator)
* FileTreeModel(root, filter, FILE_NAME_COMPARATOR)}.
*/
public FileTreeModel(
final java.io.File root,
final FileFilter filter) {
this(root, filter, FILE_NAME_COMPARATOR);
}
/**
* Creates a new {@code FileTreeModel} which browses the specified
* {@code root} file.
* If {@code file} is an instance of {@link de.schlichtherle.io.File},
* its archive detector is used to detect any archive files in the
* directory tree.
*
* @param root The root of this {@code FileTreeModel}.
* If this is {@code null}, an empty tree is created.
* @param filter Used to filter the files and directories which are
* present in this {@code TreeModel}.
* If this is {@code null}, all files are accepted.
* @param comparator A comparator for {@code java.io.File} instances
* or super classes.
* This must not be {@code null}.
* @throws NullPointerException If {@code comparator} is {@code null}.
* @throws IllegalArgumentException If {@code root} isn't
* {@code null} and comparing it to itself didn't result in
* {@code 0}.
* @throws ClassCastException If {@code root} isn't
* {@code null} and {@code comparator} isn't a
* {@code Comparator} for {@code java.io.File} or super
* class instances.
* @since TrueZIP 6.5.4 (this constructor)
*/
public FileTreeModel(
final java.io.File root,
final FileFilter filter,
final Comparator comparator) {
if (comparator == null)
throw new NullPointerException();
if (root != null && comparator.compare(root, root) != 0)
throw new IllegalArgumentException();
this.root = root;
this.filter = filter;
this.comparator = comparator;
}
//
// TreeModel implementation.
//
/**
* Returns the root of this tree model.
* This is actually an instance of {@link java.io.File java.io.File} or
* a subclass, like
* {@link de.schlichtherle.io.File de.schlichtherle.io.File}.
*
* @return A {@code File} object or {@code null} if this tree
* model has not been created with a {@code File} object.
*/
public Object getRoot() {
return root;
}
public Object getChild(Object parent, int index) {
final java.io.File[] children = getChildren((java.io.File) parent);
return children != null ? children[index] : null;
}
public int getChildCount(Object parent) {
final java.io.File[] children = getChildren((java.io.File) parent);
return children != null ? children.length : 0;
}
public boolean isLeaf(Object node) {
return !((java.io.File) node).isDirectory();
}
public void valueForPathChanged(TreePath path, Object newValue) {
}
public int getIndexOfChild(Object parent, Object child) {
if (parent == null || child == null)
return -1;
final java.io.File[] children = getChildren((java.io.File) parent);
if (children == null)
return -1;
for (int i = 0, l = children.length; i < l; i++)
if (children[i].equals(child))
return i;
return -1;
}
private java.io.File[] getChildren(final java.io.File parent) {
assert parent != null;
java.io.File[] children = (java.io.File[]) cache.get(parent);
if (children == null) {
if (cache.containsKey(parent))
return null; // parent is file or inaccessible directory
children = parent.listFiles(filter);
// Order is important here: FILE_NAME_COMPARATOR causes a
// recursion if the children contain an RAES encrypted ZIP file
// for which a password needs to be prompted.
// This is caused by the painting manager which repaints the tree
// model in the background while the prompting dialog is showing
// in the foreground.
// In this case, we will simply return the unsorted result in the
// recursion, which is then used for repainting.
cache.put(parent, children);
if (children != null)
Arrays.sort(children, FILE_NAME_COMPARATOR);
}
return children;
}
//
// TreePath retrieval.
//
/**
* Forwards the call to {@link #createTreePath}.
*
* @deprecated Use {@link #createTreePath} instead.
*/
public TreePath getTreePath(java.io.File node) {
return createTreePath(node);
}
/**
* Returns a {@link TreePath} for the given {@code node} or
* {@code null} if the node is not part of this file tree.
*/
public TreePath createTreePath(java.io.File node) {
java.io.File[] elements = createPath(node);
return elements != null ? new TreePath(elements) : null;
}
/**
* Returns an array of {@link java.io.File} objects indicating the path
* from the root to the given node.
*
* @param node The {@code File} object to get the path for.
* @return An array of {@code File} objects, suitable as a constructor
* argument for {@link TreePath} or {@code null} if
* {@code node} is not part of this file tree.
*/
private java.io.File[] createPath(java.io.File node) {
if (root == null /*|| !de.schlichtherle.io.File.contains(root, node)*/)
return null;
// Do not apply the filter here! The filter could depend on the file's
// state and this method may get called before the node is initialized
// to a state which would be accepted by the filter.
/*if (filter != null && !((FileFilter) filter).accept(node))
return null;*/
return createPath(node, 1);
}
private java.io.File[] createPath(final java.io.File node, int level) {
assert root != null; // FindBugs
final java.io.File[] path;
if (/*node == null ||*/ root.equals(node)) {
path = new java.io.File[level];
path[0] = root;
} else if (node != null) {
path = createPath(node.getParentFile(), level + 1);
if (path != null)
path[path.length - level] = node;
} else {
path = null;
}
return path;
}
//
// File system operations.
//
/**
* Creates {@code node} as a new file in the file system
* and updates the tree accordingly.
* However, the current selection may get lost.
* If you would like to create a new file with initial content, please
* use {@link #copyFrom(de.schlichtherle.io.File, InputStream)}.
*
* @return Whether or not the file has been newly created.
* @throws IOException If an I/O error occurs.
*/
public boolean createNewFile(final java.io.File node)
throws IOException {
if (!node.createNewFile())
return false;
nodeInserted(node);
return true;
}
/**
* Creates {@code node} as a new directory
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been newly created.
* @throws IOException If an I/O error occurs.
*/
public boolean mkdir(final java.io.File node) {
if (!node.mkdir())
return false;
nodeInserted(node);
return true;
}
/**
* Creates {@code node} as a new directory, including all parents,
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been newly created.
* @throws IOException If an I/O error occurs.
*/
public boolean mkdirs(final java.io.File node) {
if (!node.mkdirs())
return false;
nodeInserted(node);
return true;
}
/**
* Creates {@code node} as a new file with the contents read from
* {@code in} and updates the tree accordingly.
* However, the current selection may get lost.
* Note that the given stream is always closed.
*
* @return Whether or not the file has been newly created.
* @throws IOException If an I/O error occurs.
*/
public boolean copyFrom(final de.schlichtherle.io.File node, final InputStream in) {
if (!node.copyFrom(in))
return false;
nodeInsertedOrStructureChanged(node);
return true;
}
/**
* Copies {@code oldNode} to {@code node}
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been successfully renamed.
*/
public boolean copyTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
if (!oldNode.copyTo(node))
return false;
nodeInsertedOrStructureChanged(node);
return true;
}
/**
* Copies {@code oldNode} to {@code node} recursively
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been successfully renamed.
*/
public boolean copyAllTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
final boolean ok = oldNode.copyAllTo(node);
nodeInsertedOrStructureChanged(node);
return ok;
}
/**
* Copies {@code oldNode} to {@code node}, preserving
* its last modification time
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been successfully renamed.
*/
public boolean archiveCopyTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
if (!oldNode.archiveCopyTo(node))
return false;
nodeInsertedOrStructureChanged(node);
return true;
}
/**
* Copies {@code oldNode} to {@code node} recursively, preserving
* its last modification time
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been successfully renamed.
*/
public boolean archiveCopyAllTo(final de.schlichtherle.io.File oldNode, final java.io.File node) {
final boolean ok = oldNode.archiveCopyAllTo(node);
nodeInsertedOrStructureChanged(node);
return ok;
}
/**
* Renames {@code oldNode} to {@code newNode}
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file has been successfully renamed.
*/
public boolean renameTo(
final java.io.File oldNode,
final java.io.File newNode) {
if (!oldNode.renameTo(newNode))
return false;
nodeRemoved(oldNode);
nodeInserted(newNode);
return true;
}
/**
* Deletes the file or empty directory {@code node}
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file or directory has been successfully deleted.
* @throws IOException If an I/O error occurs.
*/
public boolean delete(final java.io.File node) {
if (!node.delete())
return false;
nodeRemoved(node);
return true;
}
/**
* Deletes the file or (probably not empty) directory {@code node}
* and updates the tree accordingly.
* However, the current selection may get lost.
*
* @return Whether or not the file or directory has been successfully deleted.
* @throws IOException If an I/O error occurs.
*/
public boolean deleteAll(final de.schlichtherle.io.File node) {
if (!node.deleteAll())
return false;
nodeRemoved(node);
return true;
}
//
// File system change notifications.
//
/**
* Inserts the given node in the tree or reloads the tree structure for
* the given node if it already exists.
* This method calls {@link TreeModelListener#treeNodesInserted(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
*/
public void nodeInsertedOrStructureChanged(final java.io.File node) {
if (node == null)
throw new NullPointerException();
if (cache.containsKey(node))
structureChanged(node);
else
nodeInserted(node);
}
/**
* Inserts the given node in the tree.
* If {@code node} already exists, nothing happens.
* This method calls {@link TreeModelListener#treeNodesInserted(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
*/
public void nodeInserted(final java.io.File node) {
if (cache.containsKey(node))
return;
final java.io.File parent = node.getParentFile();
forget(parent, false);
final int index = getIndexOfChild(parent, node); // new index
if (index == -1)
return;
fireTreeNodesInserted(new TreeModelEvent(
this, createTreePath(parent),
new int[] { index }, new java.io.File[] { node }));
}
/**
* Updates the given node in the tree.
* This method calls {@link TreeModelListener#treeNodesChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
*/
public void nodeChanged(final java.io.File node) {
final java.io.File parent = node.getParentFile();
final int index = getIndexOfChild(parent, node); // old index
if (index == -1)
return;
fireTreeNodesChanged(new TreeModelEvent(
this, createTreePath(parent),
new int[] { index }, new java.io.File[] { node }));
}
/**
* Removes the given node from the tree.
* This method calls {@link TreeModelListener#treeNodesRemoved(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
*/
public void nodeRemoved(final java.io.File node) {
final java.io.File parent = node.getParentFile();
final int index = getIndexOfChild(parent, node); // old index
if (index == -1)
return;
forget(node, true);
forget(parent, false);
// Fill cache again so that subsequent removes don't suffer a cache miss.
// Otherwise, the display wouldn't mirror the cache anymore.
getChildren(parent);
fireTreeNodesRemoved(new TreeModelEvent(
this, createTreePath(parent),
new int[] { index }, new java.io.File[] { node }));
}
/**
* Refreshes the tree structure for the entire tree.
* This method calls {@link TreeModelListener#treeStructureChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
*/
public void refresh() {
cache.clear();
if (root != null)
fireTreeStructureChanged(
new TreeModelEvent(this, createTreePath(root), null, null));
}
/** Alias for {@link #structureChanged(java.io.File)}. */
public final void refresh(final java.io.File node) {
structureChanged(node);
}
/**
* Reloads the tree structure for the given node.
* This method calls {@link TreeModelListener#treeStructureChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
*/
public void structureChanged(final java.io.File node) {
if (node == null)
throw new NullPointerException();
forget(node);
fireTreeStructureChanged(
new TreeModelEvent(this, createTreePath(node), null, null));
}
/**
* Clears the internal cache associated with {@code node} and all
* of its children.
*
* @deprecated This method is only public in order to make it available to
* {@link de.schlichtherle.io.swing.JFileTree}
* - it is not intended for public use.
* In particular, this method does not notify the
* tree of any structural changes in the file system.
*/
public final void forget(final java.io.File node) {
forget(node, true);
}
/**
* Clears the internal cache associated with {@code node}.
*
* @param childrenToo If and only if {@code true}, the internal
* cache for all children is cleared, too.
*/
private void forget(
final java.io.File node,
final boolean childrenToo) {
final java.io.File[] children = (java.io.File[]) cache.remove(node);
if (children != null && childrenToo)
for (int i = 0, l = children.length; i < l; i++)
forget(children[i], childrenToo);
}
//
// Event handling.
//
/**
* Adds a listener to this model.
*
* @param l The listener to add.
*/
public void addTreeModelListener(TreeModelListener l) {
listeners.add(TreeModelListener.class, l);
}
/**
* Removes a listener from this model.
*
* @param l The listener to remove.
*/
public void removeTreeModelListener(TreeModelListener l) {
listeners.remove(TreeModelListener.class, l);
}
/**
* This method calls {@link TreeModelListener#treeStructureChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
* May be used to tell the listeners about a change in the file system.
*/
protected void fireTreeNodesChanged(final TreeModelEvent evt) {
final EventListener[] l = listeners.getListeners(TreeModelListener.class);
for (int i = 0, ll = l.length; i < ll; i++)
((TreeModelListener) l[i]).treeNodesChanged(evt);
}
/**
* This method calls {@link TreeModelListener#treeStructureChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
* May be used to tell the listeners about a change in the file system.
*/
protected void fireTreeNodesInserted(final TreeModelEvent evt) {
final EventListener[] l = listeners.getListeners(TreeModelListener.class);
for (int i = 0, ll = l.length; i < ll; i++)
((TreeModelListener) l[i]).treeNodesInserted(evt);
}
/**
* This method calls {@link TreeModelListener#treeStructureChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
* May be used to tell the listeners about a change in the file system.
*/
protected void fireTreeNodesRemoved(final TreeModelEvent evt) {
final EventListener[] l = listeners.getListeners(TreeModelListener.class);
for (int i = 0, ll = l.length; i < ll; i++)
((TreeModelListener) l[i]).treeNodesRemoved(evt);
}
/**
* This method calls {@link TreeModelListener#treeStructureChanged(TreeModelEvent)}
* on all listeners of this {@code TreeModel}.
* May be used to tell the listeners about a change in the file system.
*/
protected void fireTreeStructureChanged(final TreeModelEvent evt) {
final EventListener[] l = listeners.getListeners(TreeModelListener.class);
for (int i = 0, ll = l.length; i < ll; i++)
((TreeModelListener) l[i]).treeStructureChanged(evt);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy