001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019 package com.mucommander.file.impl;
020
021 import com.mucommander.Debug;
022 import com.mucommander.file.AbstractFile;
023 import com.mucommander.file.FilePermissions;
024 import com.mucommander.file.FileProtocols;
025 import com.mucommander.file.filter.FileFilter;
026 import com.mucommander.file.filter.FilenameFilter;
027 import com.mucommander.file.impl.local.LocalFile;
028
029 import java.io.File;
030 import java.io.IOException;
031 import java.lang.reflect.Field;
032 import java.lang.reflect.Method;
033
034 /**
035 * CachedFile is a ProxyFile that caches the return values of most {@link AbstractFile} getter methods. This allows
036 * to limit the number of calls to the underlying file methods which can have a cost since they often are I/O bound.
037 * The methods that are cached are those overridden by this class, except for the <code>ls</code> methods, which are
038 * overridden only to allow recursion (see {@link #CachedFile(com.mucommander.file.AbstractFile, boolean)}).
039 *
040 * <p>The values are retrieved and cached only when the 'cached methods' are called for the first time; they are
041 * not preemptively retrieved in the constructor, so using this class has no negative impact on performance,
042 * except for the small extra CPU cost added by proxying the methods and the extra RAM used to store cached values.
043 *
044 * <p>Once the values are retrieved and cached, they never change: the same value will always be returned once a method
045 * has been called for the first time. That means if the underlying file changes (e.g. its size or date has changed),
046 * the changes will not be reflected by this CachedFile. Thus, this class should only be used when a 'real-time' view
047 * of the file is not required, or when the file instance is used only for a small amount of time.
048 *
049 * @author Maxence Bernard
050 */
051 public class CachedFile extends ProxyFile {
052
053 /** If true, AbstractFile instances returned by this class will be wrapped into CachedFile instances */
054 private boolean recurseInstances;
055
056 ///////////////////
057 // Cached values //
058 ///////////////////
059
060 private long getSize;
061 private boolean getSizeSet;
062
063 private long getDate;
064 private boolean getDateSet;
065
066 private boolean canRunProcess;
067 private boolean canRunProcessSet;
068
069 private boolean isSymlink;
070 private boolean isSymlinkSet;
071
072 private boolean isDirectory;
073 private boolean isDirectorySet;
074
075 private boolean isBrowsable;
076 private boolean isBrowsableSet;
077
078 private boolean isHidden;
079 private boolean isHiddenSet;
080
081 private String getAbsolutePath;
082 private boolean getAbsolutePathSet;
083
084 private String getCanonicalPath;
085 private boolean getCanonicalPathSet;
086
087 private String getExtension;
088 private boolean getExtensionSet;
089
090 private String getName;
091 private boolean getNameSet;
092
093 private long getFreeSpace;
094 private boolean getFreeSpaceSet;
095
096 private long getTotalSpace;
097 private boolean getTotalSpaceSet;
098
099 private boolean exists;
100 private boolean existsSet;
101
102 private FilePermissions getPermissions;
103 private boolean getPermissionsSet;
104
105 private String getPermissionsString;
106 private boolean getPermissionsStringSet;
107
108 private String getOwner;
109 private boolean getOwnerSet;
110
111 private String getGroup;
112 private boolean getGroupSet;
113
114 private boolean isRoot;
115 private boolean isRootSet;
116
117 private AbstractFile getParent;
118 private boolean getParentSet;
119
120 private AbstractFile getRoot;
121 private boolean getRootSet;
122
123 private AbstractFile getCanonicalFile;
124 private boolean getCanonicalFileSet;
125
126 // Used to access the java.io.FileSystem#getBooleanAttributes method
127 private static boolean getFileAttributesAvailable;
128 private static Method mGetBooleanAttributes;
129 private static int BA_DIRECTORY, BA_EXISTS, BA_HIDDEN;
130 private static Object fs;
131
132 static {
133 // Exposes the java.io.FileSystem class which by default has package access, in order to use its
134 // 'getBooleanAttributes' method to speed up access to file attributes under Windows.
135 // This method allows to retrieve the values of the 'exists', 'isDirectory' and 'isHidden' attributes in one
136 // pass, resolving the underlying file only once instead of 3 times. Since resolving a file is a particularly
137 // expensive operation under Windows due to improper use of the Win32 API, this helps speed things up a little.
138 // References:
139 // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5036988
140 // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6240028
141 //
142 // This hack was made for Windows, but is now used for other platforms as well as it is necessarily faster than
143 // retrieving file attributes individually.
144
145 try {
146 // Resolve FileSystem class, 'getBooleanAttributes' method and fields
147 Class cFile = File.class;
148 Class cFileSystem = Class.forName("java.io.FileSystem");
149 mGetBooleanAttributes = cFileSystem.getDeclaredMethod("getBooleanAttributes", new Class [] {cFile});
150 Field fBA_EXISTS = cFileSystem.getDeclaredField("BA_EXISTS");
151 Field fBA_DIRECTORY = cFileSystem.getDeclaredField("BA_DIRECTORY");
152 Field fBA_HIDDEN = cFileSystem.getDeclaredField("BA_HIDDEN");
153 Field fFs = cFile.getDeclaredField("fs");
154
155 // Allow access to the 'getBooleanAttributes' method and to the fields we're interested in
156 mGetBooleanAttributes.setAccessible(true);
157 fFs.setAccessible(true);
158 fBA_EXISTS.setAccessible(true);
159 fBA_DIRECTORY.setAccessible(true);
160 fBA_HIDDEN.setAccessible(true);
161
162 // Retrieve constant field values once for all
163 BA_EXISTS = ((Integer)fBA_EXISTS.get(null)).intValue();
164 BA_DIRECTORY = ((Integer)fBA_DIRECTORY.get(null)).intValue();
165 BA_HIDDEN = ((Integer)fBA_HIDDEN.get(null)).intValue();
166 fs = fFs.get(null);
167
168 getFileAttributesAvailable = true;
169 if(Debug.ON) Debug.trace("Access to java.io.FileSystem granted");
170 }
171 catch(Exception e) {
172 if(Debug.ON) Debug.trace("Error while allowing access to java.io.FileSystem: "+e);
173 }
174 }
175
176
177 /**
178 * Creates a new CachedFile instance around the specified AbstractFile, caching returned values of cached methods
179 * as they are called. If recursion is enabled, the methods returning AbstractFile will return CachedFile instances,
180 * allowing the cache files recursively.
181 *
182 * @param file the AbstractFile instance for which returned values of getter methods should be cached
183 * @param recursiveInstances if true, AbstractFile instances returned by this class will be wrapped into CachedFile instances
184 */
185 public CachedFile(AbstractFile file, boolean recursiveInstances) {
186 super(file);
187
188 this.recurseInstances = recursiveInstances;
189 }
190
191
192 /**
193 * Creates a CachedFile instance for each of the AbstractFile instances in the given array.
194 */
195 private AbstractFile[] createCachedFiles(AbstractFile files[]) {
196 int nbFiles = files.length;
197 for(int i=0; i<nbFiles; i++)
198 files[i] = new CachedFile(files[i], true);
199
200 return files;
201 }
202
203
204 /**
205 * Pre-fetches values of {@link #isDirectory}, {@link #exists} and {@link #isHidden} for the given local file,
206 * using the <code>java.io.FileSystem#getBooleanAttributes(java.io.File)</code> method.
207 * The given {@link AbstractFile} must be a local file or a proxy to a local file ('file' protocol). This method
208 * must only be called if the {@link #getFileAttributesAvailable} field is <code>true</code>.
209 */
210 private void getFileAttributes(AbstractFile file) {
211 file = file.getTopAncestor();
212
213 if(file instanceof LocalFile) {
214 try {
215 int ba = ((Integer)mGetBooleanAttributes.invoke(fs, new Object [] {file.getUnderlyingFileObject()})).intValue();
216
217 isDirectory = (ba & BA_DIRECTORY)!=0;
218 isDirectorySet = true;
219
220 exists = (ba & BA_EXISTS)!=0;
221 existsSet = true;
222
223 isHidden = (ba & BA_HIDDEN)!=0;
224 isHiddenSet = true;
225 }
226 catch(Exception e) {
227 if(Debug.ON) Debug.trace("Could not retrieve file attributes for "+file+": "+e);
228 }
229 }
230 }
231
232
233 ////////////////////////////////////////////////////
234 // Overridden methods to cache their return value //
235 ////////////////////////////////////////////////////
236
237 public long getSize() {
238 if(!getSizeSet) {
239 getSize = file.getSize();
240 getSizeSet = true;
241 }
242
243 return getSize;
244 }
245
246 public long getDate() {
247 if(!getDateSet) {
248 getDate = file.getDate();
249 getDateSet = true;
250 }
251
252 return getDate;
253 }
254
255 public boolean canRunProcess() {
256 if(!canRunProcessSet) {
257 canRunProcess = file.canRunProcess();
258 canRunProcessSet = true;
259 }
260
261 return canRunProcess;
262 }
263
264 public boolean isSymlink() {
265 if(!isSymlinkSet) {
266 isSymlink = file.isSymlink();
267 isSymlinkSet = true;
268 }
269
270 return isSymlink;
271 }
272
273 public boolean isDirectory() {
274 if(!isDirectorySet && getFileAttributesAvailable && FileProtocols.FILE.equals(file.getURL().getScheme()))
275 getFileAttributes(file);
276 // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again
277
278 if(!isDirectorySet) {
279 isDirectory = file.isDirectory();
280 isDirectorySet = true;
281 }
282
283 return isDirectory;
284 }
285
286 public boolean isBrowsable() {
287 if(!isBrowsableSet) {
288 isBrowsable = file.isBrowsable();
289 isBrowsableSet = true;
290 }
291
292 return isBrowsable;
293 }
294
295 public boolean isHidden() {
296 if(!isHiddenSet && getFileAttributesAvailable && FileProtocols.FILE.equals(file.getURL().getScheme()))
297 getFileAttributes(file);
298 // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again
299
300 if(!isHiddenSet) {
301 isHidden = file.isHidden();
302 isHiddenSet = true;
303 }
304
305 return isHidden;
306 }
307
308 public String getAbsolutePath() {
309 if(!getAbsolutePathSet) {
310 getAbsolutePath = file.getAbsolutePath();
311 getAbsolutePathSet = true;
312 }
313
314 return getAbsolutePath;
315 }
316
317 public String getCanonicalPath() {
318 if(!getCanonicalPathSet) {
319 getCanonicalPath = file.getCanonicalPath();
320 getCanonicalPathSet = true;
321 }
322
323 return getCanonicalPath;
324 }
325
326 public String getExtension() {
327 if(!getExtensionSet) {
328 getExtension = file.getExtension();
329 getExtensionSet = true;
330 }
331
332 return getExtension;
333 }
334
335 public String getName() {
336 if(!getNameSet) {
337 getName = file.getName();
338 getNameSet = true;
339 }
340
341 return getName;
342 }
343
344 public long getFreeSpace() {
345 if(!getFreeSpaceSet) {
346 getFreeSpace = file.getFreeSpace();
347 getFreeSpaceSet = true;
348 }
349
350 return getFreeSpace;
351 }
352
353 public long getTotalSpace() {
354 if(!getTotalSpaceSet) {
355 getTotalSpace = file.getTotalSpace();
356 getTotalSpaceSet = true;
357 }
358
359 return getTotalSpace;
360 }
361
362 public boolean exists() {
363 if(!existsSet && getFileAttributesAvailable && FileProtocols.FILE.equals(file.getURL().getScheme()))
364 getFileAttributes(file);
365 // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again
366
367 if(!existsSet) {
368 exists = file.exists();
369 existsSet = true;
370 }
371
372 return exists;
373 }
374
375 public FilePermissions getPermissions() {
376 if(!getPermissionsSet) {
377 getPermissions = file.getPermissions();
378 getPermissionsSet = true;
379 }
380
381 return getPermissions;
382 }
383
384 public String getPermissionsString() {
385 if(!getPermissionsStringSet) {
386 getPermissionsString = file.getPermissionsString();
387 getPermissionsStringSet = true;
388 }
389
390 return getPermissionsString;
391 }
392
393 public String getOwner() {
394 if(!getOwnerSet) {
395 getOwner = file.getOwner();
396 getOwnerSet = true;
397 }
398
399 return getOwner;
400 }
401
402 public String getGroup() {
403 if(!getGroupSet) {
404 getGroup = file.getGroup();
405 getGroupSet = true;
406 }
407
408 return getGroup;
409 }
410
411 public boolean isRoot() {
412 if(!isRootSet) {
413 isRoot = file.isRoot();
414 isRootSet = true;
415 }
416
417 return isRoot;
418 }
419
420
421 public AbstractFile getParent() throws IOException {
422 if(!getParentSet) {
423 getParent = file.getParent();
424 // Create a CachedFile instance around the file if recursion is enabled
425 if(recurseInstances && getParent!=null)
426 getParent = new CachedFile(getParent, true);
427 getParentSet = true;
428 }
429
430 return getParent;
431 }
432
433 public AbstractFile getRoot() throws IOException {
434 if(!getRootSet) {
435 getRoot = file.getRoot();
436 // Create a CachedFile instance around the file if recursion is enabled
437 if(recurseInstances)
438 getRoot = new CachedFile(getRoot, true);
439
440 getRootSet = true;
441 }
442
443 return getRoot;
444 }
445
446 public AbstractFile getCanonicalFile() {
447 if(!getCanonicalFileSet) {
448 getCanonicalFile = file.getCanonicalFile();
449 // Create a CachedFile instance around the file if recursion is enabled
450 if(recurseInstances) {
451 // AbstractFile#getCanonicalFile() may return 'this' if the file is not a symlink. In that case,
452 // no need to create a new CachedFile, simply use this one.
453 if(getCanonicalFile==file)
454 getCanonicalFile = this;
455 else
456 getCanonicalFile = new CachedFile(getCanonicalFile, true);
457 }
458
459 getCanonicalFileSet = true;
460 }
461
462 return getCanonicalFile;
463 }
464
465
466 ////////////////////////////////////////////////
467 // Overridden for recursion only (no caching) //
468 ////////////////////////////////////////////////
469
470 public AbstractFile[] ls() throws IOException {
471 // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled
472 AbstractFile files[] = file.ls();
473
474 if(recurseInstances)
475 return createCachedFiles(files);
476
477 return files;
478 }
479
480 public AbstractFile[] ls(FileFilter filter) throws IOException {
481 // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled
482 AbstractFile files[] = file.ls(filter);
483
484 if(recurseInstances)
485 return createCachedFiles(files);
486
487 return files;
488 }
489
490 public AbstractFile[] ls(FilenameFilter filter) throws IOException {
491 // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled
492 AbstractFile files[] = file.ls(filter);
493
494 if(recurseInstances)
495 return createCachedFiles(files);
496
497 return files;
498 }
499 }