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 }