All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.io.ArchiveController Maven / Gradle / Ivy

Go to download

TrueZIP is a Java based Virtual File System (VFS) to enable transparent, multi-threaded read/write access to archive files (ZIP, TAR etc.) as if they were directories. Archive files may be arbitrarily nested and the nesting level is only limited by heap and file system size.

The newest version!
/*
 * Copyright (C) 2004-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.ArchiveFileSystem.Delta;
import de.schlichtherle.io.archive.Archive;
import de.schlichtherle.io.archive.spi.ArchiveDriver;
import de.schlichtherle.io.archive.spi.ArchiveEntry;
import de.schlichtherle.io.archive.spi.TransientIOException;
import de.schlichtherle.key.PromptingKeyManager;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import javax.swing.Icon;

/**
 * This is the base class for any archive controller, providing all the
 * essential services required by the {@link File} class to implement its
 * behaviour.
 * Each instance of this class manages a globally unique archive file
 * (the target file) in order to allow random access to it as if it
 * were a regular directory in the real file system.
 * 

* In terms of software patterns, an {@code ArchiveController} is * similar to a Director in a Builder pattern, with the {@link ArchiveDriver} * interface as its Builder or Abstract Factory. * However, an archive controller does not necessarily build a new archive. * It may also simply be used to access an existing archive for read-only * operations, such as listing its top level directory, or reading entry data. * Whatever type of operation it's used for, an archive controller provides * and controls all access to any particular archive file by the * client application and deals with the rather complex details of its * states and transitions. *

* Each instance of this class maintains a virtual file system, provides input * and output streams for the entries of the archive file and methods * to update the contents of the virtual file system to the target file * in the real file system. * In cooperation with the {@link File} class, it also knows how to deal with * nested archive files (such as {@code "outer.zip/inner.tar.gz"} * and false positives, i.e. plain files or directories or file or * directory entries in an enclosing archive file which have been incorrectly * recognized to be prospective archive files by the * {@link ArchiveDetector} interface. *

* To ensure that for each archive file there is at most one * {code ArchiveController}, the path name of the archive file (called * target) is canonicalized, so it doesn't matter whether the * {@link File} class addresses an archive file as {@code "archive.zip"} * or {@code "/dir/archive.zip"} if {@code "/dir"} is the client * application's current directory. *

* Note that in general all of its methods are reentrant on exceptions. * This is important because the {@link File} class may repeatedly call them, * triggered by the client application. Of course, depending on the context, * some or all of the archive file's data may be lost in this case. * For more information, please refer to {@link File#umount} and * {@link File#update}. *

* This class is actually the abstract base class for any archive controller. * It encapsulates all the code which is not depending on a particular entry * synchronization strategy and the corresponding state of the controller. * Though currently unused, this is intended to be helpful for future * extensions of TrueZIP, where different synchronization strategies may be * implemented. * * @author Christian Schlichtherle * @version $Id: ArchiveController.java,v 1.4 2010/08/31 17:08:23 christian_schlichtherle Exp $ * @since TrueZIP 6.0 */ abstract class ArchiveController implements Archive, Entry { // // Static fields. // /*private static final String CLASS_NAME = "de.schlichtherle.io.ArchiveController"; private static final Logger logger = Logger.getLogger(CLASS_NAME, CLASS_NAME);*/ // // Instance fields. // /** * A weak reference to this archive controller. * This field is for exclusive use by {@link #setScheduled(boolean)}. */ private final WeakReference weakThis = new WeakReference(this); /** * The canonicalized or at least normalized absolute path name * representation of the target file. */ private final java.io.File target; /** * The archive controller of the enclosing archive, if any. */ private final ArchiveController enclController; /** * The name of the entry for this archive in the enclosing archive, if any. */ private final String enclEntryName; /** * The {@link ArchiveDriver} to use for this controller's target file. */ private /*volatile*/ ArchiveDriver driver; private final ReentrantLock readLock; private final ReentrantLock writeLock; // // Constructors. // /** * This constructor schedules this controller to be thrown away if no * more {@code File} objects are referring to it. * The subclass must update this schedule according to the controller's * state. * For example, if the controller has started to update some entry data, * it must call {@link #setScheduled} in order to force the * controller to be updated on the next call to {@link #umount} even if * no more {@code File} objects are referring to it. * Otherwise, all changes may get lost! * * @see #setScheduled(boolean) */ ArchiveController( final java.io.File target, final ArchiveController enclController, final String enclEntryName, final ArchiveDriver driver) { assert target != null; assert target.isAbsolute(); assert (enclController != null) == (enclEntryName != null); assert driver != null; this.target = target; this.enclController = enclController; this.enclEntryName = enclEntryName; this.driver = driver; ReadWriteLock rwl = new ReentrantReadWriteLock(); this.readLock = rwl.readLock(); this.writeLock = rwl.writeLock(); setScheduled(false); } // // Methods. // final ReentrantLock readLock() { return readLock; } final ReentrantLock writeLock() { return writeLock; } /** * Runs the given {@link IORunnable} while this controller has * acquired its write lock regardless of the state of its read lock. * You must use this method if this controller may have acquired a * read lock in order to prevent a dead lock. *

* Warning: This method temporarily releases the read lock * before the write lock is acquired and the runnable is run! * Hence, the runnable should recheck the state of the controller * before it proceeds with any write operations. * * @param runnable The {@link IORunnable} to run while the write * lock is acquired. * No read lock is acquired while it's running. */ final void runWriteLocked(IORunnable runnable) throws IOException { // A read lock cannot get upgraded to a write lock. // Hence the following mess is required. // Note that this is not just a limitation of the current // implementation in JSE 5: If automatic upgrading were implemented, // two threads holding a read lock try to upgrade concurrently, // they would dead lock each other! final int lockCount = readLock().lockCount(); for (int c = lockCount; c > 0; c--) readLock().unlock(); // The current thread may get deactivated here! writeLock().lock(); try { try { runnable.run(); } finally { // Restore lock count - effectively downgrading the lock for (int c = lockCount; c > 0; c--) readLock().lock(); } } finally { writeLock().unlock(); } } /** * Returns the canonical or at least normalized absolute * {@code java.io.File} object for the archive file to control. */ final java.io.File getTarget() { return target; } public final String getPath() { return target.getPath(); } public final Archive getEnclArchive() { return enclController; } /** * Returns {@code true} iff the given entry name refers to the * virtual root directory within this controller. */ static final boolean isRoot(String entryName) { return ROOT_NAME == entryName; // possibly assigned by File.init(...), so using == is OK! } /** * Returns the {@link ArchiveController} of the enclosing archive file, * if any. */ final ArchiveController getEnclController() { return enclController; } /** * Returns the entry name of this controller within the enclosing archive * file, if any. */ final String getEnclEntryName() { return enclEntryName; } final String enclEntryName(final String entryName) { return isRoot(entryName) ? enclEntryName : enclEntryName + SEPARATOR + entryName; } private final boolean isEnclosedBy(ArchiveController wannabe) { assert wannabe != null; if (enclController == wannabe) return true; if (enclController == null) return false; return enclController.isEnclosedBy(wannabe); } /** * Returns the driver instance which is used for the target archive. * All access to this method must be externally synchronized on this * controller's read lock! * * @return A valid reference to an {@link ArchiveDriver} object * - never {@code null}. */ final ArchiveDriver getDriver() { return driver; } /** * Sets the driver instance which is used for the target archive. * All access to this method must be externally synchronized on this * controller's write lock! * * @param driver A valid reference to an {@link ArchiveDriver} object * - never {@code null}. */ final void setDriver(ArchiveDriver driver) { assert writeLock().isLocked(); // This affects all subsequent creations of the driver's products // (In/OutputArchive and ArchiveEntry) and hence ArchiveFileSystem. // Normally, these are initialized together in mountFileSystem(...) // which is externally synchronized on this controller's write lock, // so we don't need to be afraid of this. this.driver = driver; } /** * Returns {@code true} if and only if the target file of this * controller should be considered to be a file or directory in the real * file system (RFS). * Note that the target doesn't need to exist for this method to return * {@code true}. */ final boolean isRfsEntryTarget() { // May be called from FileOutputStream while unlocked! //assert readLock().isLocked() || writeLock().isLocked(); // True iff not enclosed or the enclosing archive file is actually // a plain directory. return enclController == null || enclController.getTarget().isDirectory(); } /** * Returns {@code true} if and only if the file system has been * touched. */ abstract boolean isTouched(); /** * (Re)schedules this archive controller for the next call to * {@link ArchiveControllers#umount(String, boolean, boolean, boolean, boolean, boolean)}. * * @param scheduled If set to {@code true}, this controller and hence * its target archive file is guaranteed to get updated during the * next call to {@code ArchiveControllers.umount()} even if * there are no more {@link File} instances referring to it * meanwhile. * Call this method with this parameter value whenever the virtual * file system has been touched, i.e. modified. *

* If set to {@code false}, this controller is conditionally * scheduled to get updated. * In this case, the controller gets automatically removed from * the controllers weak hash map and discarded once the last file * object directly or indirectly referring to it has been discarded * unless {@code setScheduled(true)} has been called meanwhile. * Call this method if the archive controller has been newly created * or successfully updated. */ final void setScheduled(final boolean scheduled) { assert weakThis.get() != null || !scheduled; // (garbage collected => no scheduling) == (scheduling => not garbage collected) ArchiveControllers.set( getTarget(), scheduled ? (Object) this : weakThis); } /** * Tests if the archive entry with the given name has received or is * currently receiving new data via an output stream. * As an implication, the entry cannot receive new data from another * output stream before the next call to {@link #umount}. * Note that for directories this method will always return * {@code false}! */ abstract boolean hasNewData(String entryName); /** * Returns the virtual archive file system mounted from the target file. * This method is reentrant with respect to any exceptions it may throw. *

* Warning: Either the read or the write lock of this controller * must be acquired while this method is called! * If only a read lock is acquired, but a write lock is required, this * method will temporarily release all locks, so any preconditions must be * checked again upon return to protect against concurrent modifications! * * @param create If the archive file does not exist and this is * {@code true}, a new file system with only a virtual root * directory is created with its last modification time set to the * system's current time. * @return A valid archive file system - {@code null} is never returned. * @throws FalsePositiveException * @throws IOException On any other I/O related issue with the target file * or the target file of any enclosing archive file's controller. */ abstract ArchiveFileSystem autoMount(boolean create) throws IOException; /** * Unmounts the archive file only if the archive file has already new * data for {@code entryName}. *

* Warning: As a side effect, all data structures returned by this * controller get reset (filesystem, entries, streams, etc.)! * As an implication, this method requires external synchronization on * this controller's write lock! *

* TODO: Consider adding configuration switch to allow overwriting * an archive entry to the same output archive multiple times, whereby * only the last written entry would be added to the central directory * of the archive (unless the archive type doesn't support this). * * @see #umount(ArchiveException, boolean, boolean, boolean, boolean, boolean, boolean) * @see ArchiveException */ final void autoUmount(final String entryName) throws ArchiveException { assert writeLock().isLocked(); if (hasNewData(entryName)) umount(null, true, false, true, false, false, false); } /** * Synchronizes the contents of the target archive file managed by this * archive controller to the real file system. *

* Warning: As a side effect, all data structures returned by this * controller get reset (filesystem, entries, streams, etc.)! * As an implication, this method requires external synchronization on * this controller's write lock! * * @param waitInputStreams See {@link ArchiveControllers#umount}. * @param closeInputStreams See {@link ArchiveControllers#umount}. * @param waitOutputStreams See {@link ArchiveControllers#umount}. * @param closeOutputStreams See {@link ArchiveControllers#umount}. * @param umount See {@link ArchiveControllers#umount}. * @param reassemble Let's assume this archive file is enclosed * in another archive file. * Then if this parameter is {@code true}, the updated archive * file is also written to its enclosing archive file. * Note that this parameter must be set if {@code umount} * is set as well. Failing to comply to this requirement may throw * a {@link java.lang.AssertionError} and will incur loss of data! * @see #autoUmount * @see ArchiveException * @throws ArchiveException If any exception condition occurs throughout * the course of this method, an {@link ArchiveException} * is created, prepended to {@code exceptionChain} and finally * thrown. */ abstract void umount( ArchiveException exceptionChain, final boolean waitInputStreams, final boolean closeInputStreams, final boolean waitOutputStreams, final boolean closeOutputStreams, final boolean umount, final boolean reassemble) throws ArchiveException; /* FIXME: * The implementation in the class {@code ArchiveController} just * deletes the entries in this archive controller which have been passed * to the {@link #deleteOnUmount} method. * It is up to the subclass to implement the updating strategy for the * target archive file. * When overriding this method, the subclass must call the implementation * of this class before the actual update is performed! */ /*{ assert closeInputStreams || !closeOutputStreams; // closeOutputStreams => closeInputStreams assert !umount || reassemble; // umount => reassemble assert writeLock().isLocked(); int deleted = 0; // Do the logging part and leave the work to umount0. Object[] stats = new Object[] { getPath(), exceptionChain, Boolean.valueOf(waitInputStreams), Boolean.valueOf(closeInputStreams), Boolean.valueOf(waitOutputStreams), Boolean.valueOf(closeOutputStreams), Boolean.valueOf(umount), Boolean.valueOf(reassemble), // TODO: For JSE 5: Integer.valueOf(deleted), new Integer(deleted), }; logger.log(Level.FINEST, "umount.entering", stats); // NOI18N try { deleted = umount0(exceptionChain, waitInputStreams, closeInputStreams, waitOutputStreams, closeOutputStreams, umount, reassemble); } catch (ArchiveException ex) { logger.log(Level.FINEST, "umount.throwing", ex); // NOI18N throw ex; } stats = new Object[] { // update getPath(), exceptionChain, Boolean.valueOf(waitInputStreams), Boolean.valueOf(closeInputStreams), Boolean.valueOf(waitOutputStreams), Boolean.valueOf(closeOutputStreams), Boolean.valueOf(umount), Boolean.valueOf(reassemble), // TODO: For JSE 5: Integer.valueOf(deleted), new Integer(deleted), }; logger.log(Level.FINEST, "umount.exiting", stats); // NOI18N } private int umount0( ArchiveException exceptionChain, final boolean waitInputStreams, final boolean closeInputStreams, final boolean waitOutputStreams, final boolean closeOutputStreams, final boolean umount, final boolean reassemble) throws ArchiveException { //System.err.println("FIXME: Write algorithm to delete archive entries on umount!"); return 0; }*/ // TODO: Document this! abstract int waitAllInputStreamsByOtherThreads(long timeout); // TODO: Document this! abstract int waitAllOutputStreamsByOtherThreads(long timeout); /** * Resets the archive controller to its initial state - all changes to the * archive file which have not yet been updated get lost! * Thereafter, the archive controller will behave as if it has just been * created and any subsequent operations on its entries will remount * the virtual file system from the archive file again. *

* This method should be overridden by subclasses, but must still be * called when doing so. */ abstract void reset() throws IOException; public String toString() { return getClass().getName() + "@" + System.identityHashCode(this) + "(" + getPath() + ")"; } // // File system operations used by the File* classes. // Stream operations: // /** * A factory method returning an input stream which is positioned * at the beginning of the given entry in the target archive file. * * @param entryName An entry in the virtual archive file system * - {@code null} or {@code ""} is not permitted. * @return A valid {@code InputStream} object * - {@code null} is never returned. * @throws FileNotFoundException If the entry cannot get read for * any reason. */ final InputStream createInputStream(final String entryName) throws FileNotFoundException { assert entryName != null; try { return createInputStream0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.createInputStream(enclEntryName(entryName)); } catch (FileNotFoundException ex) { // includes RfsEntryFalsePositiveException! throw ex; } catch (ArchiveBusyException ex) { throw new FileBusyException(ex); } catch (IOException ioe) { final FileNotFoundException fnfe = new FileNotFoundException(ioe.toString()); fnfe.initCause(ioe); throw fnfe; } } InputStream createInputStream0(final String entryName) throws IOException { assert entryName != null; readLock().lock(); try { if (isRoot(entryName)) { try { final boolean directory = isDirectory0(entryName); // detect false positives assert directory : "The root entry must be a directory!"; } catch (FalsePositiveException ex) { if (!(ex.getCause() instanceof FileNotFoundException)) throw ex; } throw new ArchiveEntryNotFoundException(entryName, "cannot read (potential) virtual root directory"); } else { if (hasNewData(entryName)) { runWriteLocked(new IORunnable() { public void run() throws IOException { autoUmount(entryName); } }); } final ArchiveEntry entry = autoMount(false).get(entryName); // lookup file entries only! if (entry == null) throw new ArchiveEntryNotFoundException(entryName, "no such file entry"); return createInputStream(entry, null); } } finally { readLock().unlock(); } } /** * Important: *

    *
  • This controller's read or write lock must be acquired. *
  • {@code entry} must not have received * {@link #hasNewData new data}. *
      */ abstract InputStream createInputStream( ArchiveEntry entry, ArchiveEntry dstEntry) throws IOException; /** * A factory method returning an {@code OutputStream} allowing to * (re)write the given entry in the target archive file. * * @param entryName An entry in the virtual archive file system * - {@code null} or {@code ""} is not permitted. * @return A valid {@code OutputStream} object * - {@code null} is never returned. * @throws FileNotFoundException If the entry cannot get (re)written for * any reason. */ final OutputStream createOutputStream( final String entryName, final boolean append) throws FileNotFoundException { assert entryName != null; try { return createOutputStream0(entryName, append); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.createOutputStream(enclEntryName(entryName), append); } catch (FileNotFoundException ex) { // includes RfsEntryFalsePositiveException! throw ex; } catch (ArchiveBusyException ex) { throw new FileBusyException(ex); } catch (IOException ioe) { final FileNotFoundException fnfe = new FileNotFoundException(ioe.toString()); fnfe.initCause(ioe); throw fnfe; } } OutputStream createOutputStream0( final String entryName, final boolean append) throws IOException { assert entryName != null; final InputStream in; final OutputStream out; writeLock().lock(); try { if (isRoot(entryName)) { try { final boolean directory = isDirectory0(entryName); // detect false positives assert directory : "The root entry must be a directory!"; } catch (FalsePositiveException ex) { if (!(ex.getCause() instanceof FileNotFoundException)) throw ex; } throw new ArchiveEntryNotFoundException(entryName, "cannot write (potential) virtual root directory"); } else { autoUmount(entryName); final boolean lenient = File.isLenient(); final ArchiveFileSystem fileSystem = autoMount(lenient); in = append && fileSystem.isFile(entryName) ? createInputStream0(entryName) : null; // Start creating or overwriting the archive entry. // Note that this will fail if the entry already exists as a // directory. final Delta delta = fileSystem.link(entryName, lenient); // Create output stream. out = createOutputStream(delta.getEntry(), null); // Now link the entry into the file system. delta.commit(); } } finally { writeLock().unlock(); } if (in != null) { try { Streams.cat(in, out); } finally { in.close(); } } return out; } /** * Important: *
        *
      • This controller's write lock must be acquired. *
      • {@code entry} must not have received * {@link #hasNewData new data}. *
          */ abstract OutputStream createOutputStream( ArchiveEntry entry, ArchiveEntry srcEntry) throws IOException; // // File system operations used by the File class. // Read only operations: // final boolean exists(final String entryName) throws RfsEntryFalsePositiveException { try { return exists0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.exists(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean exists0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.exists(entryName); } finally { readLock().unlock(); } } final boolean isFile(final String entryName) throws RfsEntryFalsePositiveException { try { return isFile0(entryName); } catch (FileArchiveEntryFalsePositiveException ex) { // TODO: Document this! if (isRoot(entryName) && ex.getCause() instanceof FileNotFoundException) return false; return enclController.isFile(enclEntryName(entryName)); } catch (DirectoryArchiveEntryFalsePositiveException ex) { return enclController.isFile(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean isFile0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.isFile(entryName); } finally { readLock().unlock(); } } final boolean isDirectory(final String entryName) throws RfsEntryFalsePositiveException { try { return isDirectory0(entryName); } catch (FileArchiveEntryFalsePositiveException ex) { return false; } catch (DirectoryArchiveEntryFalsePositiveException ex) { return enclController.isDirectory(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean isDirectory0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.isDirectory(entryName); } finally { readLock().unlock(); } } final Icon getOpenIcon(final String entryName) throws RfsEntryFalsePositiveException { try { return getOpenIcon0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.getOpenIcon(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return null; } } private final Icon getOpenIcon0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); // detect false positives! return isRoot(entryName) ? getDriver().getOpenIcon(this) : fileSystem.getOpenIcon(entryName); } finally { readLock().unlock(); } } final Icon getClosedIcon(final String entryName) throws RfsEntryFalsePositiveException { try { return getClosedIcon0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.getClosedIcon(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return null; } } private final Icon getClosedIcon0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); // detect false positives! return isRoot(entryName) ? getDriver().getClosedIcon(this) : fileSystem.getClosedIcon(entryName); } finally { readLock().unlock(); } } final boolean canRead(final String entryName) throws RfsEntryFalsePositiveException { try { return canRead0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.canRead(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean canRead0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.exists(entryName); } finally { readLock().unlock(); } } final boolean canWrite(final String entryName) throws RfsEntryFalsePositiveException { try { return canWrite0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.canWrite(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean canWrite0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.canWrite(entryName); } finally { readLock().unlock(); } } final long length(final String entryName) throws RfsEntryFalsePositiveException { try { return length0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.length(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return 0; } } private final long length0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.length(entryName); } finally { readLock().unlock(); } } final long lastModified(final String entryName) throws RfsEntryFalsePositiveException { try { return lastModified0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.lastModified(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return 0; } } private final long lastModified0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.lastModified(entryName); } finally { readLock().unlock(); } } final String[] list(final String entryName) throws RfsEntryFalsePositiveException { try { return list0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.list(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return null; } } private final String[] list0(final String entryName) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.list(entryName); } finally { readLock().unlock(); } } final String[] list( final String entryName, final FilenameFilter filenameFilter, final File dir) throws RfsEntryFalsePositiveException { try { return list0(entryName, filenameFilter, dir); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.list(enclEntryName(entryName), filenameFilter, dir); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return null; } } private final String[] list0( final String entryName, final FilenameFilter filenameFilter, final File dir) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.list(entryName, filenameFilter, dir); } finally { readLock().unlock(); } } final File[] listFiles( final String entryName, final FilenameFilter filenameFilter, final File dir, final FileFactory factory) throws RfsEntryFalsePositiveException { try { return listFiles0(entryName, filenameFilter, dir, factory); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.listFiles(enclEntryName(entryName), filenameFilter, dir, factory); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return null; } } private final File[] listFiles0( final String entryName, final FilenameFilter filenameFilter, final File dir, final FileFactory factory) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.listFiles(entryName, filenameFilter, dir, factory); } finally { readLock().unlock(); } } final File[] listFiles( final String entryName, final FileFilter fileFilter, final File dir, final FileFactory factory) throws RfsEntryFalsePositiveException { try { return listFiles0(entryName, fileFilter, dir, factory); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.listFiles(enclEntryName(entryName), fileFilter, dir, factory); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return null; } } private final File[] listFiles0( final String entryName, final FileFilter fileFilter, final File dir, final FileFactory factory) throws IOException { readLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.listFiles(entryName, fileFilter, dir, factory); } finally { readLock().unlock(); } } // // File system operations used by the File class. // Write operations: // final boolean setReadOnly(final String entryName) throws RfsEntryFalsePositiveException { try { return setReadOnly0(entryName); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.setReadOnly(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean setReadOnly0(final String entryName) throws IOException { writeLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.setReadOnly(entryName); } finally { writeLock().unlock(); } } final boolean setLastModified( final String entryName, final long time) throws RfsEntryFalsePositiveException { try { return setLastModified0(entryName, time); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.setLastModified(enclEntryName(entryName), time); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final boolean setLastModified0( final String entryName, final long time) throws IOException { writeLock().lock(); try { autoUmount(entryName); final ArchiveFileSystem fileSystem = autoMount(false); return fileSystem.setLastModified(entryName, time); } finally { writeLock().unlock(); } } final boolean createNewFile( final String entryName, final boolean autoCreate) throws IOException { try { return createNewFile0(entryName, autoCreate); } catch (ArchiveEntryFalsePositiveException ex) { return enclController.createNewFile(enclEntryName(entryName), autoCreate); } } private final boolean createNewFile0( final String entryName, final boolean autoCreate) throws IOException { assert !isRoot(entryName); writeLock().lock(); try { final ArchiveFileSystem fileSystem = autoMount(autoCreate); if (fileSystem.exists(entryName)) return false; // If we got until here without an exception, // write an empty file now. createOutputStream0(entryName, false).close(); return true; } finally { writeLock().unlock(); } } final boolean mkdir( final String entryName, final boolean autoCreate) throws RfsEntryFalsePositiveException { try { mkdir0(entryName, autoCreate); return true; } catch (ArchiveEntryFalsePositiveException ex) { return enclController.mkdir(enclEntryName(entryName), autoCreate); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final void mkdir0(final String entryName, final boolean autoCreate) throws IOException { writeLock().lock(); try { if (isRoot(entryName)) { // This is the virtual root of an archive file system, so we // are actually working on the controller's target file. if (isRfsEntryTarget()) { if (target.exists()) throw new IOException("target file exists already!"); } else { if (enclController.exists(enclEntryName)) throw new IOException("target file exists already!"); } // Ensure file system existence. autoMount(true); } else { // !isRoot(entryName) // This file is a regular archive entry. final ArchiveFileSystem fileSystem = autoMount(autoCreate); fileSystem.mkdir(entryName, autoCreate); } } finally { writeLock().unlock(); } } final boolean delete(final String entryName) throws RfsEntryFalsePositiveException { try { delete0(entryName); return true; } catch (DirectoryArchiveEntryFalsePositiveException ex) { return enclController.delete(enclEntryName(entryName)); } catch (FileArchiveEntryFalsePositiveException ex) { // TODO: Document this! if (isRoot(entryName) && !enclController.isDirectory(enclEntryName(entryName)) && ex.getCause() instanceof FileNotFoundException) return false; return enclController.delete(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { return false; } } private final void delete0(final String entryName) throws IOException { writeLock().lock(); try { autoUmount(entryName); if (isRoot(entryName)) { // Get the file system or die trying! final ArchiveFileSystem fileSystem; try { fileSystem = autoMount(false); } catch (FalsePositiveException ex) { // The File instance is going to delete the target file // anyway, so we need to reset now. try { reset(); } catch (IOException cannotHappen) { throw new AssertionError(cannotHappen); } throw ex; } // We are actually working on the controller's target file. // Do not use the number of entries in the file system // for the following test - it's size would count absolute // pathnames as well! final String[] members = fileSystem.list(entryName); if (members != null && members.length != 0) throw new IOException("archive file system not empty!"); final int outputStreams = waitAllOutputStreamsByOtherThreads(50); // TODO: Review: This policy may be changed - see method start. assert outputStreams <= 0 : "Entries for open output streams should not be deletable!"; // Note: Entry for open input streams ARE deletable! final int inputStreams = waitAllInputStreamsByOtherThreads(50); if (inputStreams > 0 || outputStreams > 0) throw new IOException("archive file has open streams!"); reset(); // Just in case our target is an RAES encrypted ZIP file, // forget it's password as well. // TODO: Review: This is an archive driver dependency! // Calling it doesn't harm, but please consider a more opaque // way to model this. PromptingKeyManager.resetKeyProvider(getPath()); // Delete the target file or the entry in the enclosing // archive file, too. if (isRfsEntryTarget()) { // The target file of the controller is NOT enclosed // in another archive file. if (!target.delete()) throw new IOException("couldn't delete archive file!"); } else { // The target file of the controller IS enclosed in // another archive file. enclController.delete0(enclEntryName(entryName)); } } else { // !isRoot(entryName) final ArchiveFileSystem fileSystem = autoMount(false); fileSystem.delete(entryName); } } finally { writeLock().unlock(); } } /*final void deleteOnUmount(final String entryName) throws RfsEntryFalsePositiveException { try { deleteOnUmount0(entryName); } catch (DirectoryArchiveEntryFalsePositiveException ex) { enclController.deleteOnUmount(enclEntryName(entryName)); } catch (FileArchiveEntryFalsePositiveException ex) { // TODO: Document this! if (isRoot(entryName) && !enclController.isDirectory(enclEntryName(entryName)) && ex.getCause() instanceof FileNotFoundException) return; enclController.deleteOnUmount(enclEntryName(entryName)); } catch (RfsEntryFalsePositiveException ex) { throw ex; } catch (IOException ex) { throw new AssertionError(ex); } } private final void deleteOnUmount0(final String entryName) throws IOException { // FIXME: Delete entries. }*/ // // Exception classes. // Note that these are all inner classes, not just static member classes. // /** * Thrown if a controller's target file is a false positive archive file * which actually exists as a plain file or directory in the real file * system or in an enclosing archive file. *

          * Instances of this class are always associated with an * {@code IOException} as their cause. */ abstract class FalsePositiveException extends FileNotFoundException { private final boolean cacheable; /** * Creates a new {@code FalsePositiveException}. * * @param cause The cause for this exception. * If this is an instance of {@link TransientIOException}, * then its transient cause is unwrapped and used as the cause * of this exception instead and * {@link FalsePositiveException#isCacheable} is set to return * {@code false}. */ private FalsePositiveException(IOException cause) { // This exception type is never passed to the client application, // so a descriptive message would be waste of performance. //super(cause.toString()); assert cause != null; // A transient I/O exception is just a wrapper exception to mark // the real transient cause, therefore we can safely throw it away. // We must do this in order to allow the File class to inspect // the real transient cause and act accordingly. final boolean trans = cause instanceof TransientIOException; super.initCause(trans ? cause.getCause() : cause); cacheable = !trans; } /** * Returns the archive controller which has thrown this exception. * This is the controller which detected the false positive archive * file. */ ArchiveController getController() { return ArchiveController.this; } /** * Returns {@code true} if and only if there is no cause * associated with this exception or it is safe to cache it. */ boolean isCacheable() { return cacheable; } } // class FalsePositiveException /** * Thrown if a controller's target file is a false positive archive file * which actually exists as a plain file or directory in the real file * system. *

          * Instances of this class are always associated with an * {@code IOException} as their cause. */ final class RfsEntryFalsePositiveException extends FalsePositiveException { /** * Creates a new {@code RfsEntryFalsePositiveException}. * * @param cause The cause for this exception. * If this is an instance of {@link TransientIOException}, * then its transient cause is unwrapped and used as the cause * of this exception instead and * {@link FalsePositiveException#isCacheable} is set to return * {@code false}. */ RfsEntryFalsePositiveException(IOException cause) { super(cause); } } // class RfsEntryFalsePositiveException /** * Thrown if a controller's target file is a false positive archive file * which actually exists as a plain file or directory in an enclosing * archive file. *

          * Instances of this class are always associated with an * {@code IOException} as their cause. */ abstract class ArchiveEntryFalsePositiveException extends FalsePositiveException { private final ArchiveController enclController; private final String enclEntryName; /** * Creates a new {@code ArchiveEntryFalsePositiveException}. * * @param enclController The controller in which the archive file * exists as a false positive. * This must be an enclosing controller. * @param enclEntryName The entry name which is a false positive * archive file. * {@code null} is not permitted. * @param cause The cause for this exception. * If this is an instance of {@link TransientIOException}, * then its transient cause is unwrapped and used as the cause * of this exception instead and * {@link FalsePositiveException#isCacheable} is set to return * {@code false}. */ private ArchiveEntryFalsePositiveException( ArchiveController enclController, String enclEntryName, IOException cause) { super(cause); assert enclController != ArchiveController.this; assert isEnclosedBy(enclController); assert enclEntryName != null; this.enclController = enclController; this.enclEntryName = enclEntryName; } /** * Returns the controller which's target file contains the * false positive archive file as an archive entry. * Never {@code null}. *

          * Note that this is not the same */ ArchiveController getEnclController() { return enclController; } /** * Returns the entry name of the false positive archive file. * Never {@code null}. */ String getEnclEntryName() { return enclEntryName; } } // class ArchiveEntryFalsePositiveException /** * Thrown if a controller's target file is a false positive archive file * which actually exists as a plain file in an enclosing archive file. *

          * Instances of this class are always associated with an * {@code IOException} as their cause. */ final class FileArchiveEntryFalsePositiveException extends ArchiveEntryFalsePositiveException { /** * Creates a new {@code FileArchiveEntryFalsePositiveException}. * * @param enclController The controller in which the archive file * exists as a false positive. * This must be an enclosing controller. * @param enclEntryName The entry name which is a false positive * archive file. * {@code null} is not permitted. * @param cause The cause for this exception. * If this is an instance of {@link TransientIOException}, * then its transient cause is unwrapped and used as the cause * of this exception instead and * {@link FalsePositiveException#isCacheable} is set to return * {@code false}. */ FileArchiveEntryFalsePositiveException( ArchiveController enclController, String enclEntryName, IOException cause) { super(enclController, enclEntryName, cause); } } // class FileArchiveEntryFalsePositiveException /** * Thrown if a controller's target file is a false positive archive file * which actually exists as a plain directory in an enclosing archive file. *

          * Instances of this class are always associated with an * {@code IOException} as their cause. */ final class DirectoryArchiveEntryFalsePositiveException extends ArchiveEntryFalsePositiveException { /** * Creates a new {@code DirectoryArchiveEntryFalsePositiveException}. * * @param enclController The controller in which the archive file * exists as a false positive. * This must be an enclosing controller. * @param enclEntryName The entry name which is a false positive * archive file. * {@code null} is not permitted. * @param cause The cause for this exception. * If this is an instance of {@link TransientIOException}, * then its transient cause is unwrapped and used as the cause * of this exception instead and * {@link FalsePositiveException#isCacheable} is set to return * {@code false}. */ DirectoryArchiveEntryFalsePositiveException( ArchiveController enclController, String enclEntryName, IOException cause) { super(enclController, enclEntryName, cause); } } // class DirectoryArchiveEntryFalsePositiveException /** * Thrown if a controller's target file does not exist or is not * accessible. * May be thrown by {@link #autoMount(boolean)} if automatic creation of * the target file is not allowed. */ final class ArchiveFileNotFoundException extends FileNotFoundException { ArchiveFileNotFoundException(String msg) { super(msg); } public String getMessage() { String msg = super.getMessage(); if (msg != null) return getPath() + " (" + msg + ")"; else return getPath(); } } // class ArchiveFileNotFoundException /** * Thrown if an archive entry does not exist * or is not accessible. * May be thrown by {@link #createInputStream} or * {@link #createOutputStream}. */ final class ArchiveEntryNotFoundException extends FileNotFoundException { private final String entryName; ArchiveEntryNotFoundException(final String entryName, final String msg) { super(msg); assert entryName != null; assert msg != null; this.entryName = entryName; } public String getMessage() { String path = getPath(); if (!isRoot(entryName)) path += File.separator + entryName.replace(SEPARATOR_CHAR, File.separatorChar); String msg = super.getMessage(); if (msg != null) path += " (" + msg + ")"; return path; } } // class ArchiveEntryNotFoundException }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy