001: /*=============================================================================
002: * Copyright Texas Instruments 2001, 2002. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package ti.chimera.fs;
020:
021: import java.util.*;
022: import java.lang.ref.*;
023: import javax.swing.tree.TreePath;
024:
025: import ti.swing.treetable.*;
026:
027: import oscript.exceptions.ProgrammingErrorException;
028: import oscript.fs.AbstractFile;
029: import oscript.fs.AbstractFileSystem;
030: import oscript.util.WorkerThread;
031:
032: /**
033: * Class that makes a TreeModel of the file system {@link oscript.fs.AbstractFileSystem}.
034: * There are some additional "add-ons" that you should use to ensure proper
035: * behavior of your tree. (XXX I should probably just add a factory method to
036: * install the TreeCellRenderer and TreeExpansionListener, and any other steps
037: * that may get added in the future....)
038: * <pre>
039: * JTree tree = new JTree( new FileSystemTreeModel( "/", null ) )
040: * tree.setCellRenderer( new FileSystemTreeModel.FileSystemTreeCellRenderer() );
041: * tree.addTreeExpansionListener( new FileSystemTreeModel.FileSystemTreeExpansionListener() );
042: * </pre>
043: *
044: * @author Rob Clark
045: * @version 0.1
046: */
047: public class FileSystemTreeModel extends AbstractTreeModel {
048: /* how it works:
049: * There is two levels of caching, which serve two purposes:
050: *
051: * 1) WatchedDirectory: there is a single instance of this per directory
052: * which is shared amongst all FileSystemTreeModel-s. This is where
053: * the actual access to the filesystem happens (from the context of a
054: * worker thread). The actual directory in the filesystem has it's
055: * timestamp periodically polled (from the worker thread), and if it
056: * changes, the worker will resolve the new children (which may be
057: * expensive), and pass the new array of children to all the
058: * FileSystemNode-s which represent the same directory.
059: *
060: * 2) FileSystemNode: this is where the children of a parent node are
061: * cached. The set of children may be smaller than the actual children
062: * in the filesystem (from WatchedDirectory), due to whatever filter
063: * may be installed. Since different FileSystemTreeModel-s may have
064: * different filters, this is not shared between filesystems.
065: */
066:
067: /**
068: * The path of the root file
069: */
070: private String rootPath;
071:
072: /**
073: * The default filter shows all files.
074: */
075: private static FileSystemTreeFilter SHOW_ALL_FILES_FILTER = new FileSystemTreeFilter() {
076:
077: public boolean accept(AbstractFile file) {
078: return true;
079: }
080:
081: };
082:
083: private FileSystemTreeFilter filter = SHOW_ALL_FILES_FILTER;
084:
085: /**
086: * Class Constructor.
087: *
088: * @param path the path to the root node of the tree
089: * @param filter the filter, or <code>null</code>
090: */
091: public FileSystemTreeModel(String path, FileSystemTreeFilter filter) {
092: this (pathToFile(path), filter);
093: }
094:
095: /**
096: * Class Constructor.
097: *
098: * @param root the root node of the tree
099: * @param filter the filter, or <code>null</code>
100: */
101: public FileSystemTreeModel(AbstractFile root,
102: FileSystemTreeFilter filter) {
103: super (new FileSystemNode(null, root));
104:
105: rootPath = root.getPath();
106:
107: // the following hacks needed, because we can't reference "this" at
108: // the time when we need to construct the root node:
109: ((FileSystemNode) getRoot()).outer = this ;
110: fsNodeTable.put(((FileSystemNode) getRoot()).getFile(),
111: getRoot());
112:
113: if (filter != null)
114: this .filter = filter;
115:
116: startWatchingNode(((FileSystemNode) getRoot()));
117: }
118:
119: public boolean isLeaf(Object node) {
120: return !((FileSystemNode) node).getFile().isDirectory();
121: }
122:
123: public Object getChild(Object parent, int idx) {
124: return ((FileSystemNode) parent).getChild(idx);
125: }
126:
127: public int getChildCount(Object parent) {
128: return ((FileSystemNode) parent).getChildCount();
129: }
130:
131: /*=======================================================================*/
132: /**
133: * A cache of file-system nodes, to minimize repeated resolve() calls...
134: * probably this caching should actually go on in AbstractFileSystem
135: * layer.
136: */
137: private Hashtable fsNodeTable = new Hashtable();
138:
139: /**
140: * Cached lookup of FileSystemNode
141: */
142: public FileSystemNode getFileSystemNode(String path) {
143: return getFileSystemNode(pathToFile(path));
144: }
145:
146: /**
147: * Cached lookup of FileSystemNode
148: */
149: public FileSystemNode getFileSystemNode(AbstractFile file) {
150: FileSystemNode fsn = (FileSystemNode) (fsNodeTable.get(file));
151:
152: if (fsn == null) {
153: fsn = new FileSystemNode(this , file);
154: fsNodeTable.put(file, fsn);
155: }
156:
157: return fsn;
158: }
159:
160: /**
161: * Given a collection of files, apply this FileSystemTreeModel-s filter,
162: * and return the result as an array of FileSystemNode
163: */
164: private FileSystemNode[] getFilteredChildren(Collection files) {
165: LinkedList fileList = new LinkedList();
166:
167: for (Iterator itr = files.iterator(); itr.hasNext();) {
168: AbstractFile file = (AbstractFile) (itr.next());
169: if (filter.accept(file))
170: fileList.add(file);
171: }
172:
173: java.util.Collections.sort(fileList, new Comparator() {
174:
175: // the following sillyness of caching f.isDirectory()
176: // is because File#isDirectory() seems to be a very
177: // expensive operation on windoze... I guess redmond
178: // never heard of caching... duh!
179: private Map isDirectoryCache = new WeakHashMap();
180:
181: private boolean isDirectory(AbstractFile f) {
182: Boolean b = (Boolean) (isDirectoryCache.get(f));
183: if (b == null) {
184: b = Boolean.valueOf(f.isDirectory());
185: isDirectoryCache.put(f, b);
186: }
187: return b.booleanValue();
188: }
189:
190: public int compare(Object obj1, Object obj2) {
191: AbstractFile f1 = (AbstractFile) obj1;
192: AbstractFile f2 = (AbstractFile) obj2;
193: if (isDirectory(f1) != isDirectory(f2))
194: return f1.isDirectory() ? -1 : 1;
195: return f1.getName().compareToIgnoreCase(f2.getName());
196: }
197:
198: });
199:
200: FileSystemNode[] children = new FileSystemNode[fileList.size()];
201:
202: int i = 0;
203: for (Iterator itr = fileList.iterator(); itr.hasNext();)
204: children[i++] = getFileSystemNode((AbstractFile) (itr
205: .next()));
206:
207: return children;
208: }
209:
210: /**
211: * Filter which controls what files are visible in the model.
212: */
213: public interface FileSystemTreeFilter {
214: /**
215: * Test whether the file is accepted by this filter.
216: */
217: public boolean accept(AbstractFile file);
218: }
219:
220: private static final FileSystemNode[] INITIAL_CHILDREN = new FileSystemNode[0];
221: private static final int NODES_CHANGED = 0;
222: private static final int NODES_REMOVED = 1;
223: private static final int NODES_INSERTED = 2;
224: private static final int STRUCTURE_CHANGED = 3;
225:
226: /**
227: * Because the AbstractFileSystem.children() can be expensive, we cache
228: * the results in the FileSystemNode.
229: */
230: public static class FileSystemNode {
231: private AbstractFile file;
232: private FileSystemNode[] children = INITIAL_CHILDREN;
233: private FileSystemTreeModel outer;
234: private long lastModified = -1;
235: private long length = -1;
236:
237: FileSystemNode(FileSystemTreeModel outer, AbstractFile file) {
238: this .outer = outer;
239: this .file = file;
240: }
241:
242: /**
243: * Access this node's parent.
244: */
245: public FileSystemNode getParent() {
246: String path = file.getPath();
247: if ((path.indexOf(outer.rootPath) == 0)
248: && (path.length() > outer.rootPath.length()))
249: return outer.getFileSystemNode(path.substring(0, path
250: .lastIndexOf('/')));
251: return null;
252: }
253:
254: /**
255: * Get the file represented by this node.
256: */
257: public AbstractFile getFile() {
258: return file;
259: }
260:
261: /**
262: * Return <code>true</code> if we are still waiting on the worker thread to
263: * know the children of this node, or any parent of this node.
264: */
265: public boolean pending() {
266: if (children == INITIAL_CHILDREN)
267: return true;
268:
269: FileSystemNode fsn = getParent();
270: if (fsn != null)
271: return fsn.pending();
272:
273: return false;
274: }
275:
276: /**
277: * Get the length of the file. If this info is needed from the AWT thread,
278: * this should be called instead of <code>getFile().length()</code>, to
279: * avoid potentially expensive filesystem access from the AWT thread. If
280: * this method returns <code>-1</code>, that means that the length is not
281: * known yet. In this case, a tree event will be fired once it is known.
282: */
283: public long length() {
284: if (length == -1)
285: asyncLoadAttributes();
286: return length;
287: }
288:
289: /**
290: * Get the modification time of the file. If this info is needed from the
291: * AWT thread, this should be called instead of <code>getFile().lastModified()</code>,
292: * to avoid potentially expensive filesystem access from the AWT thread. If
293: * this method returns <code>-1</code>, that means that the modification
294: * time is not known yet. In this case, a tree event will be fired once it
295: * is known.
296: */
297: public long lastModified() {
298: if (lastModified == -1)
299: asyncLoadAttributes();
300: return lastModified;
301: }
302:
303: private void asyncLoadAttributes() {
304: WorkerThread.invokeLater(new Runnable() {
305: public void run() {
306: fileChanged(null);
307: }
308: });
309: }
310:
311: /**
312: * Get the number of children
313: */
314: public int getChildCount() {
315: return children.length;
316: }
317:
318: /**
319: * Get the child at the specified index (0..getChildCount()-1)
320: * @param idx the child index
321: * @return the child node
322: */
323: public FileSystemNode getChild(int idx) {
324: return children[idx];
325: }
326:
327: private void updateTree(final int type,
328: final FileSystemNode[] newChildren,
329: final int[] childIdxs, final Object[] children) {
330: final Object[] path = outer.getTreePath(file).getPath();
331: javax.swing.SwingUtilities.invokeLater(new Runnable() {
332:
333: public void run() {
334: if (newChildren != null) {
335: boolean wasPending = pending();
336: FileSystemNode.this .children = newChildren;
337: if (wasPending)
338: outer.handlePendingExpansions();
339: }
340:
341: switch (type) {
342: case NODES_CHANGED:
343: outer.fireTreeNodesChanged(FileSystemNode.this ,
344: path, childIdxs, children);
345: break;
346: case NODES_REMOVED:
347: outer.fireTreeNodesRemoved(FileSystemNode.this ,
348: path, childIdxs, children);
349: break;
350: case NODES_INSERTED:
351: outer.fireTreeNodesInserted(
352: FileSystemNode.this , path, childIdxs,
353: children);
354: break;
355: case STRUCTURE_CHANGED:
356: outer.fireTreeStructureChanged(
357: FileSystemNode.this , path, childIdxs,
358: children);
359: break;
360: default:
361: throw new ProgrammingErrorException(
362: "bad type: " + type);
363: }
364: }
365: });
366: }
367:
368: /**
369: * Called from a worker thread, when it is detected that the file associated
370: * with this node has changed.
371: * @param files the children of this directory, or <code>null</code.
372: */
373: void fileChanged(Collection files) {
374: boolean changedAttr = false;
375: long newLastModified = file.lastModified();
376: long newLength = file.length();
377:
378: if ((lastModified != newLastModified)
379: || (length != newLength)) {
380: lastModified = newLastModified;
381: length = newLength;
382: changedAttr = true;
383: }
384:
385: if (files != null) {
386: FileSystemNode[] oldChildren = children;
387: FileSystemNode[] newChildren = outer
388: .getFilteredChildren(files);
389:
390: if ((oldChildren != null) && (newChildren != null)) {
391: Collection added = notIn(newChildren, oldChildren);
392: Collection removed = notIn(oldChildren, newChildren);
393:
394: /* *********************************************************************
395: * NOTE: in the case of multiple added/removed, this doesn't behave
396: * quite right... to avoid making the tree (view) go haywire,
397: * for now multiple adds/removes will fireTreeNodesChanged
398: * instead of fireNodesInserted/fireNodesRemoved
399: */
400: if ((added.size() > 1) || (removed.size() > 1)) {
401: updateTree(STRUCTURE_CHANGED, newChildren,
402: null, null);
403: return;
404: }
405:
406: // handle added children:
407: if (added.size() > 0) {
408: int[] childIdxs = new int[added.size()];
409: Object[] children = new Object[added.size()];
410: int i = 0;
411:
412: for (Iterator itr = added.iterator(); itr
413: .hasNext();) {
414: FileSystemNode n = (FileSystemNode) (itr
415: .next());
416: childIdxs[i] = findIdx(newChildren, n);
417: children[i] = n;
418: }
419:
420: updateTree(NODES_INSERTED, newChildren,
421: childIdxs, children);
422: }
423:
424: // handle removed children:
425: if (removed.size() > 0) {
426: int[] childIdxs = new int[removed.size()];
427: Object[] children = new Object[removed.size()];
428: int i = 0;
429:
430: for (Iterator itr = removed.iterator(); itr
431: .hasNext();) {
432: FileSystemNode n = (FileSystemNode) (itr
433: .next());
434: childIdxs[i] = findIdx(oldChildren, n);
435: children[i] = n;
436: }
437:
438: updateTree(NODES_REMOVED, newChildren,
439: childIdxs, children);
440: }
441: } else {
442: updateTree(STRUCTURE_CHANGED, newChildren, null,
443: null);
444: return;
445: }
446: }
447:
448: if (changedAttr)
449: updateTree(NODES_CHANGED, null, null, null);
450: }
451:
452: /**
453: */
454: public int hashCode() {
455: return getFile().hashCode();
456: }
457:
458: /**
459: * Two nodes are determined to be equal if they represent the
460: * same file.
461: */
462: public boolean equals(Object obj) {
463: return (obj instanceof FileSystemNode)
464: && getFile().equals(
465: ((FileSystemNode) obj).getFile());
466: }
467:
468: /**
469: * Cast to string, which is useful if a default-cell-renderer is used.
470: */
471: public String toString() {
472: return getFile().getName();
473: }
474: }
475:
476: /**
477: * Table that maps file extension to icon.
478: */
479: private static Hashtable iconTable = new Hashtable();
480:
481: /**
482: * Maps file extension to info string describing file type.
483: */
484: private static Hashtable infoTable = new Hashtable();
485:
486: /**
487: * Set the icon to draw, and the description of the type, for files of
488: * the specified type (as determined by file extension). If an icon or
489: * description has already been registered for this file type, the new
490: * icon will replace the existing one.
491: *
492: * @param extension the file type extension
493: * @param icon the icon to display for this file type, or <code>null</code>
494: * @param info an info string describing the file type, or <code>null</code>
495: */
496: public static void setFileInfo(String extension,
497: javax.swing.Icon icon, String info) {
498: if (icon != null)
499: iconTable.put(extension, icon);
500:
501: if (info != null)
502: infoTable.put(extension, info);
503: }
504:
505: /**
506: * Get the icon associated with the specified file type.
507: *
508: * @param extension the file type extension
509: * @return the icon to displayed for this file type, or <code>null</code> if none
510: */
511: public static javax.swing.Icon getFileIcon(String extension) {
512: return (javax.swing.Icon) (iconTable.get(extension));
513: }
514:
515: public static String getFileInfo(String extension) {
516: return (String) (infoTable.get(extension));
517: }
518:
519: /**
520: * A {@link javax.swing.tree.TreeCellRenderer} that uses the icons set
521: * with {@link #setFileIcon}. To make the correct icons be displayed,
522: * when you create a new tree, you need to set it's renderer to be an
523: * instance of this class (or a subclass).
524: * <pre>
525: * JTree tree = new JTree( new FileSystemTreeModel( "/", null ) )
526: * tree.setCellRenderer( new FileSystemTreeModel.FileSystemTreeCellRenderer() );
527: * </pre>
528: */
529: public static class FileSystemTreeCellRenderer extends
530: javax.swing.tree.DefaultTreeCellRenderer {
531: public java.awt.Component getTreeCellRendererComponent(
532: javax.swing.JTree tree, Object value, boolean selected,
533: boolean expanded, boolean leaf, int row,
534: boolean hasFocus) {
535: super .getTreeCellRendererComponent(tree, value, selected,
536: expanded, leaf, row, hasFocus);
537:
538: AbstractFile file = ((FileSystemNode) value).getFile();
539: javax.swing.Icon icon = getFileIcon(getExtension(file));
540: String info = getFileInfo(getExtension(file));
541:
542: if (icon != null)
543: setIcon(icon);
544:
545: if (info != null)
546: setToolTipText(info);
547:
548: return this ;
549: }
550: }
551:
552: private static String getExtension(AbstractFile file) {
553: String type = null;
554:
555: if (file instanceof AbstractFileSystem.MountPointFile)
556: type = ((AbstractFileSystem.MountPointFile) file)
557: .getMountType();
558:
559: if ((type == null) && file.isDirectory())
560: type = "/dir/";
561:
562: if (type == null)
563: type = file.getExtension();
564:
565: return type;
566: }
567:
568: /**
569: * contains FileSystemNode -> path mappings for nodes that were expanded while
570: * still waiting for structure info (ie. children) from worker thread.
571: */
572: private static Map pendingExpansionMap = new WeakHashMap();
573:
574: private void handlePendingExpansions() {
575: restart: for (Iterator itr = pendingExpansionMap.keySet()
576: .iterator(); itr.hasNext();) {
577: FileSystemNode fsn = (FileSystemNode) (itr.next());
578: if ((fsNodeTable.get(fsn.getFile()) == fsn)
579: && !fsn.pending()) {
580: Reference ref = (Reference) (pendingExpansionMap
581: .get(fsn));
582: if (ref != null) {
583: javax.swing.JTree tree = (javax.swing.JTree) (ref
584: .get());
585: if (tree != null) {
586: TreePath path = getTreePath(fsn.getFile());
587: tree.expandPath(path);
588: tree.scrollPathToVisible(path);
589: pendingExpansionMap.remove(fsn);
590: break restart;
591: }
592: }
593: }
594: }
595: }
596:
597: /**
598: * A {@link javax.swing.event.TreeExpansionListener} which helps the
599: * <code>TreeModel</code> track what files are visible, so that the
600: * model can make intelligent choices about what files to watch (poll)
601: * for changes. To ensure that the model notices changes to the file
602: * system, and therefore the tree gets the appropriate update events
603: * when the filesystem changes, you need to add this expansion listener,
604: * for example:
605: * <pre>
606: * JTree tree = new JTree( new FileSystemTreeModel( "/", null ) )
607: * tree.addTreeExpansionListener( new FileSystemTreeModel.FileSystemTreeExpansionListener() );
608: * </pre>
609: */
610: public static class FileSystemTreeExpansionListener implements
611: javax.swing.event.TreeExpansionListener {
612: /**
613: * Called whenever an item in the tree has been expanded.
614: */
615: public void treeExpanded(
616: javax.swing.event.TreeExpansionEvent evt) {
617: FileSystemNode fsn = (FileSystemNode) (evt.getPath()
618: .getLastPathComponent());
619:
620: if (fsn.pending())
621: pendingExpansionMap.put(fsn, new WeakReference(evt
622: .getSource()));
623:
624: Object[] nodes = evt.getPath().getPath();
625: for (int i = 0; i < nodes.length; i++)
626: startWatchingNode((FileSystemNode) (nodes[i]));
627: }
628:
629: /**
630: * Called whenever an item in the tree has been collapsed.
631: */
632: public void treeCollapsed(
633: javax.swing.event.TreeExpansionEvent evt) {
634: // just in case:
635: pendingExpansionMap.remove(evt.getPath()
636: .getLastPathComponent());
637: Object[] nodes = evt.getPath().getPath();
638: for (int i = 0; i < nodes.length; i++)
639: stopWatchingNode((FileSystemNode) (nodes[i]));
640: }
641: }
642:
643: /* How we keep track of what to watch:
644: * In order to keep track of the files that need watching in a way that
645: * is efficient when it comes to figure out what files to check each time
646: * we poll, and at the same time not prevent the file and FileSystemNodes
647: * from being GC'd, we keep a weak-hash-map to map file to info about
648: * the watched file, which includes the last-modified time and a list of
649: * weak-references to the FileSystemNodes that need notification of the
650: * change. Since the FileSystemNode holds a reference to the file, the
651: * file won't be GC'd and removed from the weak-hash-map until all the
652: * FileSystemNodes are GC'd, so there is no problem with WatchedDirectory
653: * being removed from the weak-hash-map while there is still a tree
654: * interested in updates to that file.
655: *
656: * All synchronization is handled at the "topmost" function in the API,
657: * ie startWatchingNode, stopWatchingNode, and scanFiles
658: */
659:
660: /**
661: * Tracks the pertainent info associated with a watched file, such as the
662: * last-modified time from the last time we scanned this file, and the
663: * list of nodes to notify of the change.
664: */
665: private static class WatchedDirectory {
666: private AbstractFile file;
667: private Collection children; // collection of AbstractFile
668: private long lastModified;
669: private Set nodeReferenceSet = new HashSet();
670: private FileSystemNodeReference[] nodeReferences = null;
671:
672: WatchedDirectory(AbstractFile file) {
673: this .file = file;
674: this .lastModified = 0;
675: }
676:
677: AbstractFile getFile() {
678: return file;
679: }
680:
681: /**
682: * Add a node to the list of nodes that will be notified (via fileChanged()
683: * if the file changes. This will not hold a hard reference to the node and
684: * will not prevent the node from being GCd.
685: */
686: synchronized void addFileSystemNode(final FileSystemNode node) {
687: nodeReferenceSet.add(new FileSystemNodeReference(node));
688: nodeReferences = null;
689: WorkerThread.invokeLater(new Runnable() {
690: public void run() {
691: if (children != null)
692: node.fileChanged(children);
693: else
694: scan();
695: }
696: });
697: }
698:
699: /**
700: * Remove a node from the list of nodes to notify when file changes.
701: */
702: synchronized void removeFileSystemNode(FileSystemNode node) {
703: nodeReferenceSet.remove(new FileSystemNodeReference(node));
704: nodeReferences = null;
705: }
706:
707: private synchronized FileSystemNodeReference[] getNodeReferences() {
708: if (nodeReferences == null) {
709: nodeReferences = (FileSystemNodeReference[]) (nodeReferenceSet
710: .toArray(new FileSystemNodeReference[nodeReferenceSet
711: .size()]));
712: }
713: return nodeReferences;
714: }
715:
716: /**
717: * Scan file, notify nodes if file has changed. Returns <code>true</code>
718: * if this node should continue to be scanned, else <code>false</code> if
719: * there are no more nodes that haven't been GC'd that would need to be
720: * notified if the file has changed.
721: */
722: boolean scan() {
723: // XXX prune GC'd entries from the list... perhaps this should be
724: // done with a ReferenceQueue and a seperate thread?
725: for (Iterator itr = nodeReferenceSet.iterator(); itr
726: .hasNext();)
727: if (((FileSystemNodeReference) (itr.next()))
728: .getFileSystemNode() == null)
729: itr.remove();
730:
731: // first check for the case that there are no more nodes to watch,
732: // and if that is the case remove ourself from the table of files
733: // to check. This has to be done regardless of whether the file
734: // changes, so we are not dependent on a file change to stop the
735: // timer:
736: if (nodeReferenceSet.size() == 0)
737: return false;
738:
739: if (lastModified != file.lastModified()) {
740: lastModified = file.lastModified();
741:
742: FileSystemNodeReference[] nodeReferences = getNodeReferences();
743: try {
744: children = AbstractFileSystem.children(file);
745: } catch (java.io.IOException e) {
746: lastModified = 0;
747: e.printStackTrace(); // ????
748: return false;
749: }
750:
751: // notify watched nodes of the change:
752: for (int i = 0; i < nodeReferences.length; i++) {
753: FileSystemNode node = nodeReferences[i]
754: .getFileSystemNode();
755: if (node != null)
756: node.fileChanged(children);
757: }
758: }
759:
760: return true;
761: }
762: }
763:
764: /**
765: * A weak-reference used to track the watched FileSystemNode without preventing
766: * it from being GC'd
767: */
768: private static class FileSystemNodeReference extends WeakReference {
769: private int hashCode;
770:
771: FileSystemNodeReference(FileSystemNode node) {
772: super (node);
773: hashCode = node.hashCode();
774: }
775:
776: public int hashCode() {
777: return hashCode;
778: }
779:
780: public FileSystemNode getFileSystemNode() {
781: return (FileSystemNode) get();
782: }
783:
784: public boolean equals(Object obj) {
785: if (obj instanceof FileSystemNodeReference) {
786: FileSystemNode this Node = getFileSystemNode();
787: FileSystemNode otherNode = ((FileSystemNodeReference) obj)
788: .getFileSystemNode();
789:
790: if ((this Node != null) && (otherNode != null))
791: return this Node == otherNode; // XXX: uses == because .equals() compares node's files
792: }
793:
794: return false;
795: }
796: }
797:
798: /**
799: * Track the files to poll.
800: */
801: private static Map fileToInfoMap = new TreeMap(new Comparator() {
802:
803: public int compare(Object obj1, Object obj2) {
804: String p1 = ((AbstractFile) obj1).getPath();
805: String p2 = ((AbstractFile) obj2).getPath();
806: int result = p1.length() - p2.length();
807: if (result == 0)
808: result = p1.compareTo(p2);
809: return result;
810: }
811:
812: });
813:
814: /**
815: * Timer that is created to periodically scan watched files.
816: */
817: private static Runnable fileWatcherRunnable = null;
818:
819: /**
820: * Start monitoring the file associated with the specified node. If the
821: * file is determined to have changed, then the node's fileChanged()
822: * method is called.
823: */
824: private static synchronized void startWatchingNode(
825: FileSystemNode node) {
826: AbstractFile file = node.getFile();
827: WatchedDirectory wd = (WatchedDirectory) (fileToInfoMap
828: .get(file));
829:
830: if (wd == null) {
831: wd = new WatchedDirectory(file);
832: fileToInfoMap.put(file, wd);
833: }
834:
835: wd.addFileSystemNode(node);
836:
837: if (fileWatcherRunnable == null) {
838: fileWatcherRunnable = new Runnable() {
839: public void run() {
840: scanFiles();
841: }
842: };
843: WorkerThread.addRunnable(fileWatcherRunnable, 1000);
844: }
845: }
846:
847: /**
848: * Stop monitoring the file associated with the node on behalf of the node.
849: * If no other nodes are associated with the same file, the file will no
850: * longer be monitored.
851: */
852: private static synchronized void stopWatchingNode(
853: FileSystemNode node) {
854: AbstractFile file = node.getFile();
855: WatchedDirectory wd = (WatchedDirectory) (fileToInfoMap
856: .get(file));
857:
858: if (wd == null)
859: return;
860:
861: wd.removeFileSystemNode(node);
862: }
863:
864: /**
865: * Scan the watched files. This is called periodically to scan all watched
866: * files.
867: */
868: private static void scanFiles() {
869: WatchedDirectory[] wds;
870:
871: synchronized (FileSystemTreeModel.class) {
872: wds = (WatchedDirectory[]) (fileToInfoMap.values()
873: .toArray(new WatchedDirectory[fileToInfoMap.size()]));
874: }
875:
876: for (int i = 0; i < wds.length; i++)
877: if (wds[i].scan())
878: wds[i] = null; // don't need to remove this node
879:
880: synchronized (FileSystemTreeModel.class) {
881: for (int i = 0; i < wds.length; i++)
882: if (wds[i] != null)
883: fileToInfoMap.remove(wds[i].getFile());
884: }
885: }
886:
887: //////////////////////////////////////////////////////////////////////////////
888: // some utilities:
889:
890: /**
891: * find index of item within an array
892: */
893: private static final int findIdx(Object[] arr, Object item) {
894: for (int i = 0; i < arr.length; i++)
895: if (item.equals(arr[i]))
896: return i;
897: return -1;
898: }
899:
900: /**
901: * return a set of nodes that are in a, but not in b.. returned as an array
902: * of file objects (or <code>null</code> if none)
903: */
904: private static final Collection notIn(FileSystemNode[] a,
905: FileSystemNode[] b) {
906: Collection v = new Vector();
907:
908: for (int i = 0; i < a.length; i++)
909: v.add(a[i]);
910:
911: for (int i = 0; i < b.length; i++)
912: v.remove(b[i]);
913:
914: return v;
915: }
916:
917: /**
918: * Utility to go from AbstractFile to TreePath
919: */
920: public TreePath getTreePath(AbstractFile file) {
921: LinkedList nodeList = new LinkedList();
922:
923: FileSystemNode node = getFileSystemNode(file);
924: while (node != null) {
925: nodeList.addFirst(node);
926: node = node.getParent();
927: }
928:
929: return new TreePath(nodeList.toArray());
930: }
931:
932: // private void pathToNodeList( LinkedList nodeList, String path )
933: // {
934: // FileSystemNode node = getFileSystemNode( path.equals("") ? "/" : path );
935: //
936: // if( (path.indexOf(rootPath) == 0) && (path.length() > rootPath.length()) )
937: // pathToNodeList( nodeList, path.substring( 0, path.lastIndexOf('/') ) );
938: //
939: // nodeList.add(node);
940: // }
941:
942: /**
943: * utility to go from path to file... catches and deals with exceptions,
944: * but could return null.
945: */
946: protected static final AbstractFile pathToFile(String path) {
947: try {
948: return AbstractFileSystem.resolve(path);
949: } catch (java.io.IOException e) {
950: e.printStackTrace();
951: return null;
952: }
953: }
954: //////////////////////////////////////////////////////////////////////////////
955: }
956:
957: /*
958: * Local Variables:
959: * tab-width: 2
960: * indent-tabs-mode: nil
961: * mode: java
962: * c-indentation-style: java
963: * c-basic-offset: 2
964: * eval: (c-set-offset 'substatement-open '0)
965: * eval: (c-set-offset 'case-label '+)
966: * eval: (c-set-offset 'inclass '+)
967: * eval: (c-set-offset 'inline-open '0)
968: * End:
969: */
|