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