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    }