de.schlichtherle.io.ArchiveFileSystem Maven / Gradle / Ivy
Show all versions of truezip Show documentation
/*
* Copyright (C) 2005-2010 Schlichtherle IT Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.schlichtherle.io;
import de.schlichtherle.io.archive.spi.*;
import java.io.*;
import java.util.*;
import javax.swing.Icon;
/**
* This class implements a virtual file system of archive entries for use
* by the archive controller provided to the constructor.
*
* WARNING:This class is not thread safe!
* All calls to non-static methods must be synchronized on the
* respective {@code ArchiveController} object!
*
* @author Christian Schlichtherle
* @version $Id$
* @since TrueZIP 6.0 (refactored from the former {@code ZipFileSystem})
*/
final class ArchiveFileSystem implements Entry {
/**
* Denotes the entry name of the virtual root directory as a valid
* directory entry name.
*
* This constant cannot be used for identity comparison!
*
* @since TrueZIP 6.5
*/
private static final String ROOT_DIRECTORY_NAME = SEPARATOR;
/** The controller that this filesystem belongs to. */
private final ArchiveFileSystemController controller;
/** The read only status of this file system. */
private final boolean readOnly;
/**
* The map of ArchiveEntries in this file system.
* If this is a read-only file system, this is actually an unmodifiable
* map.
* This field should be considered final!
*
* Note that the ArchiveEntries in this map are shared with the
* {@link InputArchive} object provided to this class' constructor.
*/
private Map master;
/** The archive entry for the virtual root of this file system. */
private final ArchiveEntry root;
/** The number of times this file system has been modified (touched). */
private long touched;
/** For use by {@link #split} only! */
private final String[] split = new String[2];
/**
* Creates a new archive file system and ensures its integrity.
* The root directory is created with its last modification time set to
* the system's current time.
* The file system is modifiable and marked as touched!
*
* @param controller The controller which will use this file system.
* This constructor will finally call
* {@link ArchiveFileSystemController#touch} once it has fully
* initialized this instance.
*/
ArchiveFileSystem(final ArchiveFileSystemController controller)
throws IOException {
this.controller = controller;
touched = 1;
master = new LinkedHashMap(64);
// Setup root.
root = createArchiveEntry(ROOT_DIRECTORY_NAME);
root.setTime(System.currentTimeMillis());
master.put(ROOT_DIRECTORY_NAME, root);
readOnly = false;
controller.touch();
}
/**
* Mounts the archive file system from {@code archive} and ensures its
* integrity.
* First, a root directory with the given last modification time is
* created - it's never loaded from the archive!
* Then the entries from the archive are loaded into the file system and
* its integrity is checked:
* Any missing parent directories are created using the system's current
* time as their last modification time - existing directories will never
* be replaced.
*
* Note that the entries in this file system are shared with
* {@code archive}.
*
* @param controller The controller which will use this file system.
* This constructor will solely use the controller as a factory
* to create missing archive entries using
* {@link ArchiveFileSystemController#createArchiveEntry}.
* @param archive The archive to mount the file system from.
* @param rootTime The last modification time of the root of the mounted
* file system in milliseconds since the epoch.
* @param readOnly If and only if {@code true}, any subsequent
* modifying operation will result in a
* {@link ArchiveReadOnlyException}.
*/
ArchiveFileSystem(
final ArchiveFileSystemController controller,
final InputArchive archive,
final long rootTime,
final boolean readOnly) {
this.controller = controller;
final int iniCap = (int) (archive.getNumArchiveEntries() / 0.75f) + 1;
master = new LinkedHashMap(iniCap);
// Setup root.
root = createArchiveEntry(ROOT_DIRECTORY_NAME);
root.setTime(rootTime); // do NOT yet touch the file system!
master.put(ROOT_DIRECTORY_NAME, root);
Enumeration entries = archive.getArchiveEntries();
while (entries.hasMoreElements()) {
final ArchiveEntry entry = (ArchiveEntry) entries.nextElement();
final String entryName = getEntryName(entry);
// Map entry if it doesn't address the virtual root directory.
if (!ROOT_DIRECTORY_NAME.equals(entryName)
&& !("." + SEPARATOR).equals(entryName)) {
entry.setMetaData(new ArchiveEntryMetaData(entry));
master.put(entryName, entry);
}
}
// Now perform a file system check to fix missing parent directories.
// This needs to be done separately!
//entries = Collections.enumeration(master.values()); // concurrent modification!
entries = archive.getArchiveEntries();
while (entries.hasMoreElements())
fixParents((ArchiveEntry) entries.nextElement());
// Reset master map to be unmodifiable if this is a readonly file system
this.readOnly = readOnly;
if (readOnly)
master = Collections.unmodifiableMap(master);
assert touched == 0; // don't call !isTouched() - preconditions not met yet!
}
private String getEntryName(ArchiveEntry entry) {
// Fix issue #42 - see https://truezip.dev.java.net/issues/show_bug.cgi?id=42
return entry.getName().replace('\\', SEPARATOR_CHAR);
}
/**
* Checks whether the given entry entryName is a legal entry name.
* A legal entry name does not denote the virtual root directory, the dot
* directory ({@code "."}) or the dot-dot directory
* ({@code ".."}) or any of their descendants.
*/
private static boolean isLegalEntryName(final String entryName) {
final int l = entryName.length();
if (l <= 0)
return false; // never fix empty pathnames
switch (entryName.charAt(0)) {
case SEPARATOR_CHAR:
return false; // never fix root or absolute pathnames
case '.':
if (l >= 2) {
switch (entryName.charAt(1)) {
case '.':
if (l >= 3) {
if (entryName.charAt(2) == SEPARATOR_CHAR) {
assert entryName.startsWith(".." + SEPARATOR);
return false;
}
// Fall through.
} else {
assert "..".equals(entryName);
return false;
}
break;
case SEPARATOR_CHAR:
assert entryName.startsWith("." + SEPARATOR);
return false;
default:
// Fall through.
}
} else {
assert ".".equals(entryName);
return false;
}
break;
default:
// Fall through.
}
return true;
}
/**
* Called from a constructor to fix the parent directories of
* {@code entry}, ensuring that all parent directories of the entry
* exist and that they contain the respective child.
* If a parent directory does not exist, it is created using an
* unkown time as the last modification time - this is defined to be a
* ghost directory.
* If a parent directory does exist, the respective child is added
* (possibly yet again) and the process is continued.
*/
private void fixParents(final ArchiveEntry entry) {
final String entryName = getEntryName(entry);
if (isLegalEntryName(entryName) && SEPARATOR_CHAR != entryName.charAt(0))
fixParents(entryName);
}
private void fixParents(final String entryName) {
// When recursing into this method, it may be called with the root
// directory as its parameter, so we may NOT skip the following test.
if (ROOT_DIRECTORY_NAME.equals(entryName))
return; // never fix root or empty or absolute pathnames
final String split[] = split(entryName);
final String parentName = split[0];
final String baseName = split[1];
ArchiveEntry parent = (ArchiveEntry) master.get(parentName);
if (null == parent) {
parent = createArchiveEntry(parentName);
master.put(parentName, parent);
}
fixParents(parentName);
parent.getMetaData().children.add(baseName);
}
/**
* Splits the given entry name in a parent entry name and a base name.
*
* @param entryName The name of the entry which's parent entry name and
* base name are to be returned.
* @return The {@link #split} array, which will hold at least two strings:
*
* - Index 0 holds the parent entry name.
* If {@code entryName} is empty or equals
* {@code SEPARATOR}, this is {@code null}.
* Otherwise, this contains the parent name of the entry and
* always ends with an {@code SEPARATOR}.
*
- Index 1 holds the base name.
* If {@code entryName} is empty or equals
* {@code SEPARATOR}, this is an empty string.
* Otherwise, this contains the base name of the entry and
* never contains an {@code SEPARATOR}.
*
* @throws NullPointerException If {@code entryName} is
* {@code null}.
*/
private String[] split(final String entryName) {
//return Paths.split(entryName, SEPARATOR_CHAR, split);
return split(entryName, split);
}
// This method is package private only to enable unit tests!
static String[] split(final String entryName, final String[] result) {
assert entryName != null;
assert result != null;
assert result.length >= 2;
// Calculate index of last character, ignoring trailing entry separator.
int end = entryName.length();
if (0 <= --end)
if (entryName.charAt(end) == SEPARATOR_CHAR)
end--;
// Now look for the separator.
int base = entryName.lastIndexOf(SEPARATOR_CHAR, end);
end++; // convert end index to interval boundary
// Finally split according to our findings.
if (base != -1) { // found slash?
base++;
result[0] = entryName.substring(0, base); // include separator, may produce only separator!
result[1] = entryName.substring(base, end); // between separator and trailing separator
} else { // no slash
if (end > 0) { // At least one character exists, excluding a trailing separator?
result[0] = ROOT_DIRECTORY_NAME;
} else {
result[0] = null; // no parent
}
result[1] = entryName.substring(0, end); // between prefix and trailing separator
}
return result;
}
/**
* Indicates whether this file system is read only or not.
* The default is {@code false}.
*/
boolean isReadOnly() {
return readOnly;
}
/**
* Indicates whether this file system has been modified since
* its time of creation or the last call to {@code resetTouched()}.
*/
boolean isTouched() {
assert controller.getFileSystem() == this;
return touched != 0;
}
/**
* Ensures that the controller's data structures required to output
* entries are properly initialized and marks this virtual archive
* file system as touched.
*
* @throws ArchiveReadOnlyExceptionn If this virtual archive file system
* is read only.
* @throws IOException If setting up the required data structures in the
* controller fails for some reason.
*/
private void touch() throws IOException {
if (isReadOnly())
throw new ArchiveReadOnlyException();
// Order is important here because of exceptions!
if (touched == 0)
controller.touch();
touched++;
}
/**
* Returns an enumeration of all {@code ArchiveEntry} instances
* in this file system.
*/
Enumeration getArchiveEntries() {
assert controller.getFileSystem() == this;
return Collections.enumeration(master.values());
}
/**
* Returns the virtual root directory of this file system.
* This archive entry always exists.
* It's name may depend on the archive type.
* It's last modification time is guaranteed to be non-negative, so it's
* not a ghost directory!
*/
ArchiveEntry getRoot() {
assert controller.getFileSystem() == this;
return root;
}
/**
* Returns {@code true} iff the given entry name refers to the
* virtual root directory within this controller.
*/
static boolean isRoot(String entryName) {
return ROOT_NAME.equals(entryName); // possibly assigned by File.init(...)
}
/**
* Looks up the specified entry in the file system and returns it or
* {@code null} if not existent.
*/
ArchiveEntry get(String entryName) {
assert entryName != null;
assert controller.getFileSystem() == this;
return (ArchiveEntry) master.get(entryName);
}
/**
* Equivalent to {@link #link(String, boolean, ArchiveEntry)
* link(entryName, createParents, null)}.
*/
Delta link(final String entryName, final boolean createParents)
throws ArchiveFileSystemException {
return link(entryName, createParents, null);
}
/**
* Begins a "create and link entry" transaction to ensure that either a
* new entry for the given {@code entryName} will be created or an
* existing entry is replaced within this virtual archive file system.
*
* This is the first step of a two-step process to create an archive entry
* and link it into this virtual archive file system.
* To commit the transaction, call {@link Delta#commit} on the returned object
* after you have successfully conducted the operations which compose the
* transaction.
*
* Upon a {@code commit} operation, the last modification time of
* the newly created and linked entries will be set to the system's
* current time at the moment the transaction has begun and the file
* system will be marked as touched at the moment the transaction has
* been committed.
*
* Note that there is no rollback operation: After this method returns,
* nothing in the virtual file system has changed yet and all information
* required to commit the transaction is contained in the returned object.
* Hence, if the operations which compose the transaction fails, the
* returned object may be safely collected by the garbage collector,
*
* @param entryName The relative path name of the entry to create or replace.
* @param createParents If {@code true}, any non-existing parent
* directory will be created in this file system with its last
* modification time set to the system's current time.
* @param template If not {@code null}, then the newly created or
* replaced entry shall inherit as much properties from this
* instance as possible (with the exception of the name).
* This is typically used for archive copy operations and requires
* some support by the archive driver.
* @return A transaction object. You must call its
* {@link Delta#commit} method in order to commit
* link the newly created entry into this virtual archive file
* system.
* @throws ArchiveReadOnlyExceptionn If this virtual archive file system
* is read only.
* @throws ArchiveFileSystemException If one of the following is true:
*
* - {@code entryName} contains characters which are not
* supported by the archive file.
*
- The entry name indicates a directory (trailing {@code /})
* and its entry does already exist within this file system.
*
- The entry is a file or directory and does already exist as
* the respective other type within this file system.
*
- The parent directory does not exist and
* {@code createParents} is {@code false}.
*
- One of the entry's parents denotes a file.
*
*/
Delta link(
final String entryName,
final boolean createParents,
final ArchiveEntry template)
throws ArchiveFileSystemException {
assert isRoot(entryName) || entryName.charAt(0) != SEPARATOR_CHAR;
assert controller.getFileSystem() == this;
if (isRoot(entryName))
throw new ArchiveFileSystemException(entryName,
"virtual root directory cannot get replaced");
return new LinkDelta(entryName, createParents, template);
}
/**
* A simple transaction for creating (and hence probably replacing) and
* linking an entry in this virtual archive file system.
*
* @see #link
*/
private final class LinkDelta extends AbstractDelta {
final Element[] elements;
private LinkDelta(
final String entryName,
final boolean createParents,
final ArchiveEntry template)
throws ArchiveFileSystemException {
if (isReadOnly())
throw new ArchiveReadOnlyException();
try {
elements = createElements(entryName, createParents, template, 1);
} catch (CharConversionException cce) {
final ArchiveFileSystemException afse
= new ArchiveFileSystemException(cce.toString());
afse.initCause(cce);
throw afse;
}
}
private Element[] createElements(
final String entryName,
final boolean createParents,
final ArchiveEntry template,
final int level)
throws ArchiveFileSystemException, CharConversionException {
final String split[] = split(entryName);
final String parentName = split[0]; // could be separator only to indicate root
final String baseName = split[1];
final Element[] elements;
// Lookup parent entry, creating it where necessary and allowed.
final ArchiveEntry parent = (ArchiveEntry) master.get(parentName);
final ArchiveEntry entry;
if (parent != null) {
final ArchiveEntry oldEntry
= (ArchiveEntry) master.get(entryName);
ensureMayBeReplaced(entryName, oldEntry);
elements = new Element[level + 1];
elements[0] = new Element(parentName, parent);
entry = createArchiveEntry(entryName, template);
elements[1] = new Element(baseName, entry);
} else if (createParents) {
elements = createElements(
parentName, createParents, null, level + 1);
entry = createArchiveEntry(entryName, template);
elements[elements.length - level]
= new Element(baseName, entry);
} else {
throw new ArchiveFileSystemException(entryName,
"missing parent directory");
}
return elements;
}
private void ensureMayBeReplaced(
final String entryName,
final ArchiveEntry oldEntry)
throws ArchiveFileSystemException {
final int end = entryName.length() - 1;
if (entryName.charAt(end) == SEPARATOR_CHAR) { // entryName indicates directory
if (oldEntry != null)
throw new ArchiveFileSystemException(entryName,
"directories cannot get replaced");
if (master.get(entryName.substring(0, end)) != null)
throw new ArchiveFileSystemException(entryName,
"directories cannot replace files");
} else { // entryName indicates file
if (master.get(entryName + SEPARATOR) != null)
throw new ArchiveFileSystemException(entryName,
"files cannot replace directories");
}
}
/** Links the entries into this virtual archive file system. */
public void commit() throws IOException {
assert controller.getFileSystem() == ArchiveFileSystem.this;
assert elements.length >= 2;
touch();
final long time = System.currentTimeMillis();
final int l = elements.length;
ArchiveEntry parent = elements[0].entry;
for (int i = 1; i < l ; i++) {
final Element element = elements[i];
final String baseName = element.baseName;
final ArchiveEntry entry = element.entry;
if (parent.getMetaData().children.add(baseName)
&& parent.getTime() != ArchiveEntry.UNKNOWN) // never touch ghosts!
parent.setTime(time);
master.put(entry.getName(), entry);
parent = entry;
}
final ArchiveEntry entry = elements[l - 1].entry;
if (entry.getTime() == ArchiveEntry.UNKNOWN)
entry.setTime(time);
}
public ArchiveEntry getEntry() {
assert controller.getFileSystem() == ArchiveFileSystem.this;
return elements[elements.length - 1].entry;
}
} // class LinkDelta
private static abstract class AbstractDelta implements Delta {
/** A data class for use by subclasses. */
static class Element {
final String baseName;
final ArchiveEntry entry;
// This constructor is provided for convenience only.
Element(String baseName, ArchiveEntry entry) {
this.baseName = baseName; // may be null!
assert entry != null;
this.entry = entry;
}
}
} // class AbstractDelta
/**
* This interface encapsulates the methods required to begin and commit
* a simplified transaction (a delta) on this virtual archive file system.
*
* Note that there is no {@code begin} or {@code rollback}
* method in this class.
* Instead, {@code begin} is expected to be implemented by the
* constructor of the implementation and must not modify the file system,
* so that an explicit {@code rollback} is not required.
*/
interface Delta {
/**
* Returns the entry operated by this file system delta.
*/
ArchiveEntry getEntry();
/**
* Commits the simplified transaction, possibly modifying the
* enclosing virtual archive file system.
*
* @throws IOException If the commit operation fails for any I/O
* related reason.
*/
void commit() throws IOException;
} // interface Delta
/**
* Creates an archive entry which is going to be linked into this virtual
* archive file system in the near future.
* The returned entry has properly initialized meta data, but is
* otherwise left as created by the archive driver.
*
* @param entryName The path name of the entry to create or replace.
* This must be a relative path name.
* @param blueprint If not {@code null}, then the newly created entry
* shall inherit as much attributes from this object as possible
* (with the exception of the name).
* This is typically used for archive copy operations and requires
* some support by the archive driver.
* @return An {@link ArchiveEntry} created by the archive driver and
* properly initialized with meta data.
* @throws CharConversionException If {@code entryName} contains
* characters which are not supported by the archive file.
*/
private ArchiveEntry createArchiveEntry(
final String entryName,
final ArchiveEntry blueprint)
throws CharConversionException {
final ArchiveEntry entry
= controller.createArchiveEntry(entryName, blueprint);
entry.setMetaData(new ArchiveEntryMetaData(entry));
return entry;
}
/**
* Like {@link #createArchiveEntry}, but throws an
* {@code AssertionError} instead of
* {@code CharConversionException}.
*
* @throws AssertionError If a {@link CharConversionException} occurs.
*/
private ArchiveEntry createArchiveEntry(final String entryName) {
try {
return createArchiveEntry(entryName, null);
} catch (CharConversionException ex) {
throw new AssertionError(ex);
}
}
/**
* If this method returns, the entry identified by the given
* {@code entryName} has been successfully deleted from the virtual
* archive file system.
* If the entry is a directory, it must be empty for successful deletion.
*
* @throws ArchiveReadOnlyExceptionn If the virtual archive file system is
* read only.
* @throws ArchiveIllegalOperationException If the operation failed for
* any other reason.
*/
private void unlink(final String entryName)
throws IOException {
if (isRoot(entryName))
throw new ArchiveFileSystemException(entryName,
"virtual root directory cannot get unlinked");
try {
final ArchiveEntry entry = (ArchiveEntry) master.remove(entryName);
if (entry == null)
throw new ArchiveFileSystemException(entryName,
"entry does not exist");
if (entry == root
|| entry.isDirectory()
&& !entry.getMetaData().children.isEmpty()) {
master.put(entryName, entry); // Restore file system
throw new ArchiveFileSystemException(entryName,
"directory is not empty");
}
final String split[] = split(entryName);
final String parentName = split[0];
final ArchiveEntry parent = (ArchiveEntry) master.get(parentName);
assert parent != null : "The parent directory of \"" + entryName
+ "\" is missing - archive file system is corrupted!";
final boolean ok = parent.getMetaData().children.remove(split[1]);
assert ok : "The parent directory of \"" + entryName
+ "\" does not contain this entry - archive file system is corrupted!";
touch();
if (parent.getTime() != ArchiveEntry.UNKNOWN) // never touch ghosts!
parent.setTime(System.currentTimeMillis());
} catch (UnsupportedOperationException unmodifiableMap) {
throw new ArchiveReadOnlyException();
}
}
//
// File system operations used by the ArchiveController class:
//
boolean exists(final String entryName) {
return get(entryName) != null
|| get(entryName + SEPARATOR) != null;
}
boolean isFile(final String entryName) {
/*ArchiveEntry entry = get(entryName);
if (entry == null)
entry = get(entryName + SEPARATOR);
return entry != null && !entry.isDirectory();*/
return get(entryName) != null;
}
boolean isDirectory(final String entryName) {
/*ArchiveEntry entry = get(entryName + SEPARATOR);
if (entry == null)
entry = get(entryName);
return entry != null && entry.isDirectory();*/
return get(entryName + SEPARATOR) != null;
}
Icon getOpenIcon(final String entryName) {
assert !isRoot(entryName);
ArchiveEntry entry = get(entryName);
if (entry == null)
entry = get(entryName + SEPARATOR);
return entry != null ? entry.getOpenIcon() : null;
}
Icon getClosedIcon(final String entryName) {
assert !isRoot(entryName);
ArchiveEntry entry = get(entryName);
if (entry == null)
entry = get(entryName + SEPARATOR);
return entry != null ? entry.getClosedIcon() : null;
}
boolean canWrite(final String entryName) {
return !isReadOnly() && exists(entryName);
}
boolean setReadOnly(final String entryName) {
return isReadOnly() && exists(entryName);
}
long length(final String entryName) {
final ArchiveEntry entry = get(entryName);
if (entry == null || entry.isDirectory())
return 0;
// TODO: Review: Can we avoid this special case?
// It's probably ZipDriver specific!
// This entry is a plain file in the file system.
// If entry.getSize() returns ArchiveEntry.UNKNOWN, the length is yet unknown.
// This may happen if e.g. a ZIP entry has only been partially
// written, i.e. not yet closed by another thread, or if this is a
// ghost directory.
// As this is not specified in the contract of the File class, return
// 0 in this case instead.
final long length = entry.getSize();
return length != ArchiveEntry.UNKNOWN ? length : 0;
}
long lastModified(final String entryName) {
ArchiveEntry entry = get(entryName);
if (entry == null)
entry = get(entryName + SEPARATOR);
if (entry != null) {
// Depending on the driver type, entry.getTime() could return
// a negative value. E.g. this is the default value that the
// ArchiveDriver uses for newly created entries in order to
// indicate an unknown time.
// As this is not specified in the contract of the File class,
// 0 is returned in this case instead.
final long time = entry.getTime();
return time >= 0 ? time : 0;
}
// This entry does not exist.
return 0;
}
boolean setLastModified(final String entryName, final long time)
throws IOException {
if (time < 0)
throw new IllegalArgumentException(entryName +
" (negative entry modification time)");
if (isReadOnly())
return false;
ArchiveEntry entry = get(entryName);
if (entry == null) {
entry = get(entryName + SEPARATOR);
if (entry == null) {
// This entry does not exist.
return false;
}
}
// Order is important here!
touch();
entry.setTime(time);
return true;
}
String[] list(final String entryName) {
// Lookup the entry as a directory.
final ArchiveEntry entry = get(entryName + SEPARATOR);
if (entry != null)
return entry.getMetaData().list();
else
return null; // does not exist as a directory
}
String[] list(
final String entryName,
final FilenameFilter filenameFilter,
final File dir) {
// Lookup the entry as a directory.
final ArchiveEntry entry = get(entryName + SEPARATOR);
if (entry != null)
if (filenameFilter != null)
return entry.getMetaData().list(filenameFilter, dir);
else
return entry.getMetaData().list(); // most efficient
else
return null; // does not exist as directory
}
File[] listFiles(
final String entryName,
final FilenameFilter filenameFilter,
final File dir,
final FileFactory factory) { // deprecated warning is OK!
// Lookup the entry as a directory.
final ArchiveEntry entry = get(entryName + SEPARATOR);
if (entry != null)
return entry.getMetaData().listFiles(filenameFilter, dir, factory);
else
return null; // does not exist as a directory
}
File[] listFiles(
final String entryName,
final FileFilter fileFilter,
final File dir,
final FileFactory factory) { // deprecated warning is OK!
// Lookup the entry as a directory.
final ArchiveEntry entry = get(entryName + SEPARATOR);
if (entry != null)
return entry.getMetaData().listFiles(fileFilter, dir, factory);
else
return null; // does not exist as a directory
}
void mkdir(String entryName, boolean createParents)
throws IOException {
link(entryName + SEPARATOR, createParents).commit();
}
void delete(final String entryName)
throws IOException {
assert isRoot(entryName) || entryName.charAt(0) != SEPARATOR_CHAR;
if (get(entryName) != null) {
unlink(entryName);
return;
}
final String dirEntryName = entryName + SEPARATOR;
if (get(dirEntryName) != null) {
unlink(dirEntryName);
return;
}
throw new ArchiveFileSystemException(entryName,
"archive entry does not exist");
}
//
// Exceptions:
//
/**
* This exception is thrown when a client application tries to perform an
* illegal operation on an archive file system.
*
* This exception is private by intention: Clients applications should not
* even know about the existence of virtual archive file systems.
*/
static class ArchiveFileSystemException extends IOException {
private static final long serialVersionUID = 7625038629582374837L;
/** The entry's path name. */
private final String entryName;
private ArchiveFileSystemException(String message) {
super(message);
this.entryName = null;
}
private ArchiveFileSystemException(String entryName, String message) {
super(message);
this.entryName = entryName;
}
public String getMessage() {
// For performance reasons, this string is constructed on demand
// only!
return entryName != null
? entryName + " (" + super.getMessage() + ")"
: super.getMessage();
}
}
/**
* This exception is thrown when a client tries to modify a read only
* virtual archive file system.
*/
static class ArchiveReadOnlyException extends ArchiveFileSystemException {
private static final long serialVersionUID = 7625038627494543837L;
private ArchiveReadOnlyException() {
super("Archive file is read-only!");
}
}
}