/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.commons.file.protocol.local.LocalFile; /** * CachedFile is a ProxyFile that caches the return values of most {@link AbstractFile} getter methods. This allows * to limit the number of calls to the underlying file methods which can have a cost since they often are I/O bound. * The methods that are cached are those overridden by this class, except for the <code>ls</code> methods, which are * overridden only to allow recursion (see {@link #CachedFile(com.mucommander.commons.file.AbstractFile, boolean)}). * * <p>The values are retrieved and cached only when the 'cached methods' are called for the first time; they are * not preemptively retrieved in the constructor, so using this class has no negative impact on performance, * except for the small extra CPU cost added by proxying the methods and the extra RAM used to store cached values. * * <p>Once the values are retrieved and cached, they never change: the same value will always be returned once a method * has been called for the first time. That means if the underlying file changes (e.g. its size or date has changed), * the changes will not be reflected by this CachedFile. Thus, this class should only be used when a 'real-time' view * of the file is not required, or when the file instance is used only for a small amount of time. * * @author Maxence Bernard */ public class CachedFile extends ProxyFile { private static final Logger LOGGER = LoggerFactory.getLogger(CachedFile.class); /** If true, AbstractFile instances returned by this class will be wrapped into CachedFile instances */ private boolean recurseInstances; /////////////////// // Cached values // /////////////////// private long getSize; private boolean getSizeSet; private long getDate; private boolean getDateSet; private boolean isSymlink; private boolean isSymlinkSet; private boolean isDirectory; private boolean isDirectorySet; private boolean isArchive; private boolean isArchiveSet; private boolean isHidden; private boolean isHiddenSet; private String getAbsolutePath; private boolean getAbsolutePathSet; private String getCanonicalPath; private boolean getCanonicalPathSet; private String getExtension; private boolean getExtensionSet; private String getName; private boolean getNameSet; private long getFreeSpace; private boolean getFreeSpaceSet; private long getTotalSpace; private boolean getTotalSpaceSet; private boolean exists; private boolean existsSet; private FilePermissions getPermissions; private boolean getPermissionsSet; private String getPermissionsString; private boolean getPermissionsStringSet; private String getOwner; private boolean getOwnerSet; private String getGroup; private boolean getGroupSet; private boolean isRoot; private boolean isRootSet; private AbstractFile getParent; private boolean getParentSet; private AbstractFile getRoot; private boolean getRootSet; private AbstractFile getCanonicalFile; private boolean getCanonicalFileSet; // Used to access the java.io.FileSystem#getBooleanAttributes method private static boolean getFileAttributesAvailable; private static Method mGetBooleanAttributes; private static int BA_DIRECTORY, BA_EXISTS, BA_HIDDEN; private static Object fs; static { // Exposes the java.io.FileSystem class which by default has package access, in order to use its // 'getBooleanAttributes' method to speed up access to file attributes under Windows. // This method allows to retrieve the values of the 'exists', 'isDirectory' and 'isHidden' attributes in one // pass, resolving the underlying file only once instead of 3 times. Since resolving a file is a particularly // expensive operation under Windows due to improper use of the Win32 API, this helps speed things up a little. // References: // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5036988 // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6240028 // // This hack was made for Windows, but is now used for other platforms as well as it is necessarily faster than // retrieving file attributes individually. try { // Resolve FileSystem class, 'getBooleanAttributes' method and fields Class<?> cFile = File.class; Class<?> cFileSystem = Class.forName("java.io.FileSystem"); mGetBooleanAttributes = cFileSystem.getDeclaredMethod("getBooleanAttributes", new Class [] {cFile}); Field fBA_EXISTS = cFileSystem.getDeclaredField("BA_EXISTS"); Field fBA_DIRECTORY = cFileSystem.getDeclaredField("BA_DIRECTORY"); Field fBA_HIDDEN = cFileSystem.getDeclaredField("BA_HIDDEN"); Field fFs = cFile.getDeclaredField("fs"); // Allow access to the 'getBooleanAttributes' method and to the fields we're interested in mGetBooleanAttributes.setAccessible(true); fFs.setAccessible(true); fBA_EXISTS.setAccessible(true); fBA_DIRECTORY.setAccessible(true); fBA_HIDDEN.setAccessible(true); // Retrieve constant field values once for all BA_EXISTS = (Integer) fBA_EXISTS.get(null); BA_DIRECTORY = (Integer) fBA_DIRECTORY.get(null); BA_HIDDEN = (Integer) fBA_HIDDEN.get(null); fs = fFs.get(null); getFileAttributesAvailable = true; LOGGER.trace("Access to java.io.FileSystem granted"); } catch(Exception e) { LOGGER.info("Error while allowing access to java.io.FileSystem", e); } } /** * Creates a new CachedFile instance around the specified AbstractFile, caching returned values of cached methods * as they are called. If recursion is enabled, the methods returning AbstractFile will return CachedFile instances, * allowing the cache files recursively. * * @param file the AbstractFile instance for which returned values of getter methods should be cached * @param recursiveInstances if true, AbstractFile instances returned by this class will be wrapped into CachedFile instances */ public CachedFile(AbstractFile file, boolean recursiveInstances) { super(file); this.recurseInstances = recursiveInstances; } /** * Creates a CachedFile instance for each of the AbstractFile instances in the given array. */ private AbstractFile[] createCachedFiles(AbstractFile files[]) { int nbFiles = files.length; for(int i=0; i<nbFiles; i++) files[i] = new CachedFile(files[i], true); return files; } /** * Pre-fetches values of {@link #isDirectory}, {@link #exists} and {@link #isHidden} for the given local file, * using the <code>java.io.FileSystem#getBooleanAttributes(java.io.File)</code> method. * The given {@link AbstractFile} must be a local file or a proxy to a local file ('file' protocol). This method * must only be called if the {@link #getFileAttributesAvailable} field is <code>true</code>. */ private void getFileAttributes(AbstractFile file) { file = file.getTopAncestor(); if(file instanceof LocalFile) { try { int ba = (Integer) mGetBooleanAttributes.invoke(fs, new Object[]{file.getUnderlyingFileObject()}); isDirectory = (ba & BA_DIRECTORY)!=0; isDirectorySet = true; exists = (ba & BA_EXISTS)!=0; existsSet = true; isHidden = (ba & BA_HIDDEN)!=0; isHiddenSet = true; } catch(Exception e) { LOGGER.info("Could not retrieve file attributes for {}", file, e); } } } //////////////////////////////////////////////////// // Overridden methods to cache their return value // //////////////////////////////////////////////////// @Override public long getSize() { if(!getSizeSet) { getSize = file.getSize(); getSizeSet = true; } return getSize; } @Override public long getDate() { if(!getDateSet) { getDate = file.getDate(); getDateSet = true; } return getDate; } @Override public boolean isSymlink() { if(!isSymlinkSet) { isSymlink = file.isSymlink(); isSymlinkSet = true; } return isSymlink; } @Override public boolean isDirectory() { if(!isDirectorySet && getFileAttributesAvailable && FileProtocols.FILE.equals(file.getURL().getScheme())) getFileAttributes(file); // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again if(!isDirectorySet) { isDirectory = file.isDirectory(); isDirectorySet = true; } return isDirectory; } @Override public boolean isArchive() { if(!isArchiveSet) { isArchive = file.isArchive(); isArchiveSet = true; } return isArchive; } @Override public boolean isHidden() { if(!isHiddenSet && getFileAttributesAvailable && FileProtocols.FILE.equals(file.getURL().getScheme())) getFileAttributes(file); // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again if(!isHiddenSet) { isHidden = file.isHidden(); isHiddenSet = true; } return isHidden; } @Override public String getAbsolutePath() { if(!getAbsolutePathSet) { getAbsolutePath = file.getAbsolutePath(); getAbsolutePathSet = true; } return getAbsolutePath; } @Override public String getCanonicalPath() { if(!getCanonicalPathSet) { getCanonicalPath = file.getCanonicalPath(); getCanonicalPathSet = true; } return getCanonicalPath; } @Override public String getExtension() { if(!getExtensionSet) { getExtension = file.getExtension(); getExtensionSet = true; } return getExtension; } @Override public String getName() { if(!getNameSet) { getName = file.getName(); getNameSet = true; } return getName; } @Override public long getFreeSpace() throws IOException, UnsupportedFileOperationException { if(!getFreeSpaceSet) { getFreeSpace = file.getFreeSpace(); getFreeSpaceSet = true; } return getFreeSpace; } @Override public long getTotalSpace() throws IOException, UnsupportedFileOperationException { if(!getTotalSpaceSet) { getTotalSpace = file.getTotalSpace(); getTotalSpaceSet = true; } return getTotalSpace; } @Override public boolean exists() { if(!existsSet && getFileAttributesAvailable && FileProtocols.FILE.equals(file.getURL().getScheme())) getFileAttributes(file); // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again if(!existsSet) { exists = file.exists(); existsSet = true; } return exists; } @Override public FilePermissions getPermissions() { if(!getPermissionsSet) { getPermissions = file.getPermissions(); getPermissionsSet = true; } return getPermissions; } @Override public String getPermissionsString() { if(!getPermissionsStringSet) { getPermissionsString = file.getPermissionsString(); getPermissionsStringSet = true; } return getPermissionsString; } @Override public String getOwner() { if(!getOwnerSet) { getOwner = file.getOwner(); getOwnerSet = true; } return getOwner; } @Override public String getGroup() { if(!getGroupSet) { getGroup = file.getGroup(); getGroupSet = true; } return getGroup; } @Override public boolean isRoot() { if(!isRootSet) { isRoot = file.isRoot(); isRootSet = true; } return isRoot; } @Override public AbstractFile getParent() { if(!getParentSet) { getParent = file.getParent(); // Create a CachedFile instance around the file if recursion is enabled if(recurseInstances && getParent!=null) getParent = new CachedFile(getParent, true); getParentSet = true; } return getParent; } @Override public AbstractFile getRoot() { if(!getRootSet) { getRoot = file.getRoot(); // Create a CachedFile instance around the file if recursion is enabled if(recurseInstances) getRoot = new CachedFile(getRoot, true); getRootSet = true; } return getRoot; } @Override public AbstractFile getCanonicalFile() { if(!getCanonicalFileSet) { getCanonicalFile = file.getCanonicalFile(); // Create a CachedFile instance around the file if recursion is enabled if(recurseInstances) { // AbstractFile#getCanonicalFile() may return 'this' if the file is not a symlink. In that case, // no need to create a new CachedFile, simply use this one. if(getCanonicalFile==file) getCanonicalFile = this; else getCanonicalFile = new CachedFile(getCanonicalFile, true); } getCanonicalFileSet = true; } return getCanonicalFile; } //////////////////////////////////////////////// // Overridden for recursion only (no caching) // //////////////////////////////////////////////// @Override public AbstractFile[] ls() throws IOException, UnsupportedFileOperationException { // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled AbstractFile files[] = file.ls(); if(recurseInstances) return createCachedFiles(files); return files; } @Override public AbstractFile[] ls(FileFilter filter) throws IOException, UnsupportedFileOperationException { // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled AbstractFile files[] = file.ls(filter); if(recurseInstances) return createCachedFiles(files); return files; } @Override public AbstractFile[] ls(FilenameFilter filter) throws IOException, UnsupportedFileOperationException { // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled AbstractFile files[] = file.ls(filter); if(recurseInstances) return createCachedFiles(files); return files; } }