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