1 /*
   2  * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javax.swing.plaf.basic;
  27 
  28 import java.io.File;
  29 import java.util.*;
  30 import java.util.concurrent.Callable;
  31 import javax.swing.*;
  32 import javax.swing.filechooser.*;
  33 import javax.swing.event.*;
  34 import java.beans.*;
  35 
  36 import sun.awt.shell.ShellFolder;
  37 
  38 /**
  39  * Basic implementation of a file list.
  40  *
  41  * @author Jeff Dinkins
  42  */
  43 @SuppressWarnings("serial") // Superclass is not serializable across versions
  44 public class BasicDirectoryModel extends AbstractListModel<Object> implements PropertyChangeListener {
  45 
  46     private JFileChooser filechooser = null;
  47     // PENDING(jeff) pick the size more sensibly
  48     private Vector<File> fileCache = new Vector<File>(50);
  49     private LoadFilesThread loadThread = null;
  50     private Vector<File> files = null;
  51     private Vector<File> directories = null;
  52     private int fetchID = 0;
  53 
  54     private PropertyChangeSupport changeSupport;
  55 
  56     private boolean busy = false;
  57 
  58     public BasicDirectoryModel(JFileChooser filechooser) {
  59         this.filechooser = filechooser;
  60         validateFileCache();
  61     }
  62 
  63     public void propertyChange(PropertyChangeEvent e) {
  64         String prop = e.getPropertyName();
  65         if(prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY ||
  66            prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY ||
  67            prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY ||
  68            prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY ||
  69            prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) {
  70             validateFileCache();
  71         } else if ("UI".equals(prop)) {
  72             Object old = e.getOldValue();
  73             if (old instanceof BasicFileChooserUI) {
  74                 BasicFileChooserUI ui = (BasicFileChooserUI) old;
  75                 BasicDirectoryModel model = ui.getModel();
  76                 if (model != null) {
  77                     model.invalidateFileCache();
  78                 }
  79             }
  80         } else if ("JFileChooserDialogIsClosingProperty".equals(prop)) {
  81             invalidateFileCache();
  82         }
  83     }
  84 
  85     /**
  86      * This method is used to interrupt file loading thread.
  87      */
  88     public void invalidateFileCache() {
  89         if (loadThread != null) {
  90             loadThread.interrupt();
  91             loadThread.cancelRunnables();
  92             loadThread = null;
  93         }
  94     }
  95 
  96     public Vector<File> getDirectories() {
  97         synchronized(fileCache) {
  98             if (directories != null) {
  99                 return directories;
 100             }
 101             Vector fls = getFiles();
 102             return directories;
 103         }
 104     }
 105 
 106     public Vector<File> getFiles() {
 107         synchronized(fileCache) {
 108             if (files != null) {
 109                 return files;
 110             }
 111             files = new Vector<File>();
 112             directories = new Vector<File>();
 113             directories.addElement(filechooser.getFileSystemView().createFileObject(
 114                 filechooser.getCurrentDirectory(), "..")
 115             );
 116 
 117             for (int i = 0; i < getSize(); i++) {
 118                 File f = fileCache.get(i);
 119                 if (filechooser.isTraversable(f)) {
 120                     directories.add(f);
 121                 } else {
 122                     files.add(f);
 123                 }
 124             }
 125             return files;
 126         }
 127     }
 128 
 129     public void validateFileCache() {
 130         File currentDirectory = filechooser.getCurrentDirectory();
 131         if (currentDirectory == null) {
 132             return;
 133         }
 134         if (loadThread != null) {
 135             loadThread.interrupt();
 136             loadThread.cancelRunnables();
 137         }
 138 
 139         setBusy(true, ++fetchID);
 140 
 141         loadThread = new LoadFilesThread(currentDirectory, fetchID);
 142         loadThread.start();
 143     }
 144 
 145     /**
 146      * Renames a file in the underlying file system.
 147      *
 148      * @param oldFile a <code>File</code> object representing
 149      *        the existing file
 150      * @param newFile a <code>File</code> object representing
 151      *        the desired new file name
 152      * @return <code>true</code> if rename succeeded,
 153      *        otherwise <code>false</code>
 154      * @since 1.4
 155      */
 156     public boolean renameFile(File oldFile, File newFile) {
 157         synchronized(fileCache) {
 158             if (oldFile.renameTo(newFile)) {
 159                 validateFileCache();
 160                 return true;
 161             }
 162             return false;
 163         }
 164     }
 165 
 166 
 167     public void fireContentsChanged() {
 168         // System.out.println("BasicDirectoryModel: firecontentschanged");
 169         fireContentsChanged(this, 0, getSize()-1);
 170     }
 171 
 172     public int getSize() {
 173         return fileCache.size();
 174     }
 175 
 176     public boolean contains(Object o) {
 177         return fileCache.contains(o);
 178     }
 179 
 180     public int indexOf(Object o) {
 181         return fileCache.indexOf(o);
 182     }
 183 
 184     public Object getElementAt(int index) {
 185         return fileCache.get(index);
 186     }
 187 
 188     /**
 189      * Obsolete - not used.
 190      */
 191     public void intervalAdded(ListDataEvent e) {
 192     }
 193 
 194     /**
 195      * Obsolete - not used.
 196      */
 197     public void intervalRemoved(ListDataEvent e) {
 198     }
 199 
 200     protected void sort(Vector<? extends File> v){
 201         ShellFolder.sort(v);
 202     }
 203 
 204     // Obsolete - not used
 205     protected boolean lt(File a, File b) {
 206         // First ignore case when comparing
 207         int diff = a.getName().toLowerCase().compareTo(b.getName().toLowerCase());
 208         if (diff != 0) {
 209             return diff < 0;
 210         } else {
 211             // May differ in case (e.g. "mail" vs. "Mail")
 212             return a.getName().compareTo(b.getName()) < 0;
 213         }
 214     }
 215 
 216 
 217     class LoadFilesThread extends Thread {
 218         File currentDirectory = null;
 219         int fid;
 220         Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10);
 221 
 222         public LoadFilesThread(File currentDirectory, int fid) {
 223             super("Basic L&F File Loading Thread");
 224             this.currentDirectory = currentDirectory;
 225             this.fid = fid;
 226         }
 227 
 228         public void run() {
 229             run0();
 230             setBusy(false, fid);
 231         }
 232 
 233         public void run0() {
 234             FileSystemView fileSystem = filechooser.getFileSystemView();
 235 
 236             if (isInterrupted()) {
 237                 return;
 238             }
 239 
 240             File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled());
 241 
 242             if (isInterrupted()) {
 243                 return;
 244             }
 245 
 246             final Vector<File> newFileCache = new Vector<File>();
 247             Vector<File> newFiles = new Vector<File>();
 248 
 249             // run through the file list, add directories and selectable files to fileCache
 250             // Note that this block must be OUTSIDE of Invoker thread because of
 251             // deadlock possibility with custom synchronized FileSystemView
 252             for (File file : list) {
 253                 if (filechooser.accept(file)) {
 254                     boolean isTraversable = filechooser.isTraversable(file);
 255 
 256                     if (isTraversable) {
 257                         newFileCache.addElement(file);
 258                     } else if (filechooser.isFileSelectionEnabled()) {
 259                         newFiles.addElement(file);
 260                     }
 261 
 262                     if (isInterrupted()) {
 263                         return;
 264                     }
 265                 }
 266             }
 267 
 268             // First sort alphabetically by filename
 269             sort(newFileCache);
 270             sort(newFiles);
 271 
 272             newFileCache.addAll(newFiles);
 273 
 274             // To avoid loads of synchronizations with Invoker and improve performance we
 275             // execute the whole block on the COM thread
 276             DoChangeContents doChangeContents = ShellFolder.invoke(new Callable<DoChangeContents>() {
 277                 public DoChangeContents call() {
 278                     int newSize = newFileCache.size();
 279                     int oldSize = fileCache.size();
 280 
 281                     if (newSize > oldSize) {
 282                         //see if interval is added
 283                         int start = oldSize;
 284                         int end = newSize;
 285                         for (int i = 0; i < oldSize; i++) {
 286                             if (!newFileCache.get(i).equals(fileCache.get(i))) {
 287                                 start = i;
 288                                 for (int j = i; j < newSize; j++) {
 289                                     if (newFileCache.get(j).equals(fileCache.get(i))) {
 290                                         end = j;
 291                                         break;
 292                                     }
 293                                 }
 294                                 break;
 295                             }
 296                         }
 297                         if (start >= 0 && end > start
 298                             && newFileCache.subList(end, newSize).equals(fileCache.subList(start, oldSize))) {
 299                             if (isInterrupted()) {
 300                                 return null;
 301                             }
 302                             return new DoChangeContents(newFileCache.subList(start, end), start, null, 0, fid);
 303                         }
 304                     } else if (newSize < oldSize) {
 305                         //see if interval is removed
 306                         int start = -1;
 307                         int end = -1;
 308                         for (int i = 0; i < newSize; i++) {
 309                             if (!newFileCache.get(i).equals(fileCache.get(i))) {
 310                                 start = i;
 311                                 end = i + oldSize - newSize;
 312                                 break;
 313                             }
 314                         }
 315                         if (start >= 0 && end > start
 316                             && fileCache.subList(end, oldSize).equals(newFileCache.subList(start, newSize))) {
 317                             if (isInterrupted()) {
 318                                 return null;
 319                             }
 320                             return new DoChangeContents(null, 0, new Vector(fileCache.subList(start, end)), start, fid);
 321                         }
 322                     }
 323                     if (!fileCache.equals(newFileCache)) {
 324                         if (isInterrupted()) {
 325                             cancelRunnables(runnables);
 326                         }
 327                         return new DoChangeContents(newFileCache, 0, fileCache, 0, fid);
 328                     }
 329                     return null;
 330                 }
 331             });
 332 
 333             if (doChangeContents != null) {
 334                 runnables.addElement(doChangeContents);
 335                 SwingUtilities.invokeLater(doChangeContents);
 336             }
 337         }
 338 
 339 
 340         public void cancelRunnables(Vector<DoChangeContents> runnables) {
 341             for (DoChangeContents runnable : runnables) {
 342                 runnable.cancel();
 343             }
 344         }
 345 
 346         public void cancelRunnables() {
 347             cancelRunnables(runnables);
 348         }
 349    }
 350 
 351 
 352     /**
 353      * Adds a PropertyChangeListener to the listener list. The listener is
 354      * registered for all bound properties of this class.
 355      * <p>
 356      * If <code>listener</code> is <code>null</code>,
 357      * no exception is thrown and no action is performed.
 358      *
 359      * @param    listener  the property change listener to be added
 360      *
 361      * @see #removePropertyChangeListener
 362      * @see #getPropertyChangeListeners
 363      *
 364      * @since 1.6
 365      */
 366     public void addPropertyChangeListener(PropertyChangeListener listener) {
 367         if (changeSupport == null) {
 368             changeSupport = new PropertyChangeSupport(this);
 369         }
 370         changeSupport.addPropertyChangeListener(listener);
 371     }
 372 
 373     /**
 374      * Removes a PropertyChangeListener from the listener list.
 375      * <p>
 376      * If listener is null, no exception is thrown and no action is performed.
 377      *
 378      * @param listener the PropertyChangeListener to be removed
 379      *
 380      * @see #addPropertyChangeListener
 381      * @see #getPropertyChangeListeners
 382      *
 383      * @since 1.6
 384      */
 385     public void removePropertyChangeListener(PropertyChangeListener listener) {
 386         if (changeSupport != null) {
 387             changeSupport.removePropertyChangeListener(listener);
 388         }
 389     }
 390 
 391     /**
 392      * Returns an array of all the property change listeners
 393      * registered on this component.
 394      *
 395      * @return all of this component's <code>PropertyChangeListener</code>s
 396      *         or an empty array if no property change
 397      *         listeners are currently registered
 398      *
 399      * @see      #addPropertyChangeListener
 400      * @see      #removePropertyChangeListener
 401      * @see      java.beans.PropertyChangeSupport#getPropertyChangeListeners
 402      *
 403      * @since 1.6
 404      */
 405     public PropertyChangeListener[] getPropertyChangeListeners() {
 406         if (changeSupport == null) {
 407             return new PropertyChangeListener[0];
 408         }
 409         return changeSupport.getPropertyChangeListeners();
 410     }
 411 
 412     /**
 413      * Support for reporting bound property changes for boolean properties.
 414      * This method can be called when a bound property has changed and it will
 415      * send the appropriate PropertyChangeEvent to any registered
 416      * PropertyChangeListeners.
 417      *
 418      * @param propertyName the property whose value has changed
 419      * @param oldValue the property's previous value
 420      * @param newValue the property's new value
 421      *
 422      * @since 1.6
 423      */
 424     protected void firePropertyChange(String propertyName,
 425                                       Object oldValue, Object newValue) {
 426         if (changeSupport != null) {
 427             changeSupport.firePropertyChange(propertyName,
 428                                              oldValue, newValue);
 429         }
 430     }
 431 
 432 
 433     /**
 434      * Set the busy state for the model. The model is considered
 435      * busy when it is running a separate (interruptable)
 436      * thread in order to load the contents of a directory.
 437      */
 438     private synchronized void setBusy(final boolean busy, int fid) {
 439         if (fid == fetchID) {
 440             boolean oldValue = this.busy;
 441             this.busy = busy;
 442 
 443             if (changeSupport != null && busy != oldValue) {
 444                 SwingUtilities.invokeLater(new Runnable() {
 445                     public void run() {
 446                         firePropertyChange("busy", !busy, busy);
 447                     }
 448                 });
 449             }
 450         }
 451     }
 452 
 453 
 454     class DoChangeContents implements Runnable {
 455         private List<File> addFiles;
 456         private List<File> remFiles;
 457         private boolean doFire = true;
 458         private int fid;
 459         private int addStart = 0;
 460         private int remStart = 0;
 461 
 462         public DoChangeContents(List<File> addFiles, int addStart, List<File> remFiles, int remStart, int fid) {
 463             this.addFiles = addFiles;
 464             this.addStart = addStart;
 465             this.remFiles = remFiles;
 466             this.remStart = remStart;
 467             this.fid = fid;
 468         }
 469 
 470         synchronized void cancel() {
 471                 doFire = false;
 472         }
 473 
 474         public synchronized void run() {
 475             if (fetchID == fid && doFire) {
 476                 int remSize = (remFiles == null) ? 0 : remFiles.size();
 477                 int addSize = (addFiles == null) ? 0 : addFiles.size();
 478                 synchronized(fileCache) {
 479                     if (remSize > 0) {
 480                         fileCache.removeAll(remFiles);
 481                     }
 482                     if (addSize > 0) {
 483                         fileCache.addAll(addStart, addFiles);
 484                     }
 485                     files = null;
 486                     directories = null;
 487                 }
 488                 if (remSize > 0 && addSize == 0) {
 489                     fireIntervalRemoved(BasicDirectoryModel.this, remStart, remStart + remSize - 1);
 490                 } else if (addSize > 0 && remSize == 0 && addStart + addSize <= fileCache.size()) {
 491                     fireIntervalAdded(BasicDirectoryModel.this, addStart, addStart + addSize - 1);
 492                 } else {
 493                     fireContentsChanged();
 494                 }
 495             }
 496         }
 497     }
 498 }