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    }