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