/**
* 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.archive;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.FileOperation;
import com.mucommander.commons.file.FileURL;
import com.mucommander.commons.file.ProxyFile;
import com.mucommander.commons.file.UnsupportedFileOperationException;
import com.mucommander.commons.file.filter.FileFilter;
import com.mucommander.commons.file.filter.FilenameFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.tree.DefaultMutableTreeNode;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import java.util.WeakHashMap;
/**
* <code>AbstractArchiveFile</code> is the superclass of all archive files. It allows archive file to be browsed as if
* they were regular directories, independently of the underlying protocol used to access the actual file.
* <p>
* <code>AbstractArchiveFile</code> extends {@link ProxyFile} to delegate the <code>AbstractFile</code>
* implementation to the actual archive file and overrides some methods to provide the added functionality.<br>
* There are two kinds of <code>AbstractArchiveFile</code>, both of which extend this class:
* <ul>
* <li>{@link AbstractROArchiveFile}: read-only archives, these are only able to perform read operations such as
* listing the archive's contents or retrieving a particular entry's contents.
* <li>{@link AbstractRWArchiveFile}: read-write archives, these are also able to modify the archive by adding or
* deleting an entry from the archive. These operations usually require random access to the underlying file,
* so write operations may not be available on all underlying file types. The {@link #isWritable()} method allows
* to determine whether the archive file is able to carry out write operations or not.
* </ul>
* When implementing a new archive file/format, either <code>AbstractROArchiveFile</code> or <code>AbstractRWArchiveFile</code>
* should be subclassed, but not this class.
* </p>
*
* <p>The first time one of the <code>ls()</code> methods is called to list the archive's contents,
* {@link #getEntryIterator()} is called to retrieve a list of *all* the entries contained by the archive, not only the
* ones at the top level but also the ones nested one of several levels below. Using this list of entries, it creates
* a tree to map the structure of the archive and list the content of any particular directory within the archive.
* This tree is recreated (<code>getEntryIterator()</code> is called again) only if the archive file has changed, i.e.
* if its date has changed since the tree was created.</p>
*
* <p>Files returned by the <code>ls()</code> are {@link AbstractArchiveEntryFile} instances which use an {@link ArchiveEntry}
* object to retrieve the entry's attributes. In turn, these <code>AbstractArchiveEntryFile</code> instances query the
* associated <code>AbstractArchiveFile</code> to list their content.
* <br>From an implementation perspective, one only needs to deal with {@link ArchiveEntry} instances, all the nuts
* and bolts are taken care of by this class.</p>
*
* <p>Note that an instance of <code>AbstractArchiveFile</code> may or may not actually be an archive:
* {@link #isArchive()} returns <code>true</code> only if the file currently exists and is not a directory. The value
* returned by {@link #isArchive()} may change over time as the file is modified. When an
* <code>AbstractArchiveFile</code> is not currently an archive, it acts just as a 'normal' file and delegates
* <code>ls()</code> methods to the underlying {@link AbstractFile}</p>
*
* @see com.mucommander.commons.file.FileFactory
* @see com.mucommander.commons.file.archive.ArchiveFormatProvider
* @see com.mucommander.commons.file.archive.ArchiveEntry
* @see AbstractArchiveEntryFile
* @see com.mucommander.commons.file.archiver.Archiver
* @author Maxence Bernard
*/
public abstract class AbstractArchiveFile extends ProxyFile {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractArchiveFile.class);
/** Archive entries tree */
protected ArchiveEntryTree entryTreeRoot;
/** Date this file had when the entries tree was created. Used to detect if the archive file has changed and entries
* need to be reloaded */
protected long entryTreeDate;
/** Caches {@link AbstractArchiveEntryFile} instances so that there is only one AbstractArchiveEntryFile
* corresponding to the same entry at any given time, to avoid attribute inconsistencies. The key is the
* corresponding ArchiveEntry. */
protected WeakHashMap<ArchiveEntry, AbstractArchiveEntryFile> archiveEntryFiles;
/**
* Creates an AbstractArchiveFile on top of the given file.
*
* @param file the file on top of which to create the archive
*/
protected AbstractArchiveFile(AbstractFile file) {
super(file);
}
/**
* Creates the entries tree, used by {@link #ls(AbstractArchiveEntryFile , com.mucommander.commons.file.filter.FilenameFilter, com.mucommander.commons.file.filter.FileFilter)}
* to quickly list the contents of an archive's subfolder.
*
* @throws IOException if an error occured while retrieving this archive's entries
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
protected void createEntriesTree() throws IOException, UnsupportedFileOperationException {
// TODO: this method is not thread-safe and needs to be synchronized
ArchiveEntryTree treeRoot = new ArchiveEntryTree();
archiveEntryFiles = new WeakHashMap<ArchiveEntry, AbstractArchiveEntryFile>();
long start = System.currentTimeMillis();
ArchiveEntryIterator entries = getEntryIterator();
try {
ArchiveEntry entry;
while((entry=entries.nextEntry())!=null)
treeRoot.addArchiveEntry(entry);
LOGGER.info("entries tree created in "+(System.currentTimeMillis()-start)+" ms");
this.entryTreeRoot = treeRoot;
declareEntriesTreeUpToDate();
}
finally {
try { entries.close(); }
catch(IOException e) {
// Not much we can do about it
}
}
}
/**
* Checks if the entries tree exists and if this file hasn't been modified since the tree was last created.
* If any of those 2 conditions isn't met, the entries tree is (re)created.
*
* @throws IOException if an error occurred while creating the tree
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
protected void checkEntriesTree() throws IOException, UnsupportedFileOperationException {
if(this.entryTreeRoot==null || getDate()!=this.entryTreeDate)
createEntriesTree();
}
/**
* Declares the entries tree up-to-date by setting the current tree date to the archive file's.
* This method should be called by {@link AbstractRWArchiveFile} implementations when the archive file has been
* modified and the entries propagated in the tree, to avoid the tree from being automatically re-created when
* {@link #checkEntriesTree()} is called.
*/
protected void declareEntriesTreeUpToDate() {
this.entryTreeDate = getDate();
}
/**
* Adds the given {@link ArchiveEntry} to the entries tree. This method will create the tree if it doesn't already
* exist, or re-create it if the archive file has changed since it was last created.
*
* @param entry the ArchiveEntry to add to the tree
* @throws IOException if an error occurred while creating the entries tree
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
protected void addToEntriesTree(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException {
checkEntriesTree();
entryTreeRoot.addArchiveEntry(entry);
}
/**
* Removes the given {@link ArchiveEntry} from the entries tree. This method will create the tree if it doesn't
* already exist, or re-create it if the archive file has changed since it was last created.
*
* @param entry the ArchiveEntry to remove from the tree
* @throws IOException if an error occurred while creating the entries tree
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
protected void removeFromEntriesTree(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException {
checkEntriesTree();
DefaultMutableTreeNode entryNode = entryTreeRoot.findEntryNode(entry.getPath());
if(entryNode!=null) {
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)entryNode.getParent();
parentNode.remove(entryNode);
}
}
/**
* Returns the {@link ArchiveEntryTree} instance corresponding to the root of the archive entry tree.
* The returned value can be <code>null</code> if the tree hasn't been intialized yet.
*
* @return the ArchiveEntryTree instance corresponding to the root of the archive entry tree
*/
ArchiveEntryTree getArchiveEntryTree() {
return entryTreeRoot;
}
/**
* Returns the contents of the specified folder entry.
*
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
protected AbstractFile[] ls(AbstractArchiveEntryFile entryFile, FilenameFilter filenameFilter, FileFilter fileFilter) throws IOException, UnsupportedFileOperationException {
// Make sure the entries tree is created and up-to-date
checkEntriesTree();
if(!entryFile.isBrowsable())
throw new IOException();
DefaultMutableTreeNode matchNode = entryTreeRoot.findEntryNode(entryFile.getEntry().getPath());
if(matchNode==null)
throw new IOException();
return ls(matchNode, entryFile, filenameFilter, fileFilter);
}
/**
* Returns the contents (direct children) of the specified tree node.
*
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
private AbstractFile[] ls(DefaultMutableTreeNode treeNode, AbstractFile parentFile, FilenameFilter filenameFilter, FileFilter fileFilter) throws IOException, UnsupportedFileOperationException {
AbstractFile files[];
int nbChildren = treeNode.getChildCount();
// No FilenameFilter, create entry files and store them directly into an array
if(filenameFilter==null) {
files = new AbstractFile[nbChildren];
for(int c=0; c<nbChildren; c++) {
files[c] = getArchiveEntryFile((ArchiveEntry)(((DefaultMutableTreeNode)treeNode.getChildAt(c)).getUserObject()), parentFile);
}
}
// Use provided FilenameFilter and temporarily store created entry files that match the filter in a Vector
else {
Vector<AbstractFile> filesV = new Vector<AbstractFile>();
for(int c=0; c<nbChildren; c++) {
ArchiveEntry entry = (ArchiveEntry)(((DefaultMutableTreeNode)treeNode.getChildAt(c)).getUserObject());
if(!filenameFilter.accept(entry.getName()))
continue;
filesV.add(getArchiveEntryFile(entry, parentFile));
}
files = new AbstractFile[filesV.size()];
filesV.toArray(files);
}
return fileFilter==null?files:fileFilter.filter(files);
}
/**
* Creates and returns an AbstractFile using the provided entry and parent file. This method takes care of
* creating the proper AbstractArchiveFile instance if the entry is itself an archive.
* The entry file's path will use the separator of the underlying file, as returned by {@link #getSeparator()}.
* That means entries paths of archives located on Windows local filesystems will use '\' as a separator, and
* '/' for Unix local archives.
*/
protected AbstractFile getArchiveEntryFile(ArchiveEntry entry, AbstractFile parentFile) throws IOException {
String entryPath = entry.getPath();
// If the parent file's separator is not '/' (the default entry separator), replace '/' occurrences by
// the parent file's separator. For local files Under Windows, this allows entries' path to have '\' separators.
String fileSeparator = getSeparator();
if(!fileSeparator.equals("/"))
entryPath = entryPath.replace("/", fileSeparator);
// Cache AbstractArchiveEntryFile instances so that there is only one AbstractArchiveEntryFile corresponding to
// the same entry at any given time, to avoid attribute inconsistencies.
AbstractArchiveEntryFile entryFile = archiveEntryFiles.get(entry);
if(entryFile==null) {
FileURL archiveURL = getURL();
FileURL entryURL = (FileURL)archiveURL.clone();
entryURL.setPath(addTrailingSeparator(archiveURL.getPath()) + entryPath);
// Create an RO and RW entry file, depending on whether this archive file is RO or RW
entryFile = this instanceof AbstractRWArchiveFile
?new RWArchiveEntryFile(
entryURL,
this,
entry
)
:new ROArchiveEntryFile(
entryURL,
this,
entry
);
entryFile.setParent(parentFile);
archiveEntryFiles.put(entry, entryFile);
}
return FileFactory.wrapArchive(entryFile);
}
/**
* Shorthand for {@link #getArchiveEntryFile(String)} called with the given entry's path.
*
* @param entry an entry contained by this archive
* @return an AbstractFile that corresponds to the given entry
* @throws IOException if neither the entry nor its parent exist within the archive
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
public AbstractFile getArchiveEntryFile(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException {
return getArchiveEntryFile(entry.getPath());
}
/**
* Creates and returns an AbstractFile that corresponds to the given entry path within the archive.
* The requested entry may or may not exist in the archive, the {@link #exists()} method of the returned entry file
* can be used to find this out. However, if the requested entry does not exist in the archive and is
* not located at the top level (i.e. is located in a subfolder), its parent folder must exist in the archive or
* else an <code>IOException</code> will be thrown.
*
* <p>Important note: the given path's separator character must be '/' and the path must be relative to the
* archive's root, i.e. not start with a leading '/', otherwise the entry will not be found.</p>
*
* @param entryPath path to an entry within this archive
* @return an AbstractFile that corresponds to the given entry path
* @throws IOException if neither the entry nor its parent exist within the archive
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
public AbstractFile getArchiveEntryFile(String entryPath) throws IOException, UnsupportedFileOperationException {
// Make sure the entries tree is created and up-to-date
checkEntriesTree();
// Todo: check if that's really necessary / if there is a way to remove this
entryPath = entryPath.replace('\\', '/');
// Find the entry node corresponding to the given path
DefaultMutableTreeNode entryNode = entryTreeRoot.findEntryNode(entryPath);
if(entryNode==null) {
int depth = ArchiveEntry.getDepth(entryPath);
AbstractFile parentFile;
if(depth==1)
parentFile = this;
else {
String parentPath = entryPath;
if(parentPath.endsWith("/"))
parentPath = parentPath.substring(0, parentPath.length()-1);
parentPath = parentPath.substring(0, parentPath.lastIndexOf('/'));
parentFile = getArchiveEntryFile(parentPath);
if(parentFile==null) // neither the entry nor the parent exist
throw new IOException();
}
return getArchiveEntryFile(new ArchiveEntry(entryPath, false, 0, 0, false), parentFile);
}
return getArchiveEntryFile(entryNode);
}
/**
* Creates and returns an {@link AbstractFile} instance corresponding to the given entry node.
* This method recurses to resolve the entry's parent file.
*
* @param entryNode tree node corresponding to the entry for which to return a file
* @return an {@link AbstractFile} instance corresponding to the given entry node
*/
protected AbstractFile getArchiveEntryFile(DefaultMutableTreeNode entryNode) throws IOException {
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)entryNode.getParent();
return getArchiveEntryFile(
(ArchiveEntry)entryNode.getUserObject(),
parentNode==entryTreeRoot
?this
:getArchiveEntryFile(parentNode)
);
}
//////////////////////
// Abstract methods //
//////////////////////
/**
* Returns an iterator of {@link ArchiveEntry} that iterates through all the entries of this archive.
* Implementations of this method should as much as possible return entries in their "natural order", i.e. the order
* in which they are stored in the archive.
* <p>
* This method is called the first time one of the <code>ls()</code> is called. It will not be called anymore,
* unless the file's date has changed since the last time one of the <code>ls()</code> methods was called.
* </p>
*
* @return an iterator of {@link ArchiveEntry} that iterates through all the entries of this archive
* @throws IOException if an error occurred while reading the archive, either because the archive is corrupt or
* because of an I/O error
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
public abstract ArchiveEntryIterator getEntryIterator() throws IOException, UnsupportedFileOperationException;
/**
* Returns an <code>InputStream</code> to read from the given archive entry. The specified {@link ArchiveEntry}
* instance must be one of the entries that were returned by the {@link ArchiveEntryIterator} returned by
* {@link #getEntryIterator()}.
*
* @param entry the archive entry to read
* @param entryIterator the iterator that is used to iterate through entries by the caller (if any). This parameter
* may be <code>null</code>, but when it is known, specifying may improve the performance of this method
* by an order of magnitude.
* @return an <code>InputStream</code> to read from the given archive entry
* @throws IOException if an error occurred while reading the archive, either because the archive is corrupt or
* because of an I/O error, or if the given entry wasn't found in the archive
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
public abstract InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException, UnsupportedFileOperationException;
/**
* Returns <code>true</code> if this archive file is writable, i.e. is capable of adding and deleting entries from
* the underlying archive file.
*
* <p>
* This method is implemented by {@link com.mucommander.commons.file.archive.AbstractROArchiveFile} and
* {@link com.mucommander.commons.file.archive.AbstractRWArchiveFile} to respectively return <code>false</code> and
* <code>true</code>. This method may be overridden by <code>AbstractRWArchiveFile</code> implementations if write
* access is only available under certain conditions, for example if it requires random write access to the
* proxied archive file (which may not always be available).
* Therefore, this method should be used to test if an <code>AbstractArchiveFile</code> is writable, rather than
* testing if it is an instance of <code>AbstractRWArchiveFile</code>.
* </p>
*
* @return <code>true</code> if this archive is writable, i.e. is capable of adding and deleting entries from
* the underlying archive file.
*/
public abstract boolean isWritable();
/////////////////////////////////////////
// Partial AbstractFile implementation //
/////////////////////////////////////////
@Override
public boolean isArchive() {
return exists() && !isDirectory();
}
////////////////////////
// Overridden methods //
////////////////////////
/**
* This method is overridden to list and return the topmost entries contained by this archive.
* The returned files are {@link AbstractArchiveEntryFile} instances.
*
* @return the topmost entries contained by this archive
* @throws IOException if the archive entries could not be listed
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
@Override
public AbstractFile[] ls() throws IOException, UnsupportedFileOperationException {
// Delegate to the ancestor if this file isn't actually an archive
if(!isArchive())
return super.ls();
// Make sure the entries tree is created and up-to-date
checkEntriesTree();
return ls(entryTreeRoot, this, null, null);
}
/**
* This method is overridden to list and return the topmost entries contained by this archive, filtering out
* the ones that do not match the specified {@link FilenameFilter}. The returned files are {@link AbstractArchiveEntryFile}
* instances.
*
* @param filter the FilenameFilter to be used to filter files out from the list, may be <code>null</code>
* @return the topmost entries contained by this archive
* @throws IOException if the archive entries could not be listed
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
@Override
public AbstractFile[] ls(FilenameFilter filter) throws IOException, UnsupportedFileOperationException {
// Delegate to the ancestor if this file isn't actually an archive
if(!isArchive())
return super.ls(filter);
// Make sure the entries tree is created and up-to-date
checkEntriesTree();
return ls(entryTreeRoot, this, filter, null);
}
/**
* This method is overridden to list and return the topmost entries contained by this archive, filtering out
* the ones that do not match the specified {@link FileFilter}. The returned files are {@link AbstractArchiveEntryFile} instances.
*
* @param filter the FilenameFilter to be used to filter files out from the list, may be <code>null</code>
* @return the topmost entries contained by this archive
* @throws IOException if the archive entries could not be listed
* @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the
* underlying file protocol.
*/
@Override
public AbstractFile[] ls(FileFilter filter) throws IOException, UnsupportedFileOperationException {
// Delegate to the ancestor if this file isn't actually an archive
if(!isArchive())
return super.ls(filter);
// Make sure the entries tree is created and up-to-date
checkEntriesTree();
return ls(entryTreeRoot, this, null, filter);
}
// Note: do not override #isDirectory() to always return true, as AbstractArchiveFile instances may be created when
// the file does not exist yet, and then be mkdir(): in that case, the file will be a directory and not an archive.
}