001 package ui.model;
002
003 import util.MergeSort;
004
005 import java.io.IOException;
006 import java.io.File;
007 import java.util.Date;
008 import java.util.Stack;
009 import javax.swing.SwingUtilities;
010 import javax.swing.tree.TreePath;
011
012 /**
013 * FileSystemModel2 is a TreeTableModel representing a hierarchical file
014 * system.<p>
015 * This will recursively load all the children from the path it is
016 * created with. The loading is done with the method reloadChildren, and
017 * happens in another thread. The method isReloading can be invoked to check
018 * if there are active threads. The total size of all the files are also
019 * accumulated.
020 * <p>
021 * By default links are not descended. java.io.File does not have a way
022 * to distinguish links, so a file is assumed to be a link if its canonical
023 * path does not start with its parent path. This may not cover all cases,
024 * but works for the time being.
025 * <p>Reloading happens such that all the files of the directory are
026 * loaded and immediately available. The background thread then recursively
027 * loads all the files of each of those directories. When each directory has
028 * finished loading all its sub files they are attached and an event is
029 * generated in the event dispatching thread. A more ambitious approach
030 * would be to attach each set of directories as they are loaded and generate
031 * an event. Then, once all the direct descendants of the directory being
032 * reloaded have finished loading, it is resorted based on total size.
033 * <p>
034 * While you can invoke reloadChildren as many times as you want, care
035 * should be taken in doing this. You should not invoke reloadChildren
036 * on a node that is already being reloaded, or going to be reloaded
037 * (meaning its parent is reloading but it hasn't started reloading
038 * that directory yet). If this is done odd results may
039 * happen. FileSystemModel2 does not enforce any policy in this manner,
040 * and it is up to the user of FileSystemModel2 to ensure it doesn't
041 * happen.
042 *
043 * @version 1.12 05/12/98
044 * @author Philip Milne
045 * @author Scott Violet
046 */
047
048 public class FileSystemModel2 extends AbstractTreeTableModel {
049
050 // Names of the columns.
051 static protected String[] cNames = {"Name", "Size", "Type", "Modified"};
052
053 // Types of the columns.
054 static protected Class[] cTypes = { TreeTableModel.class,
055 Integer.class,
056 String.class,
057 Date.class};
058
059 // The the returned file length for directories.
060 public static final Integer ZERO = new Integer(0);
061
062 /** An array of MergeSorter sorters, that will sort based on size. */
063 static Stack sorters = new Stack();
064
065
066 /** True if the receiver is valid, once set to false all Threads
067 * loading files will stop. */
068 protected boolean isValid;
069
070 /** Node currently being reloaded, this becomes somewhat muddy if
071 * reloading is happening in multiple threads. */
072 protected FileNode reloadNode;
073
074 /** > 0 indicates reloading some nodes. */
075 int reloadCount;
076
077 /** Returns true if links are to be descended. */
078 protected boolean descendLinks;
079
080
081 /**
082 * Returns a MergeSort that can sort on the totalSize of a FileNode.
083 */
084 protected static MergeSort getSizeSorter() {
085 synchronized(sorters) {
086 if (sorters.size() == 0) {
087 return new SizeSorter();
088 }
089 return (MergeSort)sorters.pop();
090 }
091 }
092
093 /**
094 * Should be invoked when a MergeSort is no longer needed.
095 */
096 protected static void recycleSorter(MergeSort sorter) {
097 synchronized(sorters) {
098 sorters.push(sorter);
099 }
100 }
101
102
103 /**
104 * Creates a FileSystemModel2 rooted at File.separator, which is usually
105 * the root of the file system. This does not load it, you should invoke
106 * <code>reloadChildren</code> with the root to start loading.
107 */
108 public FileSystemModel2() {
109 this(File.separator);
110 }
111
112 /**
113 * Creates a FileSystemModel2 with the root being <code>rootPath</code>.
114 * This does not load it, you should invoke
115 * <code>reloadChildren</code> with the root to start loading.
116 */
117 public FileSystemModel2(String rootPath) {
118 super(null);
119 isValid = true;
120 root = new FileNode(new File(rootPath));
121 }
122
123 //
124 // The TreeModel interface
125 //
126
127 /**
128 * Returns the number of children of <code>node</code>.
129 */
130 public int getChildCount(Object node) {
131 Object[] children = getChildren(node);
132 return (children == null) ? 0 : children.length;
133 }
134
135 /**
136 * Returns the child of <code>node</code> at index <code>i</code>.
137 */
138 public Object getChild(Object node, int i) {
139 return getChildren(node)[i];
140 }
141
142 /**
143 * Returns true if the passed in object represents a leaf, false
144 * otherwise.
145 */
146 public boolean isLeaf(Object node) {
147 return ((FileNode)node).isLeaf();
148 }
149
150 //
151 // The TreeTableNode interface.
152 //
153
154 /**
155 * Returns the number of columns.
156 */
157 public int getColumnCount() {
158 return cNames.length;
159 }
160
161 /**
162 * Returns the name for a particular column.
163 */
164 public String getColumnName(int column) {
165 return cNames[column];
166 }
167
168 /**
169 * Returns the class for the particular column.
170 */
171 public Class getColumnClass(int column) {
172 return cTypes[column];
173 }
174
175 /**
176 * Returns the value of the particular column.
177 */
178 public Object getValueAt(Object node, int column) {
179 FileNode fn = (FileNode)node;
180
181 try {
182 switch(column) {
183 case 0:
184 return fn.getFile().getName();
185 case 1:
186 if (fn.isTotalSizeValid()) {
187 return new Integer((int)((FileNode)node).totalSize());
188 }
189 return null;
190 case 2:
191 return fn.isLeaf() ? "File" : "Directory";
192 case 3:
193 return fn.lastModified();
194 }
195 }
196 catch (SecurityException se) { }
197
198 return null;
199 }
200
201 //
202 // Some convenience methods.
203 //
204
205 /**
206 * Reloads the children of the specified node.
207 */
208 public void reloadChildren(Object node) {
209 FileNode fn = (FileNode)node;
210
211 synchronized(this) {
212 reloadCount++;
213 }
214 fn.resetSize();
215 new Thread(new FileNodeLoader((FileNode)node)).start();
216 }
217
218 /**
219 * Stops and waits for all threads to finish loading.
220 */
221 public void stopLoading() {
222 isValid = false;
223 synchronized(this) {
224 while (reloadCount > 0) {
225 try {
226 wait();
227 } catch (InterruptedException ie) {}
228 }
229 }
230 isValid = true;
231 }
232
233 /**
234 * If <code>newValue</code> is true, links are descended. Odd results
235 * may happen if you set this while other threads are loading.
236 */
237 public void setDescendsLinks(boolean newValue) {
238 descendLinks = newValue;
239 }
240
241 /**
242 * Returns true if links are to be automatically descended.
243 */
244 public boolean getDescendsLinks() {
245 return descendLinks;
246 }
247
248 /**
249 * Returns the path <code>node</code> represents.
250 */
251 public String getPath(Object node) {
252 return ((FileNode)node).getFile().getPath();
253 }
254
255 /**
256 * Returns the total size of the receiver.
257 */
258 public long getTotalSize(Object node) {
259 return ((FileNode)node).totalSize();
260 }
261
262 /**
263 * Returns true if the receiver is loading any children.
264 */
265 public boolean isReloading() {
266 return (reloadCount > 0);
267 }
268
269 /**
270 * Returns the path to the node that is being loaded.
271 */
272 public TreePath getPathLoading() {
273 FileNode rn = reloadNode;
274
275 if (rn != null) {
276 return new TreePath(rn.getPath());
277 }
278 return null;
279 }
280
281 /**
282 * Returns the node being loaded.
283 */
284 public Object getNodeLoading() {
285 return reloadNode;
286 }
287
288 protected File getFile(Object node) {
289 FileNode fileNode = ((FileNode)node);
290 return fileNode.getFile();
291 }
292
293 protected Object[] getChildren(Object node) {
294 FileNode fileNode = ((FileNode)node);
295 return fileNode.getChildren();
296 }
297
298
299 protected static FileNode[] EMPTY_CHILDREN = new FileNode[0];
300
301 // Used to sort the file names.
302 static private MergeSort fileMS = new MergeSort() {
303 public int compareElementsAt(int beginLoc, int endLoc) {
304 return ((String)toSort[beginLoc]).compareTo
305 ((String)toSort[endLoc]);
306 }
307 };
308
309
310 /**
311 * A FileNode is a derivative of the File class - though we delegate to
312 * the File object rather than subclassing it. It is used to maintain a
313 * cache of a directory's children and therefore avoid repeated access
314 * to the underlying file system during rendering.
315 */
316 class FileNode {
317 /** java.io.File the receiver represents. */
318 protected File file;
319 /** Parent FileNode of the receiver. */
320 private FileNode parent;
321 /** Children of the receiver. */
322 protected FileNode[] children;
323 /** Size of the receiver and all its children. */
324 protected long totalSize;
325 /** Valid if the totalSize has finished being calced. */
326 protected boolean totalSizeValid;
327 /** Path of the receiver. */
328 protected String canonicalPath;
329 /** True if the canonicalPath of this instance does not start with
330 * the canonical path of the parent. */
331 protected boolean isLink;
332 /** Date last modified. */
333 protected Date lastModified;
334
335
336 protected FileNode(File file) {
337 this(null, file);
338 }
339
340 protected FileNode(FileNode parent, File file) {
341 this.parent = parent;
342 this.file = file;
343 try {
344 canonicalPath = file.getCanonicalPath();
345 }
346 catch (IOException ioe) {
347 canonicalPath = "";
348 }
349 if (parent != null) {
350 isLink = !canonicalPath.startsWith(parent.getCanonicalPath());
351 }
352 else {
353 isLink = false;
354 }
355 if (isLeaf()) {
356 totalSize = file.length();
357 totalSizeValid = true;
358 }
359 }
360
361 /**
362 * Returns the date the receiver was last modified.
363 */
364 public Date lastModified() {
365 if (lastModified == null && file != null) {
366 lastModified = new Date(file.lastModified());
367 }
368 return lastModified;
369 }
370
371 /**
372 * Returns the the string to be used to display this leaf in the JTree.
373 */
374 public String toString() {
375 return file.getName();
376 }
377
378 /**
379 * Returns the java.io.File the receiver represents.
380 */
381 public File getFile() {
382 return file;
383 }
384
385 /**
386 * Returns size of the receiver and all its children.
387 */
388 public long totalSize() {
389 return totalSize;
390 }
391
392 /**
393 * Returns the parent of the receiver.
394 */
395 public FileNode getParent() {
396 return parent;
397 }
398
399 /**
400 * Returns true if the receiver represents a leaf, that is it is
401 * isn't a directory.
402 */
403 public boolean isLeaf() {
404 return file.isFile();
405 }
406
407 /**
408 * Returns true if the total size is valid.
409 */
410 public boolean isTotalSizeValid() {
411 return totalSizeValid;
412 }
413
414 /**
415 * Clears the date.
416 */
417 protected void resetLastModified() {
418 lastModified = null;
419 }
420
421 /**
422 * Sets the size of the receiver to be 0.
423 */
424 protected void resetSize() {
425 alterTotalSize(-totalSize);
426 }
427
428 /**
429 * Loads the children, caching the results in the children
430 * instance variable.
431 */
432 protected FileNode[] getChildren() {
433 return children;
434 }
435
436 /**
437 * Recursively loads all the children of the receiver.
438 */
439 protected void loadChildren(MergeSort sorter) {
440 totalSize = file.length();
441 children = createChildren(null);
442 for (int counter = children.length - 1; counter >= 0; counter--) {
443 Thread.yield(); // Give the GUI CPU time to draw itself.
444 if (!children[counter].isLeaf() &&
445 (descendLinks || !children[counter].isLink())) {
446 children[counter].loadChildren(sorter);
447 }
448 totalSize += children[counter].totalSize();
449 if (!isValid) {
450 counter = 0;
451 }
452 }
453 if (isValid) {
454 if (sorter != null) {
455 sorter.sort(children);
456 }
457 totalSizeValid = true;
458 }
459 }
460
461 /**
462 * Loads the children of of the receiver.
463 */
464 protected FileNode[] createChildren(MergeSort sorter) {
465 FileNode[] retArray = null;
466
467 try {
468 String[] files = file.list();
469 if(files != null) {
470 if (sorter != null) {
471 sorter.sort(files);
472 }
473 retArray = new FileNode[files.length];
474 String path = file.getPath();
475 for(int i = 0; i < files.length; i++) {
476 File childFile = new File(path, files[i]);
477 retArray[i] = new FileNode(this, childFile);
478 }
479 }
480 } catch (SecurityException se) {}
481 if (retArray == null) {
482 retArray = EMPTY_CHILDREN;
483 }
484 return retArray;
485 }
486
487 /**
488 * Returns true if the children have been loaded.
489 */
490 protected boolean loadedChildren() {
491 return (file.isFile() || (children != null));
492 }
493
494 /**
495 * Gets the path from the root to the receiver.
496 */
497 public FileNode[] getPath() {
498 return getPathToRoot(this, 0);
499 }
500
501 /**
502 * Returns the canonical path for the receiver.
503 */
504 public String getCanonicalPath() {
505 return canonicalPath;
506 }
507
508 /**
509 * Returns true if the receiver's path does not begin with the
510 * parent's canonical path.
511 */
512 public boolean isLink() {
513 return isLink;
514 }
515
516 protected FileNode[] getPathToRoot(FileNode aNode, int depth) {
517 FileNode[] retNodes;
518
519 if(aNode == null) {
520 if(depth == 0)
521 return null;
522 else
523 retNodes = new FileNode[depth];
524 }
525 else {
526 depth++;
527 retNodes = getPathToRoot(aNode.getParent(), depth);
528 retNodes[retNodes.length - depth] = aNode;
529 }
530 return retNodes;
531 }
532
533 /**
534 * Sets the children of the receiver, updates the total size,
535 * and if generateEvent is true a tree structure changed event
536 * is created.
537 */
538 protected void setChildren(FileNode[] newChildren,
539 boolean generateEvent) {
540 long oldSize = totalSize;
541
542 totalSize = file.length();
543 children = newChildren;
544 for (int counter = children.length - 1; counter >= 0;
545 counter--) {
546 totalSize += children[counter].totalSize();
547 }
548
549 if (generateEvent) {
550 FileNode[] path = getPath();
551
552 fireTreeStructureChanged(FileSystemModel2.this, path, null,
553 null);
554
555 FileNode parent = getParent();
556
557 if (parent != null) {
558 parent.alterTotalSize(totalSize - oldSize);
559 }
560 }
561 }
562
563 protected synchronized void alterTotalSize(long sizeDelta) {
564 if (sizeDelta != 0 && (parent = getParent()) != null) {
565 totalSize += sizeDelta;
566 nodeChanged();
567 parent.alterTotalSize(sizeDelta);
568 }
569 else {
570 // Need a way to specify the root.
571 totalSize += sizeDelta;
572 }
573 }
574
575 /**
576 * This should only be invoked on the event dispatching thread.
577 */
578 protected synchronized void setTotalSizeValid(boolean newValue) {
579 if (totalSizeValid != newValue) {
580 nodeChanged();
581 totalSizeValid = newValue;
582
583 FileNode parent = getParent();
584
585 if (parent != null) {
586 parent.childTotalSizeChanged(this);
587 }
588 }
589 }
590
591 /**
592 * Marks the receivers total size as valid, but does not invoke
593 * node changed, nor message the parent.
594 */
595 protected synchronized void forceTotalSizeValid() {
596 totalSizeValid = true;
597 }
598
599 /**
600 * Invoked when a childs total size has changed.
601 */
602 protected synchronized void childTotalSizeChanged(FileNode child) {
603 if (totalSizeValid != child.isTotalSizeValid()) {
604 if (totalSizeValid) {
605 setTotalSizeValid(false);
606 }
607 else {
608 FileNode[] children = getChildren();
609
610 for (int counter = children.length - 1; counter >= 0;
611 counter--) {
612 if (!children[counter].isTotalSizeValid()) {
613 return;
614 }
615 }
616 setTotalSizeValid(true);
617 }
618 }
619
620 }
621
622 /**
623 * Can be invoked when a node has changed, will create the
624 * appropriate event.
625 */
626 protected void nodeChanged() {
627 FileNode parent = getParent();
628
629 if (parent != null) {
630 FileNode[] path = parent.getPath();
631 int[] index = { getIndexOfChild(parent, this) };
632 Object[] children = { this };
633
634 fireTreeNodesChanged(FileSystemModel2.this, path, index,
635 children);
636 }
637 }
638 }
639
640
641 /**
642 * FileNodeLoader can be used to reload all the children of a
643 * particular node. It first resets the children of the FileNode
644 * it is created with, and in its run method will reload all of
645 * that nodes children. FileNodeLoader may not be running in the event
646 * dispatching thread. As swing is not thread safe it is important
647 * that we don't generate events in this thread. SwingUtilities.invokeLater
648 * is used so that events are generated in the event dispatching thread.
649 */
650 class FileNodeLoader implements Runnable {
651 /** Node creating children for. */
652 FileNode node;
653 /** Sorter. */
654 MergeSort sizeMS;
655
656 FileNodeLoader(FileNode node) {
657 this.node = node;
658 node.resetLastModified();
659 node.setChildren(node.createChildren(fileMS), true);
660 node.setTotalSizeValid(false);
661 }
662
663 public void run() {
664 FileNode[] children = node.getChildren();
665
666 sizeMS = getSizeSorter();
667 for (int counter = children.length - 1; counter >= 0; counter--) {
668 if (!children[counter].isLeaf()) {
669 reloadNode = children[counter];
670 loadChildren(children[counter]);
671 reloadNode = null;
672 }
673 if (!isValid) {
674 counter = 0;
675 }
676 }
677 recycleSorter(sizeMS);
678 if (isValid) {
679 SwingUtilities.invokeLater(new Runnable() {
680 public void run() {
681 MergeSort sorter = getSizeSorter();
682
683 sorter.sort(node.getChildren());
684 recycleSorter(sorter);
685 node.setChildren(node.getChildren(), true);
686 synchronized(FileSystemModel2.this) {
687 reloadCount--;
688 FileSystemModel2.this.notifyAll();
689 }
690 }
691 });
692 }
693 else {
694 synchronized(FileSystemModel2.this) {
695 reloadCount--;
696 FileSystemModel2.this.notifyAll();
697 }
698 }
699 }
700
701 protected void loadChildren(FileNode node) {
702 if (!node.isLeaf() && (descendLinks || !node.isLink())) {
703 final FileNode[] children = node.createChildren(null);
704
705 for (int counter = children.length - 1; counter >= 0;
706 counter--) {
707 if (!children[counter].isLeaf()) {
708 if (descendLinks || !children[counter].isLink()) {
709 children[counter].loadChildren(sizeMS);
710 }
711 else {
712 children[counter].forceTotalSizeValid();
713 }
714 }
715 if (!isValid) {
716 counter = 0;
717 }
718 }
719 if (isValid) {
720 final FileNode fn = node;
721
722 // Reset the children
723 SwingUtilities.invokeLater(new Runnable() {
724 public void run() {
725 MergeSort sorter = getSizeSorter();
726
727 sorter.sort(children);
728 recycleSorter(sorter);
729 fn.setChildren(children, true);
730 fn.setTotalSizeValid(true);
731 fn.nodeChanged();
732 }
733 });
734 }
735 }
736 else {
737 node.forceTotalSizeValid();
738 }
739 }
740 }
741
742
743 /**
744 * Sorts the contents, which must be instances of FileNode based on
745 * totalSize.
746 */
747 static class SizeSorter extends MergeSort {
748 public int compareElementsAt(int beginLoc, int endLoc) {
749 long firstSize = ((FileNode)toSort[beginLoc]).totalSize();
750 long secondSize = ((FileNode)toSort[endLoc]).totalSize();
751
752 if (firstSize != secondSize) {
753 return (int)(secondSize - firstSize);
754 }
755 return ((FileNode)toSort[beginLoc]).toString().compareTo
756 (((FileNode)toSort[endLoc]).toString());
757 }
758 }
759 }