1 /*
   2  * Copyright (c) 2003, 2018, 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 sun.awt.shell;
  27 
  28 import java.awt.*;
  29 import java.awt.image.BufferedImage;
  30 import java.awt.image.BaseMultiResolutionImage;
  31 
  32 import java.io.File;
  33 import java.io.FileNotFoundException;
  34 import java.io.IOException;
  35 import java.security.AccessController;
  36 import java.security.PrivilegedAction;
  37 import java.util.*;
  38 import java.util.List;
  39 import java.util.concurrent.*;
  40 import java.util.stream.Stream;
  41 
  42 import static sun.awt.shell.Win32ShellFolder2.*;
  43 import sun.awt.OSInfo;
  44 import sun.awt.util.ThreadGroupUtils;
  45 // NOTE: This class supersedes Win32ShellFolderManager, which was removed
  46 //       from distribution after version 1.4.2.
  47 
  48 /**
  49  * @author Michael Martak
  50  * @author Leif Samuelsson
  51  * @author Kenneth Russell
  52  * @since 1.4
  53  */
  54 
  55 final class Win32ShellFolderManager2 extends ShellFolderManager {
  56 
  57     static {
  58         // Load library here
  59         sun.awt.windows.WToolkit.loadLibraries();
  60     }
  61 
  62     public ShellFolder createShellFolder(File file) throws FileNotFoundException {
  63         try {
  64             return createShellFolder(getDesktop(), file);
  65         } catch (InterruptedException e) {
  66             throw new FileNotFoundException("Execution was interrupted");
  67         }
  68     }
  69 
  70     static Win32ShellFolder2 createShellFolder(Win32ShellFolder2 parent, File file)
  71             throws FileNotFoundException, InterruptedException {
  72         long pIDL;
  73         try {
  74             pIDL = parent.parseDisplayName(file.getCanonicalPath());
  75         } catch (IOException ex) {
  76             pIDL = 0;
  77         }
  78         if (pIDL == 0) {
  79             // Shouldn't happen but watch for it anyway
  80             throw new FileNotFoundException("File " + file.getAbsolutePath() + " not found");
  81         }
  82 
  83         try {
  84             return createShellFolderFromRelativePIDL(parent, pIDL);
  85         } finally {
  86             Win32ShellFolder2.releasePIDL(pIDL);
  87         }
  88     }
  89 
  90     static Win32ShellFolder2 createShellFolderFromRelativePIDL(Win32ShellFolder2 parent, long pIDL)
  91             throws InterruptedException {
  92         // Walk down this relative pIDL, creating new nodes for each of the entries
  93         while (pIDL != 0) {
  94             long curPIDL = Win32ShellFolder2.copyFirstPIDLEntry(pIDL);
  95             if (curPIDL != 0) {
  96                 parent = Win32ShellFolder2.createShellFolder(parent, curPIDL);
  97                 pIDL = Win32ShellFolder2.getNextPIDLEntry(pIDL);
  98             } else {
  99                 // The list is empty if the parent is Desktop and pIDL is a shortcut to Desktop
 100                 break;
 101             }
 102         }
 103         return parent;
 104     }
 105 
 106     private static final int VIEW_LIST = 2;
 107     private static final int VIEW_DETAILS = 3;
 108     private static final int VIEW_PARENTFOLDER = 8;
 109     private static final int VIEW_NEWFOLDER = 11;
 110 
 111     private static final Image[] STANDARD_VIEW_BUTTONS = new Image[12];
 112 
 113     private static Image getStandardViewButton(int iconIndex) {
 114         Image result = STANDARD_VIEW_BUTTONS[iconIndex];
 115 
 116         if (result != null) {
 117             return result;
 118         }
 119 
 120         final int[] iconBits = Win32ShellFolder2
 121                 .getStandardViewButton0(iconIndex, true);
 122         if (iconBits != null) {
 123             // icons are always square
 124             final int size = (int) Math.sqrt(iconBits.length);
 125             final BufferedImage img =
 126                     new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
 127             img.setRGB(0, 0, size, size, iconBits, 0, size);
 128 
 129             STANDARD_VIEW_BUTTONS[iconIndex] = (size == SMALL_ICON_SIZE)
 130                     ? img
 131                     : new MultiResolutionIconImage(SMALL_ICON_SIZE, img);
 132         }
 133 
 134         return STANDARD_VIEW_BUTTONS[iconIndex];
 135     }
 136 
 137     // Special folders
 138     private static Win32ShellFolder2 desktop;
 139     private static Win32ShellFolder2 drives;
 140     private static Win32ShellFolder2 recent;
 141     private static Win32ShellFolder2 network;
 142     private static Win32ShellFolder2 personal;
 143 
 144     static Win32ShellFolder2 getDesktop() {
 145         if (desktop == null) {
 146             try {
 147                 desktop = new Win32ShellFolder2(DESKTOP);
 148             } catch (SecurityException e) {
 149                 // Ignore error
 150             } catch (IOException e) {
 151                 // Ignore error
 152             } catch (InterruptedException e) {
 153                 // Ignore error
 154             }
 155         }
 156         return desktop;
 157     }
 158 
 159     static Win32ShellFolder2 getDrives() {
 160         if (drives == null) {
 161             try {
 162                 drives = new Win32ShellFolder2(DRIVES);
 163             } catch (SecurityException e) {
 164                 // Ignore error
 165             } catch (IOException e) {
 166                 // Ignore error
 167             } catch (InterruptedException e) {
 168                 // Ignore error
 169             }
 170         }
 171         return drives;
 172     }
 173 
 174     static Win32ShellFolder2 getRecent() {
 175         if (recent == null) {
 176             try {
 177                 String path = Win32ShellFolder2.getFileSystemPath(RECENT);
 178                 if (path != null) {
 179                     recent = createShellFolder(getDesktop(), new File(path));
 180                 }
 181             } catch (SecurityException e) {
 182                 // Ignore error
 183             } catch (InterruptedException e) {
 184                 // Ignore error
 185             } catch (IOException e) {
 186                 // Ignore error
 187             }
 188         }
 189         return recent;
 190     }
 191 
 192     static Win32ShellFolder2 getNetwork() {
 193         if (network == null) {
 194             try {
 195                 network = new Win32ShellFolder2(NETWORK);
 196             } catch (SecurityException e) {
 197                 // Ignore error
 198             } catch (IOException e) {
 199                 // Ignore error
 200             } catch (InterruptedException e) {
 201                 // Ignore error
 202             }
 203         }
 204         return network;
 205     }
 206 
 207     static Win32ShellFolder2 getPersonal() {
 208         if (personal == null) {
 209             try {
 210                 String path = Win32ShellFolder2.getFileSystemPath(PERSONAL);
 211                 if (path != null) {
 212                     Win32ShellFolder2 desktop = getDesktop();
 213                     personal = desktop.getChildByPath(path);
 214                     if (personal == null) {
 215                         personal = createShellFolder(getDesktop(), new File(path));
 216                     }
 217                     if (personal != null) {
 218                         personal.setIsPersonal();
 219                     }
 220                 }
 221             } catch (SecurityException e) {
 222                 // Ignore error
 223             } catch (InterruptedException e) {
 224                 // Ignore error
 225             } catch (IOException e) {
 226                 // Ignore error
 227             }
 228         }
 229         return personal;
 230     }
 231 
 232 
 233     private static File[] roots;
 234 
 235     /**
 236      * @param key a {@code String}
 237      *  "fileChooserDefaultFolder":
 238      *    Returns a {@code File} - the default shellfolder for a new filechooser
 239      *  "roots":
 240      *    Returns a {@code File[]} - containing the root(s) of the displayable hierarchy
 241      *  "fileChooserComboBoxFolders":
 242      *    Returns a {@code File[]} - an array of shellfolders representing the list to
 243      *    show by default in the file chooser's combobox
 244      *   "fileChooserShortcutPanelFolders":
 245      *    Returns a {@code File[]} - an array of shellfolders representing well-known
 246      *    folders, such as Desktop, Documents, History, Network, Home, etc.
 247      *    This is used in the shortcut panel of the filechooser on Windows 2000
 248      *    and Windows Me.
 249      *  "fileChooserIcon <icon>":
 250      *    Returns an {@code Image} - icon can be ListView, DetailsView, UpFolder, NewFolder or
 251      *    ViewMenu (Windows only).
 252      *  "optionPaneIcon iconName":
 253      *    Returns an {@code Image} - icon from the system icon list
 254      *
 255      * @return An Object matching the key string.
 256      */
 257     public Object get(String key) {
 258         if (key.equals("fileChooserDefaultFolder")) {
 259             File file = getPersonal();
 260             if (file == null) {
 261                 file = getDesktop();
 262             }
 263             return checkFile(file);
 264         } else if (key.equals("roots")) {
 265             // Should be "History" and "Desktop" ?
 266             if (roots == null) {
 267                 File desktop = getDesktop();
 268                 if (desktop != null) {
 269                     roots = new File[] { desktop };
 270                 } else {
 271                     roots = (File[])super.get(key);
 272                 }
 273             }
 274             return checkFiles(roots);
 275         } else if (key.equals("fileChooserComboBoxFolders")) {
 276             Win32ShellFolder2 desktop = getDesktop();
 277 
 278             if (desktop != null && checkFile(desktop) != null) {
 279                 ArrayList<File> folders = new ArrayList<File>();
 280                 Win32ShellFolder2 drives = getDrives();
 281 
 282                 Win32ShellFolder2 recentFolder = getRecent();
 283                 if (recentFolder != null && OSInfo.getWindowsVersion().compareTo(OSInfo.WINDOWS_2000) >= 0) {
 284                     folders.add(recentFolder);
 285                 }
 286 
 287                 folders.add(desktop);
 288                 // Add all second level folders
 289                 File[] secondLevelFolders = checkFiles(desktop.listFiles());
 290                 Arrays.sort(secondLevelFolders);
 291                 for (File secondLevelFolder : secondLevelFolders) {
 292                     Win32ShellFolder2 folder = (Win32ShellFolder2) secondLevelFolder;
 293                     if (!folder.isFileSystem() || (folder.isDirectory() && !folder.isLink())) {
 294                         folders.add(folder);
 295                         // Add third level for "My Computer"
 296                         if (folder.equals(drives)) {
 297                             File[] thirdLevelFolders = checkFiles(folder.listFiles());
 298                             if (thirdLevelFolders != null && thirdLevelFolders.length > 0) {
 299                                 List<File> thirdLevelFoldersList = Arrays.asList(thirdLevelFolders);
 300 
 301                                 folder.sortChildren(thirdLevelFoldersList);
 302                                 folders.addAll(thirdLevelFoldersList);
 303                             }
 304                         }
 305                     }
 306                 }
 307                 return checkFiles(folders);
 308             } else {
 309                 return super.get(key);
 310             }
 311         } else if (key.equals("fileChooserShortcutPanelFolders")) {
 312             Toolkit toolkit = Toolkit.getDefaultToolkit();
 313             ArrayList<File> folders = new ArrayList<File>();
 314             int i = 0;
 315             Object value;
 316             do {
 317                 value = toolkit.getDesktopProperty("win.comdlg.placesBarPlace" + i++);
 318                 try {
 319                     if (value instanceof Integer) {
 320                         // A CSIDL
 321                         folders.add(new Win32ShellFolder2((Integer)value));
 322                     } else if (value instanceof String) {
 323                         // A path
 324                         folders.add(createShellFolder(new File((String)value)));
 325                     }
 326                 } catch (IOException e) {
 327                     // Skip this value
 328                 } catch (InterruptedException e) {
 329                     // Return empty result
 330                     return new File[0];
 331                 }
 332             } while (value != null);
 333 
 334             if (folders.size() == 0) {
 335                 // Use default list of places
 336                 for (File f : new File[] {
 337                     getRecent(), getDesktop(), getPersonal(), getDrives(), getNetwork()
 338                 }) {
 339                     if (f != null) {
 340                         folders.add(f);
 341                     }
 342                 }
 343             }
 344             return checkFiles(folders);
 345         } else if (key.startsWith("fileChooserIcon ")) {
 346             String name = key.substring(key.indexOf(" ") + 1);
 347 
 348             int iconIndex;
 349 
 350             if (name.equals("ListView") || name.equals("ViewMenu")) {
 351                 iconIndex = VIEW_LIST;
 352             } else if (name.equals("DetailsView")) {
 353                 iconIndex = VIEW_DETAILS;
 354             } else if (name.equals("UpFolder")) {
 355                 iconIndex = VIEW_PARENTFOLDER;
 356             } else if (name.equals("NewFolder")) {
 357                 iconIndex = VIEW_NEWFOLDER;
 358             } else {
 359                 return null;
 360             }
 361 
 362             return getStandardViewButton(iconIndex);
 363         } else if (key.startsWith("optionPaneIcon ")) {
 364             Win32ShellFolder2.SystemIcon iconType;
 365             if (key == "optionPaneIcon Error") {
 366                 iconType = Win32ShellFolder2.SystemIcon.IDI_ERROR;
 367             } else if (key == "optionPaneIcon Information") {
 368                 iconType = Win32ShellFolder2.SystemIcon.IDI_INFORMATION;
 369             } else if (key == "optionPaneIcon Question") {
 370                 iconType = Win32ShellFolder2.SystemIcon.IDI_QUESTION;
 371             } else if (key == "optionPaneIcon Warning") {
 372                 iconType = Win32ShellFolder2.SystemIcon.IDI_EXCLAMATION;
 373             } else {
 374                 return null;
 375             }
 376             return Win32ShellFolder2.getSystemIcon(iconType);
 377         } else if (key.startsWith("shell32Icon ") || key.startsWith("shell32LargeIcon ")) {
 378             String name = key.substring(key.indexOf(" ") + 1);
 379             try {
 380                 int i = Integer.parseInt(name);
 381                 if (i >= 0) {
 382                     return Win32ShellFolder2.getShell32Icon(i,
 383                          key.startsWith("shell32LargeIcon ")?LARGE_ICON_SIZE : SMALL_ICON_SIZE);
 384                 }
 385             } catch (NumberFormatException ex) {
 386             }
 387         }
 388         return null;
 389     }
 390 
 391     private File checkFile(File file) {
 392         SecurityManager sm = System.getSecurityManager();
 393         return (sm == null || file == null) ? file : checkFile(file, sm);
 394     }
 395 
 396     private File checkFile(File file, SecurityManager sm) {
 397         try {
 398             sm.checkRead(file.getPath());
 399             return file;
 400         } catch (SecurityException se) {
 401             return null;
 402         }
 403     }
 404 
 405     private File[] checkFiles(File[] files) {
 406         SecurityManager sm = System.getSecurityManager();
 407         if (sm == null || files == null || files.length == 0) {
 408             return files;
 409         }
 410         return checkFiles(Arrays.stream(files), sm);
 411     }
 412 
 413     private File[] checkFiles(List<File> files) {
 414         SecurityManager sm = System.getSecurityManager();
 415         if (sm == null || files.isEmpty()) {
 416             return files.toArray(new File[files.size()]);
 417         }
 418         return checkFiles(files.stream(), sm);
 419     }
 420 
 421     private File[] checkFiles(Stream<File> filesStream, SecurityManager sm) {
 422         return filesStream.filter((file) -> checkFile(file, sm) != null)
 423                 .toArray(File[]::new);
 424     }
 425 
 426     /**
 427      * Does {@code dir} represent a "computer" such as a node on the network, or
 428      * "My Computer" on the desktop.
 429      */
 430     public boolean isComputerNode(final File dir) {
 431         if (dir != null && dir == getDrives()) {
 432             return true;
 433         } else {
 434             String path = AccessController.doPrivileged(new PrivilegedAction<String>() {
 435                 public String run() {
 436                     return dir.getAbsolutePath();
 437                 }
 438             });
 439 
 440             return (path.startsWith("\\\\") && path.indexOf("\\", 2) < 0);      //Network path
 441         }
 442     }
 443 
 444     public boolean isFileSystemRoot(File dir) {
 445         //Note: Removable drives don't "exist" but are listed in "My Computer"
 446         if (dir != null) {
 447 
 448             if (dir instanceof Win32ShellFolder2) {
 449                 Win32ShellFolder2 sf = (Win32ShellFolder2)dir;
 450 
 451                 //This includes all the drives under "My PC" or "My Computer.
 452                 // On windows 10, "External Drives" are listed under "Desktop"
 453                 // also
 454                 return  (sf.isFileSystem() && sf.parent != null &&
 455                         (sf.parent.equals (getDrives()) ||
 456                         (sf.parent.equals (getDesktop()) && isDrive(dir))));
 457             }
 458             return isDrive(dir);
 459         }
 460         return false;
 461     }
 462 
 463     private boolean isDrive(File dir) {
 464         String path = dir.getPath();
 465         if (path.length() != 3 || path.charAt(1) != ':') {
 466             return false;
 467         }
 468         File[] roots = Win32ShellFolder2.listRoots();
 469         return roots != null && Arrays.asList(roots).contains(dir);
 470     }
 471 
 472     private static List<Win32ShellFolder2> topFolderList = null;
 473     static int compareShellFolders(Win32ShellFolder2 sf1, Win32ShellFolder2 sf2) {
 474         boolean special1 = sf1.isSpecial();
 475         boolean special2 = sf2.isSpecial();
 476 
 477         if (special1 || special2) {
 478             if (topFolderList == null) {
 479                 ArrayList<Win32ShellFolder2> tmpTopFolderList = new ArrayList<>();
 480                 tmpTopFolderList.add(Win32ShellFolderManager2.getPersonal());
 481                 tmpTopFolderList.add(Win32ShellFolderManager2.getDesktop());
 482                 tmpTopFolderList.add(Win32ShellFolderManager2.getDrives());
 483                 tmpTopFolderList.add(Win32ShellFolderManager2.getNetwork());
 484                 topFolderList = tmpTopFolderList;
 485             }
 486             int i1 = topFolderList.indexOf(sf1);
 487             int i2 = topFolderList.indexOf(sf2);
 488             if (i1 >= 0 && i2 >= 0) {
 489                 return (i1 - i2);
 490             } else if (i1 >= 0) {
 491                 return -1;
 492             } else if (i2 >= 0) {
 493                 return 1;
 494             }
 495         }
 496 
 497         // Non-file shellfolders sort before files
 498         if (special1 && !special2) {
 499             return -1;
 500         } else if (special2 && !special1) {
 501             return  1;
 502         }
 503 
 504         return compareNames(sf1.getAbsolutePath(), sf2.getAbsolutePath());
 505     }
 506 
 507     static int compareNames(String name1, String name2) {
 508         // First ignore case when comparing
 509         int diff = name1.compareToIgnoreCase(name2);
 510         if (diff != 0) {
 511             return diff;
 512         } else {
 513             // May differ in case (e.g. "mail" vs. "Mail")
 514             // We need this test for consistent sorting
 515             return name1.compareTo(name2);
 516         }
 517     }
 518 
 519     @Override
 520     protected Invoker createInvoker() {
 521         return new ComInvoker();
 522     }
 523 
 524     private static class ComInvoker extends ThreadPoolExecutor implements ThreadFactory, ShellFolder.Invoker {
 525         private static Thread comThread;
 526 
 527         private ComInvoker() {
 528             super(1, 1, 0, TimeUnit.DAYS, new LinkedBlockingQueue<>());
 529             allowCoreThreadTimeOut(false);
 530             setThreadFactory(this);
 531             final Runnable shutdownHook = () -> AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 532                 shutdownNow();
 533                 return null;
 534             });
 535             AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 536                 Thread t = new Thread(
 537                         ThreadGroupUtils.getRootThreadGroup(), shutdownHook,
 538                         "ShellFolder", 0, false);
 539                 Runtime.getRuntime().addShutdownHook(t);
 540                 return null;
 541             });
 542         }
 543 
 544         public synchronized Thread newThread(final Runnable task) {
 545             final Runnable comRun = new Runnable() {
 546                 public void run() {
 547                     try {
 548                         initializeCom();
 549                         task.run();
 550                     } finally {
 551                         uninitializeCom();
 552                     }
 553                 }
 554             };
 555             comThread = AccessController.doPrivileged((PrivilegedAction<Thread>) () -> {
 556                 String name = "Swing-Shell";
 557                  /* The thread must be a member of a thread group
 558                   * which will not get GCed before VM exit.
 559                   * Make its parent the top-level thread group.
 560                   */
 561                 Thread thread = new Thread(
 562                         ThreadGroupUtils.getRootThreadGroup(), comRun, name,
 563                         0, false);
 564                 thread.setDaemon(true);
 565                 /* This is important, since this thread running at lower priority
 566                    leads to memory consumption when listDrives() function is called
 567                    repeatedly.
 568                  */
 569                 thread.setPriority(Thread.MAX_PRIORITY);
 570                 return thread;
 571             });
 572             return comThread;
 573         }
 574 
 575         public <T> T invoke(Callable<T> task) throws Exception {
 576             if (Thread.currentThread() == comThread) {
 577                 // if it's already called from the COM
 578                 // thread, we don't need to delegate the task
 579                 return task.call();
 580             } else {
 581                 final Future<T> future;
 582 
 583                 try {
 584                     future = submit(task);
 585                 } catch (RejectedExecutionException e) {
 586                     throw new InterruptedException(e.getMessage());
 587                 }
 588 
 589                 try {
 590                     return future.get();
 591                 } catch (InterruptedException e) {
 592                     AccessController.doPrivileged(new PrivilegedAction<Void>() {
 593                         public Void run() {
 594                             future.cancel(true);
 595 
 596                             return null;
 597                         }
 598                     });
 599 
 600                     throw e;
 601                 } catch (ExecutionException e) {
 602                     Throwable cause = e.getCause();
 603 
 604                     if (cause instanceof Exception) {
 605                         throw (Exception) cause;
 606                     }
 607 
 608                     if (cause instanceof Error) {
 609                         throw (Error) cause;
 610                     }
 611 
 612                     throw new RuntimeException("Unexpected error", cause);
 613                 }
 614             }
 615         }
 616     }
 617 
 618     static native void initializeCom();
 619 
 620     static native void uninitializeCom();
 621 }