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