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      * @param e list data event
 223      */
 224     public void intervalAdded(ListDataEvent e) {
 225     }
 226 
 227     /**
 228      * Obsolete - not used.
 229      * @param e list data event
 230      */
 231     public void intervalRemoved(ListDataEvent e) {
 232     }
 233 
 234     /**
 235      * Sorts a list of files.
 236      *
 237      * @param v a list of files
 238      */
 239     protected void sort(Vector<? extends File> v){
 240         ShellFolder.sort(v);
 241     }
 242 
 243     /**
 244      * Obsolete - not used
 245      * @return a comparison of the file names
 246      * @param a a file
 247      * @param b another file
 248      */
 249     protected boolean lt(File a, File b) {
 250         // First ignore case when comparing
 251         int diff = a.getName().toLowerCase().compareTo(b.getName().toLowerCase());
 252         if (diff != 0) {
 253             return diff < 0;
 254         } else {
 255             // May differ in case (e.g. "mail" vs. "Mail")
 256             return a.getName().compareTo(b.getName()) < 0;
 257         }
 258     }
 259 
 260 
 261     class LoadFilesThread extends Thread {
 262         File currentDirectory = null;
 263         int fid;
 264         Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10);
 265 
 266         public LoadFilesThread(File currentDirectory, int fid) {
 267             super("Basic L&F File Loading Thread");
 268             this.currentDirectory = currentDirectory;
 269             this.fid = fid;
 270         }
 271 
 272         public void run() {
 273             run0();
 274             setBusy(false, fid);
 275         }
 276 
 277         public void run0() {
 278             FileSystemView fileSystem = filechooser.getFileSystemView();
 279 
 280             if (isInterrupted()) {
 281                 return;
 282             }
 283 
 284             File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled());
 285 
 286             if (isInterrupted()) {
 287                 return;
 288             }
 289 
 290             final Vector<File> newFileCache = new Vector<File>();
 291             Vector<File> newFiles = new Vector<File>();
 292 
 293             // run through the file list, add directories and selectable files to fileCache
 294             // Note that this block must be OUTSIDE of Invoker thread because of
 295             // deadlock possibility with custom synchronized FileSystemView
 296             for (File file : list) {
 297                 if (filechooser.accept(file)) {
 298                     boolean isTraversable = filechooser.isTraversable(file);
 299 
 300                     if (isTraversable) {
 301                         newFileCache.addElement(file);
 302                     } else if (filechooser.isFileSelectionEnabled()) {
 303                         newFiles.addElement(file);
 304                     }
 305 
 306                     if (isInterrupted()) {
 307                         return;
 308                     }
 309                 }
 310             }
 311 
 312             // First sort alphabetically by filename
 313             sort(newFileCache);
 314             sort(newFiles);
 315 
 316             newFileCache.addAll(newFiles);
 317 
 318             // To avoid loads of synchronizations with Invoker and improve performance we
 319             // execute the whole block on the COM thread
 320             DoChangeContents doChangeContents = ShellFolder.invoke(new Callable<DoChangeContents>() {
 321                 public DoChangeContents call() {
 322                     int newSize = newFileCache.size();
 323                     int oldSize = fileCache.size();
 324 
 325                     if (newSize > oldSize) {
 326                         //see if interval is added
 327                         int start = oldSize;
 328                         int end = newSize;
 329                         for (int i = 0; i < oldSize; i++) {
 330                             if (!newFileCache.get(i).equals(fileCache.get(i))) {
 331                                 start = i;
 332                                 for (int j = i; j < newSize; j++) {
 333                                     if (newFileCache.get(j).equals(fileCache.get(i))) {
 334                                         end = j;
 335                                         break;
 336                                     }
 337                                 }
 338                                 break;
 339                             }
 340                         }
 341                         if (start >= 0 && end > start
 342                             && newFileCache.subList(end, newSize).equals(fileCache.subList(start, oldSize))) {
 343                             if (isInterrupted()) {
 344                                 return null;
 345                             }
 346                             return new DoChangeContents(newFileCache.subList(start, end), start, null, 0, fid);
 347                         }
 348                     } else if (newSize < oldSize) {
 349                         //see if interval is removed
 350                         int start = -1;
 351                         int end = -1;
 352                         for (int i = 0; i < newSize; i++) {
 353                             if (!newFileCache.get(i).equals(fileCache.get(i))) {
 354                                 start = i;
 355                                 end = i + oldSize - newSize;
 356                                 break;
 357                             }
 358                         }
 359                         if (start >= 0 && end > start
 360                             && fileCache.subList(end, oldSize).equals(newFileCache.subList(start, newSize))) {
 361                             if (isInterrupted()) {
 362                                 return null;
 363                             }
 364                             return new DoChangeContents(null, 0, new Vector<>(fileCache.subList(start, end)), start, fid);
 365                         }
 366                     }
 367                     if (!fileCache.equals(newFileCache)) {
 368                         if (isInterrupted()) {
 369                             cancelRunnables(runnables);
 370                         }
 371                         return new DoChangeContents(newFileCache, 0, fileCache, 0, fid);
 372                     }
 373                     return null;
 374                 }
 375             });
 376 
 377             if (doChangeContents != null) {
 378                 runnables.addElement(doChangeContents);
 379                 SwingUtilities.invokeLater(doChangeContents);
 380             }
 381         }
 382 
 383 
 384         public void cancelRunnables(Vector<DoChangeContents> runnables) {
 385             for (DoChangeContents runnable : runnables) {
 386                 runnable.cancel();
 387             }
 388         }
 389 
 390         public void cancelRunnables() {
 391             cancelRunnables(runnables);
 392         }
 393    }
 394 
 395 
 396     /**
 397      * Adds a PropertyChangeListener to the listener list. The listener is
 398      * registered for all bound properties of this class.
 399      * <p>
 400      * If <code>listener</code> is <code>null</code>,
 401      * no exception is thrown and no action is performed.
 402      *
 403      * @param    listener  the property change listener to be added
 404      *
 405      * @see #removePropertyChangeListener
 406      * @see #getPropertyChangeListeners
 407      *
 408      * @since 1.6
 409      */
 410     public void addPropertyChangeListener(PropertyChangeListener listener) {
 411         if (changeSupport == null) {
 412             changeSupport = new PropertyChangeSupport(this);
 413         }
 414         changeSupport.addPropertyChangeListener(listener);
 415     }
 416 
 417     /**
 418      * Removes a PropertyChangeListener from the listener list.
 419      * <p>
 420      * If listener is null, no exception is thrown and no action is performed.
 421      *
 422      * @param listener the PropertyChangeListener to be removed
 423      *
 424      * @see #addPropertyChangeListener
 425      * @see #getPropertyChangeListeners
 426      *
 427      * @since 1.6
 428      */
 429     public void removePropertyChangeListener(PropertyChangeListener listener) {
 430         if (changeSupport != null) {
 431             changeSupport.removePropertyChangeListener(listener);
 432         }
 433     }
 434 
 435     /**
 436      * Returns an array of all the property change listeners
 437      * registered on this component.
 438      *
 439      * @return all of this component's <code>PropertyChangeListener</code>s
 440      *         or an empty array if no property change
 441      *         listeners are currently registered
 442      *
 443      * @see      #addPropertyChangeListener
 444      * @see      #removePropertyChangeListener
 445      * @see      java.beans.PropertyChangeSupport#getPropertyChangeListeners
 446      *
 447      * @since 1.6
 448      */
 449     public PropertyChangeListener[] getPropertyChangeListeners() {
 450         if (changeSupport == null) {
 451             return new PropertyChangeListener[0];
 452         }
 453         return changeSupport.getPropertyChangeListeners();
 454     }
 455 
 456     /**
 457      * Support for reporting bound property changes for boolean properties.
 458      * This method can be called when a bound property has changed and it will
 459      * send the appropriate PropertyChangeEvent to any registered
 460      * PropertyChangeListeners.
 461      *
 462      * @param propertyName the property whose value has changed
 463      * @param oldValue the property's previous value
 464      * @param newValue the property's new value
 465      *
 466      * @since 1.6
 467      */
 468     protected void firePropertyChange(String propertyName,
 469                                       Object oldValue, Object newValue) {
 470         if (changeSupport != null) {
 471             changeSupport.firePropertyChange(propertyName,
 472                                              oldValue, newValue);
 473         }
 474     }
 475 
 476 
 477     /**
 478      * Set the busy state for the model. The model is considered
 479      * busy when it is running a separate (interruptable)
 480      * thread in order to load the contents of a directory.
 481      */
 482     private synchronized void setBusy(final boolean busy, int fid) {
 483         if (fid == fetchID) {
 484             boolean oldValue = this.busy;
 485             this.busy = busy;
 486 
 487             if (changeSupport != null && busy != oldValue) {
 488                 SwingUtilities.invokeLater(new Runnable() {
 489                     public void run() {
 490                         firePropertyChange("busy", !busy, busy);
 491                     }
 492                 });
 493             }
 494         }
 495     }
 496 
 497 
 498     class DoChangeContents implements Runnable {
 499         private List<File> addFiles;
 500         private List<File> remFiles;
 501         private boolean doFire = true;
 502         private int fid;
 503         private int addStart = 0;
 504         private int remStart = 0;
 505 
 506         public DoChangeContents(List<File> addFiles, int addStart, List<File> remFiles, int remStart, int fid) {
 507             this.addFiles = addFiles;
 508             this.addStart = addStart;
 509             this.remFiles = remFiles;
 510             this.remStart = remStart;
 511             this.fid = fid;
 512         }
 513 
 514         synchronized void cancel() {
 515                 doFire = false;
 516         }
 517 
 518         public synchronized void run() {
 519             if (fetchID == fid && doFire) {
 520                 int remSize = (remFiles == null) ? 0 : remFiles.size();
 521                 int addSize = (addFiles == null) ? 0 : addFiles.size();
 522                 synchronized(fileCache) {
 523                     if (remSize > 0) {
 524                         fileCache.removeAll(remFiles);
 525                     }
 526                     if (addSize > 0) {
 527                         fileCache.addAll(addStart, addFiles);
 528                     }
 529                     files = null;
 530                     directories = null;
 531                 }
 532                 if (remSize > 0 && addSize == 0) {
 533                     fireIntervalRemoved(BasicDirectoryModel.this, remStart, remStart + remSize - 1);
 534                 } else if (addSize > 0 && remSize == 0 && addStart + addSize <= fileCache.size()) {
 535                     fireIntervalAdded(BasicDirectoryModel.this, addStart, addStart + addSize - 1);
 536                 } else {
 537                     fireContentsChanged();
 538                 }
 539             }
 540         }
 541     }
 542 }