View Javadoc

1   // Copyright (C) 2003 - 2009 Philip Aston
2   // All rights reserved.
3   //
4   // This file is part of The Grinder software distribution. Refer to
5   // the file LICENSE which is part of The Grinder distribution for
6   // licensing details. The Grinder distribution is available on the
7   // Internet at http://grinder.sourceforge.net/
8   //
9   // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
12  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
13  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
14  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
17  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
19  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
20  // OF THE POSSIBILITY OF SUCH DAMAGE.
21  
22  package net.grinder.console.swingui;
23  
24  import java.io.File;
25  import java.io.FileFilter;
26  import java.io.FilenameFilter;
27  
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.List;
32  import javax.swing.event.EventListenerList;
33  import javax.swing.event.TreeModelEvent;
34  import javax.swing.event.TreeModelListener;
35  import javax.swing.tree.TreeModel;
36  import javax.swing.tree.TreePath;
37  
38  import net.grinder.console.editor.Buffer;
39  import net.grinder.console.editor.EditorModel;
40  import net.grinder.console.distribution.FileChangeWatcher;
41  import net.grinder.util.WeakValueHashMap;
42  
43  
44  /**
45   * {@link TreeModel} that walks file system.
46   *
47   * @author Philip Aston
48   */
49  final class FileTreeModel implements TreeModel {
50  
51    private final EventListenerList m_listeners = new EventListenerList();
52    private final EditorModel m_editorModel;
53    private final FileFilter m_distributionFileFilter;
54  
55    private final FilenameFilter m_directoryFilter =
56      new FilenameFilter() {
57        public boolean accept(File dir, String name) {
58          final File file = new File(dir, name);
59          return file.isDirectory() && m_distributionFileFilter.accept(file);
60        }
61      };
62  
63    private final FilenameFilter m_fileFilter =
64      new FilenameFilter() {
65        public boolean accept(File dir, String name) {
66          final File file = new File(dir, name);
67          return file.isFile() && m_distributionFileFilter.accept(file);
68        }
69      };
70  
71    /**
72     * Map from a File value to the latest Node to be created for the File.
73     */
74    private final WeakValueHashMap<File, Node> m_filesToNodes =
75      new WeakValueHashMap<File, Node>();
76  
77    /**
78     * Map from a Buffer to the FileNode that is associated with the
79     * buffer.
80     */
81    private final WeakValueHashMap<Buffer, FileNode> m_buffersToFileNodes =
82      new WeakValueHashMap<Buffer, FileNode>();
83  
84    private RootNode m_rootNode;
85  
86    FileTreeModel(EditorModel editorModel,
87                  FileFilter distributionFileFilter,
88                  File initialRootDirectory) {
89      m_editorModel = editorModel;
90      m_distributionFileFilter = distributionFileFilter;
91      setRootDirectory(initialRootDirectory);
92    }
93  
94    public void setRootDirectory(File rootDirectory) {
95      m_rootNode = new RootNode(rootDirectory);
96      fireTreeStructureChanged(m_rootNode);
97    }
98  
99    public void refresh() {
100     m_rootNode.refresh();
101     fireTreeStructureChanged(m_rootNode);
102   }
103 
104   public Object getRoot() {
105     return m_rootNode;
106   }
107 
108   public Object getChild(Object parent, int index) {
109 
110     if (parent instanceof DirectoryNode) {
111       final DirectoryNode directoryNode = (DirectoryNode)parent;
112 
113       if (directoryNode.belongsToModel(this)) {
114         return directoryNode.getChild(index);
115       }
116     }
117 
118     return null;
119   }
120 
121   public int getChildCount(Object parent) {
122 
123     if (parent instanceof DirectoryNode) {
124       final DirectoryNode directoryNode = (DirectoryNode)parent;
125 
126       if (directoryNode.belongsToModel(this)) {
127         return directoryNode.getChildCount();
128       }
129     }
130 
131     return 0;
132   }
133 
134   public int getIndexOfChild(Object parent, Object child) {
135 
136     if (parent == null || child == null) {
137       // The TreeModel Javadoc says we should do this.
138       return -1;
139     }
140 
141     if (parent instanceof DirectoryNode) {
142       final DirectoryNode directoryNode = (DirectoryNode)parent;
143 
144       if (directoryNode.belongsToModel(this)) {
145         return directoryNode.getIndexOfChild((Node)child);
146       }
147     }
148 
149     return -1;
150   }
151 
152   public boolean isLeaf(Object node) {
153     if (node instanceof FileNode) {
154       final FileNode fileNode = (FileNode)node;
155 
156       if (fileNode.belongsToModel(this)) {
157         return true;
158       }
159     }
160 
161     return false;
162   }
163 
164   public void addTreeModelListener(TreeModelListener listener) {
165     m_listeners.add(TreeModelListener.class, listener);
166   }
167 
168   public void removeTreeModelListener(TreeModelListener listener) {
169     m_listeners.remove(TreeModelListener.class, listener);
170   }
171 
172   private void fireTreeStructureChanged(Node node) {
173     final Object[] listeners = m_listeners.getListenerList();
174 
175     final TreeModelEvent event = new TreeModelEvent(this, node.getPath());
176 
177     for (int i = listeners.length - 2; i >= 0; i -= 2) {
178       ((TreeModelListener)listeners[i + 1]).treeStructureChanged(event);
179     }
180   }
181 
182   private void fireTreeNodesChanged(TreePath path) {
183     final Object[] listeners = m_listeners.getListenerList();
184 
185     final TreeModelEvent event = new TreeModelEvent(this, path);
186 
187     for (int i = listeners.length - 2; i >= 0; i -= 2) {
188       ((TreeModelListener)listeners[i + 1]).treeNodesChanged(event);
189     }
190   }
191 
192   public void valueForPathChanged(TreePath path, Object newValue) {
193     fireTreeNodesChanged(path);
194   }
195 
196   /**
197    * Find the {Node} for a file. If a particular part of the file path
198    * isn't found in the model, that part of the model is refreshed and
199    * checked again.
200    *
201    * @param file The file to find the corresponding {@link Node} for.
202    * @return The node, or <code>null</code> if the file could not be found.
203    */
204   public Node findNode(File file) {
205     final Node existingNode = m_filesToNodes.get(file);
206 
207     if (existingNode != null) {
208       return existingNode;
209     }
210 
211     // Maybe its not been expanded. Lets try harder.
212     final File[] paths = fileToArrayOfParentPaths(file);
213 
214     Node treeStructureChangedNode = null;
215 
216     for (int i = 0; i < paths.length - 1; ++i) {
217       final Node node = m_filesToNodes.get(paths[i]);
218 
219       if (node instanceof DirectoryNode) {
220         final DirectoryNode directoryNode = (DirectoryNode)node;
221 
222         if (directoryNode.getChildForFile(paths[i + 1]) == null) {
223           directoryNode.refresh();
224           treeStructureChangedNode = directoryNode;
225 
226           if (directoryNode.getChildForFile(paths[i + 1]) == null) {
227             return null;
228           }
229         }
230       }
231     }
232 
233     if (treeStructureChangedNode != null) {
234       fireTreeStructureChanged(treeStructureChangedNode);
235     }
236 
237     return m_filesToNodes.get(file);
238   }
239 
240   private File[] fileToArrayOfParentPaths(File file) {
241     final List<File> list = new ArrayList<File>();
242 
243     File f = file;
244 
245     while (f != null) {
246       list.add(f);
247       f = f.getParentFile();
248     }
249 
250     Collections.reverse(list);
251 
252     return list.toArray(new File[list.size()]);
253   }
254 
255   public FileNode findFileNode(Buffer buffer) {
256     return m_buffersToFileNodes.get(buffer);
257   }
258 
259   /**
260    * A {@link
261    * net.grinder.console.distribution.FileChangeWatcher.FileChangedListener}
262    * that listens for changed file notifications and updates the FileTreeModel
263    * appropriately.
264    *
265    */
266   public class RefreshChangedDirectoriesListener
267     implements FileChangeWatcher.FileChangedListener {
268 
269     public void filesChanged(File[] files) {
270       // Refresh the tree path for every file. We could waste time here removing
271       // duplicate refreshes, but most times they'll only be a single file.
272 
273       for (int i = 0; i < files.length; ++i) {
274         // findNode will refresh everything up to the file itself...
275         final Node node = findNode(files[i]);
276 
277         // ...so if we find a directory node, we'd better refresh that too.
278         if (node instanceof DirectoryNode) {
279           ((DirectoryNode)node).refresh();
280           fireTreeStructureChanged(node);
281         }
282       }
283     }
284   }
285 
286   /**
287    * Node in the tree.
288    */
289   public abstract class Node implements FileTree.Node {
290 
291     private final File m_file;
292     private final TreePath m_path;
293 
294     protected Node(Node parentNode, File file) {
295       m_file = file;
296 
297       if (parentNode != null) {
298         m_path = parentNode.getPath().pathByAddingChild(this);
299       }
300       else {
301         m_path = new TreePath(this);
302       }
303 
304       m_filesToNodes.put(file, this);
305     }
306 
307     public String toString() {
308       return m_file.getName();
309     }
310 
311     public Buffer getBuffer() {
312       return null;
313     }
314 
315     public final File getFile() {
316       return m_file;
317     }
318 
319     public final TreePath getPath() {
320       return m_path;
321     }
322 
323     public boolean canOpen() {
324       return false;
325     }
326 
327     boolean belongsToModel(FileTreeModel model) {
328       return FileTreeModel.this == model;
329     }
330   }
331 
332   /**
333    * Node that represents a file. Used for the leaves of the tree.
334    */
335   public final class FileNode extends Node {
336 
337     private Buffer m_buffer;
338 
339     private FileNode(DirectoryNode parentNode, File file) {
340       super(parentNode, file);
341 
342       setBuffer(m_editorModel.getBufferForFile(file));
343     }
344 
345     public void setBuffer(Buffer buffer) {
346       if (m_buffer != null) {
347         m_buffersToFileNodes.remove(m_buffer);
348       }
349 
350       m_buffer = buffer;
351 
352       if (buffer != null) {
353         m_buffersToFileNodes.put(buffer, this);
354       }
355     }
356 
357     public Buffer getBuffer() {
358       return m_buffer;
359     }
360 
361     public boolean canOpen() {
362       return true;
363     }
364   }
365 
366   /**
367    * Node that represents a directory.
368    */
369   private class DirectoryNode extends Node {
370 
371     private final File[] m_noFiles = new File[0];
372 
373     private File[] m_childDirectories = m_noFiles;
374     private DirectoryNode[] m_childDirectoryNodes;
375     private File[] m_childFiles = m_noFiles;
376     private FileNode[] m_childFileNodes;
377 
378     DirectoryNode(DirectoryNode parentNode, File file) {
379       super(parentNode, file);
380 
381       refresh();
382     }
383 
384     public void refresh() {
385       for (int i = 0; i < m_childDirectories.length; ++i) {
386         final DirectoryNode oldDirectoryNode =
387           (DirectoryNode)m_filesToNodes.remove(m_childDirectories[i]);
388 
389         // Can be null if a DirectoryNode has never been created for the
390         // directory.
391         if (oldDirectoryNode != null) {
392           oldDirectoryNode.refresh();
393         }
394       }
395 
396       for (int i = 0; i < m_childFiles.length; ++i) {
397         final FileNode oldFileNode =
398           (FileNode)m_filesToNodes.remove(m_childFiles[i]);
399 
400         if (oldFileNode != null) {
401           oldFileNode.setBuffer(null);
402         }
403       }
404 
405       final File[] directories = getFile().listFiles(m_directoryFilter);
406 
407       if (directories != null) {
408         Arrays.sort(directories);
409         m_childDirectories = directories;
410       }
411       else {
412         m_childDirectories = m_noFiles;
413       }
414 
415       m_childDirectoryNodes = new DirectoryNode[m_childDirectories.length];
416 
417       final File[] files = getFile().listFiles(m_fileFilter);
418 
419       if (files != null) {
420         Arrays.sort(files);
421         m_childFiles = files;
422       }
423       else {
424         m_childFiles = m_noFiles;
425       }
426 
427       m_childFileNodes = new FileNode[m_childFiles.length];
428     }
429 
430     final Node getChildForFile(File file) {
431       if (file.isDirectory()) {
432         for (int i = 0; i < m_childDirectories.length; ++i) {
433           if (m_childDirectories[i].equals(file)) {
434             return getChild(i);
435           }
436         }
437       }
438       else {
439         for (int i = 0; i < m_childFiles.length; ++i) {
440           if (m_childFiles[i].equals(file)) {
441             return getChild(i + m_childDirectories.length);
442           }
443         }
444       }
445 
446       // Not known here.
447       return null;
448     }
449 
450     public final Node getChild(int index) {
451       if (index < m_childDirectories.length) {
452         if (m_childDirectoryNodes[index] == null) {
453           m_childDirectoryNodes[index] =
454             new DirectoryNode(this, m_childDirectories[index]);
455         }
456 
457         return m_childDirectoryNodes[index];
458       }
459       else if (index < m_childDirectories.length + m_childFiles.length) {
460         final int fileIndex = index - m_childDirectories.length;
461 
462         if (m_childFileNodes[fileIndex] == null) {
463           m_childFileNodes[fileIndex] =
464             new FileNode(this, m_childFiles[fileIndex]);
465         }
466 
467         return m_childFileNodes[fileIndex];
468       }
469       else {
470         return null;
471       }
472     }
473 
474     public final int getChildCount() {
475       return m_childDirectories.length + m_childFiles.length;
476     }
477 
478     public final int getIndexOfChild(Node child) {
479       for (int i = 0; i < m_childDirectories.length; ++i) {
480         if (m_childDirectories[i].equals(child.getFile())) {
481           return i;
482         }
483       }
484 
485       for (int i = 0; i < m_childFiles.length; ++i) {
486         if (m_childFiles[i].equals(child.getFile())) {
487           return m_childDirectories.length + i;
488         }
489       }
490 
491       return -1;
492     }
493   }
494 
495   /**
496    * Root node of the tree.
497    */
498   private final class RootNode extends DirectoryNode {
499 
500     private RootNode(File file) {
501       super(null, file);
502     }
503 
504     public String toString() {
505       return getFile().getPath();
506     }
507   }
508 }