1 /*
   2  * Copyright (c) 2011, 2019, 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 com.apple.laf;
  27 
  28 import java.io.*;
  29 import java.util.*;
  30 import java.util.Map.Entry;
  31 
  32 import javax.swing.Icon;
  33 import javax.swing.filechooser.FileView;
  34 
  35 import com.apple.laf.AquaUtils.RecyclableSingleton;
  36 
  37 @SuppressWarnings("serial") // JDK implementation class
  38 class AquaFileView extends FileView {
  39     private static final boolean DEBUG = false;
  40 
  41     private static final int UNINITALIZED_LS_INFO = -1;
  42 
  43     // Constants from LaunchServices.h
  44     static final int kLSItemInfoIsPlainFile        = 0x00000001; /* Not a directory, volume, or symlink*/
  45     static final int kLSItemInfoIsPackage          = 0x00000002; /* Packaged directory*/
  46     static final int kLSItemInfoIsApplication      = 0x00000004; /* Single-file or packaged application*/
  47     static final int kLSItemInfoIsContainer        = 0x00000008; /* Directory (includes packages) or volume*/
  48     static final int kLSItemInfoIsAliasFile        = 0x00000010; /* Alias file (includes sym links)*/
  49     static final int kLSItemInfoIsSymlink          = 0x00000020; /* UNIX sym link*/
  50     static final int kLSItemInfoIsInvisible        = 0x00000040; /* Invisible by any known mechanism*/
  51     static final int kLSItemInfoIsNativeApp        = 0x00000080; /* Carbon or Cocoa native app*/
  52     static final int kLSItemInfoIsClassicApp       = 0x00000100; /* CFM/68K Classic app*/
  53     static final int kLSItemInfoAppPrefersNative   = 0x00000200; /* Carbon app that prefers to be launched natively*/
  54     static final int kLSItemInfoAppPrefersClassic  = 0x00000400; /* Carbon app that prefers to be launched in Classic*/
  55     static final int kLSItemInfoAppIsScriptable    = 0x00000800; /* App can be scripted*/
  56     static final int kLSItemInfoIsVolume           = 0x00001000; /* Item is a volume*/
  57     static final int kLSItemInfoExtensionIsHidden  = 0x00100000; /* Item has a hidden extension*/
  58 
  59     static {
  60         jdk.internal.access.SharedSecrets.getJavaLangAccess().loadLibrary("osxui");
  61     }
  62 
  63     // TODO: Un-comment this out when the native version exists
  64     //private static native String getNativePathToRunningJDKBundle();
  65     private static native String getNativePathToSharedJDKBundle();
  66 
  67     private static native String getNativeMachineName();
  68     private static native String getNativeDisplayName(final byte[] pathBytes, final boolean isDirectory);
  69     private static native int getNativeLSInfo(final byte[] pathBytes, final boolean isDirectory);
  70     private static native String getNativePathForResolvedAlias(final byte[] absolutePath, final boolean isDirectory);
  71 
  72     private static final RecyclableSingleton<String> machineName = new RecyclableSingleton<String>() {
  73         @Override
  74         protected String getInstance() {
  75             return getNativeMachineName();
  76         }
  77     };
  78     private static String getMachineName() {
  79         return machineName.get();
  80     }
  81 
  82     protected static String getPathToRunningJDKBundle() {
  83         // TODO: Return empty string for now
  84         return "";//getNativePathToRunningJDKBundle();
  85     }
  86 
  87     protected static String getPathToSharedJDKBundle() {
  88         return getNativePathToSharedJDKBundle();
  89     }
  90 
  91     static class FileInfo {
  92         final boolean isDirectory;
  93         final String absolutePath;
  94         byte[] pathBytes;
  95 
  96         String displayName;
  97         Icon icon;
  98         int launchServicesInfo = UNINITALIZED_LS_INFO;
  99 
 100         FileInfo(final File file){
 101             isDirectory = file.isDirectory();
 102             absolutePath = file.getAbsolutePath();
 103             try {
 104                 pathBytes = absolutePath.getBytes("UTF-8");
 105             } catch (final UnsupportedEncodingException e) {
 106                 pathBytes = new byte[0];
 107             }
 108         }
 109     }
 110 
 111     final int MAX_CACHED_ENTRIES = 256;
 112     protected final Map<File, FileInfo> cache = new LinkedHashMap<File, FileInfo>(){
 113         protected boolean removeEldestEntry(final Entry<File, FileInfo> eldest) {
 114             return size() > MAX_CACHED_ENTRIES;
 115         }
 116     };
 117 
 118     FileInfo getFileInfoFor(final File file) {
 119         final FileInfo info = cache.get(file);
 120         if (info != null) return info;
 121         final FileInfo newInfo = new FileInfo(file);
 122         cache.put(file, newInfo);
 123         return newInfo;
 124     }
 125 
 126 
 127     final AquaFileChooserUI fFileChooserUI;
 128     public AquaFileView(final AquaFileChooserUI fileChooserUI) {
 129         fFileChooserUI = fileChooserUI;
 130     }
 131 
 132     String _directoryDescriptionText() {
 133         return fFileChooserUI.directoryDescriptionText;
 134     }
 135 
 136     String _fileDescriptionText() {
 137         return fFileChooserUI.fileDescriptionText;
 138     }
 139 
 140     boolean _packageIsTraversable() {
 141         return fFileChooserUI.fPackageIsTraversable == AquaFileChooserUI.kOpenAlways;
 142     }
 143 
 144     boolean _applicationIsTraversable() {
 145         return fFileChooserUI.fApplicationIsTraversable == AquaFileChooserUI.kOpenAlways;
 146     }
 147 
 148     public String getName(final File f) {
 149         final FileInfo info = getFileInfoFor(f);
 150         if (info.displayName != null) return info.displayName;
 151 
 152         final String nativeDisplayName = getNativeDisplayName(info.pathBytes, info.isDirectory);
 153         if (nativeDisplayName != null) {
 154             info.displayName = nativeDisplayName;
 155             return nativeDisplayName;
 156         }
 157 
 158         final String displayName = f.getName();
 159         if (f.isDirectory() && fFileChooserUI.getFileChooser().getFileSystemView().isRoot(f)) {
 160             final String localMachineName = getMachineName();
 161             info.displayName = localMachineName;
 162             return localMachineName;
 163         }
 164 
 165         info.displayName = displayName;
 166         return displayName;
 167     }
 168 
 169     public String getDescription(final File f) {
 170         return f.getName();
 171     }
 172 
 173     public String getTypeDescription(final File f) {
 174         if (f.isDirectory()) return _directoryDescriptionText();
 175         return _fileDescriptionText();
 176     }
 177 
 178     public Icon getIcon(final File f) {
 179         final FileInfo info = getFileInfoFor(f);
 180         if (info.icon != null) return info.icon;
 181 
 182         if (f == null) {
 183             info.icon = AquaIcon.SystemIcon.getDocumentIconUIResource();
 184         } else {
 185             // Look for the document's icon
 186             final AquaIcon.FileIcon fileIcon = new AquaIcon.FileIcon(f);
 187             info.icon = fileIcon;
 188             if (!fileIcon.hasIconRef()) {
 189                 // Fall back on the default icons
 190                 if (f.isDirectory()) {
 191                     if (fFileChooserUI.getFileChooser().getFileSystemView().isRoot(f)) {
 192                         info.icon = AquaIcon.SystemIcon.getComputerIconUIResource();
 193                     } else if (f.getParent() == null || f.getParent().equals("/")) {
 194                         info.icon = AquaIcon.SystemIcon.getHardDriveIconUIResource();
 195                     } else {
 196                         info.icon = AquaIcon.SystemIcon.getFolderIconUIResource();
 197                     }
 198                 } else {
 199                     info.icon = AquaIcon.SystemIcon.getDocumentIconUIResource();
 200                 }
 201             }
 202         }
 203 
 204         return info.icon;
 205     }
 206 
 207     // aliases are traversable though they aren't directories
 208     public Boolean isTraversable(final File f) {
 209         if (f.isDirectory()) {
 210             // Doesn't matter if it's a package or app, because they're traversable
 211             if (_packageIsTraversable() && _applicationIsTraversable()) {
 212                 return Boolean.TRUE;
 213             } else if (!_packageIsTraversable() && !_applicationIsTraversable()) {
 214                 if (isPackage(f) || isApplication(f)) return Boolean.FALSE;
 215             } else if (!_applicationIsTraversable()) {
 216                 if (isApplication(f)) return Boolean.FALSE;
 217             } else if (!_packageIsTraversable()) {
 218                 // [3101730] All applications are packages, but not all packages are applications.
 219                 if (isPackage(f) && !isApplication(f)) return Boolean.FALSE;
 220             }
 221 
 222             // We're allowed to traverse it
 223             return Boolean.TRUE;
 224         }
 225 
 226         if (isAlias(f)) {
 227             final File realFile = resolveAlias(f);
 228             return realFile.isDirectory() ? Boolean.TRUE : Boolean.FALSE;
 229         }
 230 
 231         return Boolean.FALSE;
 232     }
 233 
 234     int getLSInfoFor(final File f) {
 235         final FileInfo info = getFileInfoFor(f);
 236 
 237         if (info.launchServicesInfo == UNINITALIZED_LS_INFO) {
 238             info.launchServicesInfo = getNativeLSInfo(info.pathBytes, info.isDirectory);
 239         }
 240 
 241         return info.launchServicesInfo;
 242     }
 243 
 244     boolean isAlias(final File f) {
 245         final int lsInfo = getLSInfoFor(f);
 246         return ((lsInfo & kLSItemInfoIsAliasFile) != 0) && ((lsInfo & kLSItemInfoIsSymlink) == 0);
 247     }
 248 
 249     boolean isApplication(final File f) {
 250         return (getLSInfoFor(f) & kLSItemInfoIsApplication) != 0;
 251     }
 252 
 253     boolean isPackage(final File f) {
 254         return (getLSInfoFor(f) & kLSItemInfoIsPackage) != 0;
 255     }
 256 
 257     /**
 258      * Things that need to be handled:
 259      * -Change getFSRef to use CFURLRef instead of FSPathMakeRef
 260      * -Use the HFS-style path from CFURLRef in resolveAlias() to avoid
 261      *      path length limitations
 262      * -In resolveAlias(), simply resolve immediately if this is an alias
 263      */
 264 
 265     /**
 266      * Returns the actual file represented by this object.  This will
 267      * resolve any aliases in the path, including this file if it is an
 268      * alias.  No alias resolution requiring user interaction (e.g.
 269      * mounting servers) will occur.  Note that aliases to servers may
 270      * take a significant amount of time to resolve.  This method
 271      * currently does not have any provisions for a more fine-grained
 272      * timeout for alias resolution beyond that used by the system.
 273      *
 274      * In the event of a path that does not contain any aliases, or if the file
 275      *  does not exist, this method will return the file that was passed in.
 276      *    @return    The canonical path to the file
 277      *    @throws    IOException    If an I/O error occurs while attempting to
 278      *                            construct the path
 279      */
 280     File resolveAlias(final File mFile) {
 281         // If the file exists and is not an alias, there aren't
 282         // any aliases along its path, so the standard version
 283         // of getCanonicalPath() will work.
 284         if (mFile.exists() && !isAlias(mFile)) {
 285             if (DEBUG) System.out.println("not an alias");
 286             return mFile;
 287         }
 288 
 289         // If it doesn't exist, either there's an alias in the
 290         // path or this is an alias.  Traverse the path and
 291         // resolve all aliases in it.
 292         final LinkedList<String> components = getPathComponents(mFile);
 293         if (components == null) {
 294             if (DEBUG) System.out.println("getPathComponents is null ");
 295             return mFile;
 296         }
 297 
 298         File file = new File("/");
 299         for (final String nextComponent : components) {
 300             file = new File(file, nextComponent);
 301             final FileInfo info = getFileInfoFor(file);
 302 
 303             // If any point along the way doesn't exist,
 304             // just return the file.
 305             if (!file.exists()) { return mFile; }
 306 
 307             if (isAlias(file)) {
 308                 // Resolve it!
 309                 final String path = getNativePathForResolvedAlias(info.pathBytes, info.isDirectory);
 310 
 311                 // <rdar://problem/3582601> If the alias doesn't resolve (on a non-existent volume, for example)
 312                 // just return the file.
 313                 if (path == null) return mFile;
 314 
 315                 file = new File(path);
 316             }
 317         }
 318 
 319         return file;
 320     }
 321 
 322     /**
 323      * Returns a linked list of Strings consisting of the components of
 324      * the path of this file, in order, including the filename as the
 325      * last element.  The first element in the list will be the first
 326      * directory in the path, or "".
 327      *    @return A linked list of the components of this file's path
 328      */
 329     private static LinkedList<String> getPathComponents(final File mFile) {
 330         final LinkedList<String> componentList = new LinkedList<String>();
 331         String parent;
 332 
 333         File file = new File(mFile.getAbsolutePath());
 334         componentList.add(0, file.getName());
 335         while ((parent = file.getParent()) != null) {
 336             file = new File(parent);
 337             componentList.add(0, file.getName());
 338         }
 339         return componentList;
 340     }
 341 }