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

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

/*
 * 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 - 2025 Weber Informatics LLC | Privacy Policy