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 }