1/*
2 * Copyright (c) 2003, 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
26package sun.awt.shell;
27
28import java.awt.*;
29import java.awt.image.BufferedImage;
30import java.awt.image.BaseMultiResolutionImage;
31
32import java.io.File;
33import java.io.FileNotFoundException;
34import java.io.IOException;
35import java.security.AccessController;
36import java.security.PrivilegedAction;
37import java.util.*;
38import java.util.List;
39import java.util.concurrent.*;
40import java.util.stream.Stream;
41
42import static sun.awt.shell.Win32ShellFolder2.*;
43import sun.awt.OSInfo;
44import 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
55final 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 == 16)
130                    ? img
131                    : new MultiResolutionIconImage(16, 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, key.startsWith("shell32LargeIcon "));
383                }
384            } catch (NumberFormatException ex) {
385            }
386        }
387        return null;
388    }
389
390    private File checkFile(File file) {
391        SecurityManager sm = System.getSecurityManager();
392        return (sm == null || file == null) ? file : checkFile(file, sm);
393    }
394
395    private File checkFile(File file, SecurityManager sm) {
396        try {
397            sm.checkRead(file.getPath());
398            return file;
399        } catch (SecurityException se) {
400            return null;
401        }
402    }
403
404    private File[] checkFiles(File[] files) {
405        SecurityManager sm = System.getSecurityManager();
406        if (sm == null || files == null || files.length == 0) {
407            return files;
408        }
409        return checkFiles(Arrays.stream(files), sm);
410    }
411
412    private File[] checkFiles(List<File> files) {
413        SecurityManager sm = System.getSecurityManager();
414        if (sm == null || files.isEmpty()) {
415            return files.toArray(new File[files.size()]);
416        }
417        return checkFiles(files.stream(), sm);
418    }
419
420    private File[] checkFiles(Stream<File> filesStream, SecurityManager sm) {
421        return filesStream.filter((file) -> checkFile(file, sm) != null)
422                .toArray(File[]::new);
423    }
424
425    /**
426     * Does {@code dir} represent a "computer" such as a node on the network, or
427     * "My Computer" on the desktop.
428     */
429    public boolean isComputerNode(final File dir) {
430        if (dir != null && dir == getDrives()) {
431            return true;
432        } else {
433            String path = AccessController.doPrivileged(new PrivilegedAction<String>() {
434                public String run() {
435                    return dir.getAbsolutePath();
436                }
437            });
438
439            return (path.startsWith("\\\\") && path.indexOf("\\", 2) < 0);      //Network path
440        }
441    }
442
443    public boolean isFileSystemRoot(File dir) {
444        //Note: Removable drives don't "exist" but are listed in "My Computer"
445        if (dir != null) {
446
447            if (dir instanceof Win32ShellFolder2) {
448                Win32ShellFolder2 sf = (Win32ShellFolder2)dir;
449
450                return (sf.isFileSystem() && sf.parent != null &&
451                        sf.parent.equals(Win32ShellFolder2.listRoots()));
452            }
453            String path = dir.getPath();
454
455            if (path.length() != 3 || path.charAt(1) != ':') {
456                return false;
457            }
458
459            File[] roots = Win32ShellFolder2.listRoots();
460
461            return roots != null && Arrays.asList(roots).contains(dir);
462        }
463        return false;
464    }
465
466    private static List<Win32ShellFolder2> topFolderList = null;
467    static int compareShellFolders(Win32ShellFolder2 sf1, Win32ShellFolder2 sf2) {
468        boolean special1 = sf1.isSpecial();
469        boolean special2 = sf2.isSpecial();
470
471        if (special1 || special2) {
472            if (topFolderList == null) {
473                ArrayList<Win32ShellFolder2> tmpTopFolderList = new ArrayList<>();
474                tmpTopFolderList.add(Win32ShellFolderManager2.getPersonal());
475                tmpTopFolderList.add(Win32ShellFolderManager2.getDesktop());
476                tmpTopFolderList.add(Win32ShellFolderManager2.getDrives());
477                tmpTopFolderList.add(Win32ShellFolderManager2.getNetwork());
478                topFolderList = tmpTopFolderList;
479            }
480            int i1 = topFolderList.indexOf(sf1);
481            int i2 = topFolderList.indexOf(sf2);
482            if (i1 >= 0 && i2 >= 0) {
483                return (i1 - i2);
484            } else if (i1 >= 0) {
485                return -1;
486            } else if (i2 >= 0) {
487                return 1;
488            }
489        }
490
491        // Non-file shellfolders sort before files
492        if (special1 && !special2) {
493            return -1;
494        } else if (special2 && !special1) {
495            return  1;
496        }
497
498        return compareNames(sf1.getAbsolutePath(), sf2.getAbsolutePath());
499    }
500
501    static int compareNames(String name1, String name2) {
502        // First ignore case when comparing
503        int diff = name1.compareToIgnoreCase(name2);
504        if (diff != 0) {
505            return diff;
506        } else {
507            // May differ in case (e.g. "mail" vs. "Mail")
508            // We need this test for consistent sorting
509            return name1.compareTo(name2);
510        }
511    }
512
513    @Override
514    protected Invoker createInvoker() {
515        return new ComInvoker();
516    }
517
518    private static class ComInvoker extends ThreadPoolExecutor implements ThreadFactory, ShellFolder.Invoker {
519        private static Thread comThread;
520
521        private ComInvoker() {
522            super(1, 1, 0, TimeUnit.DAYS, new LinkedBlockingQueue<>());
523            allowCoreThreadTimeOut(false);
524            setThreadFactory(this);
525            final Runnable shutdownHook = () -> AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
526                shutdownNow();
527                return null;
528            });
529            AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
530                Thread t = new Thread(
531                        ThreadGroupUtils.getRootThreadGroup(), shutdownHook,
532                        "ShellFolder", 0, false);
533                Runtime.getRuntime().addShutdownHook(t);
534                return null;
535            });
536        }
537
538        public synchronized Thread newThread(final Runnable task) {
539            final Runnable comRun = new Runnable() {
540                public void run() {
541                    try {
542                        initializeCom();
543                        task.run();
544                    } finally {
545                        uninitializeCom();
546                    }
547                }
548            };
549            comThread = AccessController.doPrivileged((PrivilegedAction<Thread>) () -> {
550                String name = "Swing-Shell";
551                 /* The thread must be a member of a thread group
552                  * which will not get GCed before VM exit.
553                  * Make its parent the top-level thread group.
554                  */
555                Thread thread = new Thread(
556                        ThreadGroupUtils.getRootThreadGroup(), comRun, name,
557                        0, false);
558                thread.setDaemon(true);
559                /* This is important, since this thread running at lower priority
560                   leads to memory consumption when listDrives() function is called
561                   repeatedly.
562                 */
563                thread.setPriority(Thread.MAX_PRIORITY);
564                return thread;
565            });
566            return comThread;
567        }
568
569        public <T> T invoke(Callable<T> task) throws Exception {
570            if (Thread.currentThread() == comThread) {
571                // if it's already called from the COM
572                // thread, we don't need to delegate the task
573                return task.call();
574            } else {
575                final Future<T> future;
576
577                try {
578                    future = submit(task);
579                } catch (RejectedExecutionException e) {
580                    throw new InterruptedException(e.getMessage());
581                }
582
583                try {
584                    return future.get();
585                } catch (InterruptedException e) {
586                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
587                        public Void run() {
588                            future.cancel(true);
589
590                            return null;
591                        }
592                    });
593
594                    throw e;
595                } catch (ExecutionException e) {
596                    Throwable cause = e.getCause();
597
598                    if (cause instanceof Exception) {
599                        throw (Exception) cause;
600                    }
601
602                    if (cause instanceof Error) {
603                        throw (Error) cause;
604                    }
605
606                    throw new RuntimeException("Unexpected error", cause);
607                }
608            }
609        }
610    }
611
612    static native void initializeCom();
613
614    static native void uninitializeCom();
615}
616