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