com.google.jimfs.FileSystemView.java Source code

Java tutorial

Introduction

Here is the source code for com.google.jimfs.FileSystemView.java

Source

/*
 * Copyright 2013 Google Inc.
 *
 * 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 com.google.jimfs;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;

import com.google.common.base.Objects;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;

import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.SecureDirectoryStream;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.Lock;

import javax.annotation.Nullable;

/**
 * View of a file system with a specific working directory. As all file system operations need to
 * work when given either relative or absolute paths, this class contains the implementation of most
 * file system operations, with relative path operations resolving against the working directory.
 *
 * <p>A file system has one default view using the file system's working directory. Additional views
 * may be created for use in {@link SecureDirectoryStream} instances, which each have a different
 * working directory they use.
 *
 * @author Colin Decker
 */
final class FileSystemView {

    private final JimfsFileStore store;

    private final Directory workingDirectory;
    private final JimfsPath workingDirectoryPath;

    /**
     * Creates a new file system view.
     */
    public FileSystemView(JimfsFileStore store, Directory workingDirectory, JimfsPath workingDirectoryPath) {
        this.store = checkNotNull(store);
        this.workingDirectory = checkNotNull(workingDirectory);
        this.workingDirectoryPath = checkNotNull(workingDirectoryPath);
    }

    /**
     * Returns whether or not this view and the given view belong to the same file system.
     */
    private boolean isSameFileSystem(FileSystemView other) {
        return store == other.store;
    }

    /**
     * Returns the path of the working directory at the time this view was created. Does not reflect
     * changes to the path caused by the directory being moved.
     */
    public JimfsPath getWorkingDirectoryPath() {
        return workingDirectoryPath;
    }

    /**
     * Attempt to look up the file at the given path.
     */
    DirectoryEntry lookUpWithLock(JimfsPath path, Set<? super LinkOption> options) throws IOException {
        store.readLock().lock();
        try {
            return lookUp(path, options);
        } finally {
            store.readLock().unlock();
        }
    }

    /**
     * Looks up the file at the given path without locking.
     */
    private DirectoryEntry lookUp(JimfsPath path, Set<? super LinkOption> options) throws IOException {
        return store.lookUp(workingDirectory, path, options);
    }

    /**
     * Creates a new directory stream for the directory located by the given path. The given
     * {@code basePathForStream} is that base path that the returned stream will use. This will be
     * the same as {@code dir} except for streams created relative to another secure stream.
     */
    public DirectoryStream<Path> newDirectoryStream(JimfsPath dir, DirectoryStream.Filter<? super Path> filter,
            Set<? super LinkOption> options, JimfsPath basePathForStream) throws IOException {
        Directory file = (Directory) lookUpWithLock(dir, options).requireDirectory(dir).file();
        FileSystemView view = new FileSystemView(store, file, basePathForStream);
        JimfsSecureDirectoryStream stream = new JimfsSecureDirectoryStream(view, filter);
        return store.supportsFeature(Feature.SECURE_DIRECTORY_STREAM) ? stream
                : new DowngradedDirectoryStream(stream);
    }

    /**
     * Snapshots the entries of the working directory of this view.
     */
    public ImmutableSortedSet<Name> snapshotWorkingDirectoryEntries() {
        store.readLock().lock();
        try {
            ImmutableSortedSet<Name> names = workingDirectory.snapshot();
            workingDirectory.updateAccessTime();
            return names;
        } finally {
            store.readLock().unlock();
        }
    }

    /**
     * Returns a snapshot mapping the names of each file in the directory at the given path to the
     * last modified time of that file.
     */
    public ImmutableMap<Name, Long> snapshotModifiedTimes(JimfsPath path) throws IOException {
        ImmutableMap.Builder<Name, Long> modifiedTimes = ImmutableMap.builder();

        store.readLock().lock();
        try {
            Directory dir = (Directory) lookUp(path, Options.FOLLOW_LINKS).requireDirectory(path).file();
            // TODO(cgdecker): Investigate whether WatchServices should keep a reference to the actual
            // directory when SecureDirectoryStream is supported rather than looking up the directory
            // each time the WatchService polls

            for (DirectoryEntry entry : dir) {
                if (!entry.name().equals(Name.SELF) && !entry.name().equals(Name.PARENT)) {
                    modifiedTimes.put(entry.name(), entry.file().getLastModifiedTime());
                }
            }

            return modifiedTimes.build();
        } finally {
            store.readLock().unlock();
        }
    }

    /**
     * Returns whether or not the two given paths locate the same file. The second path is located
     * using the given view rather than this file view.
     */
    public boolean isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2) throws IOException {
        if (!isSameFileSystem(view2)) {
            return false;
        }

        store.readLock().lock();
        try {
            File file = lookUp(path, Options.FOLLOW_LINKS).fileOrNull();
            File file2 = view2.lookUp(path2, Options.FOLLOW_LINKS).fileOrNull();
            return file != null && Objects.equal(file, file2);
        } finally {
            store.readLock().unlock();
        }
    }

    /**
     * Gets the {@linkplain Path#toRealPath(LinkOption...) real path} to the file located by the
     * given path.
     */
    public JimfsPath toRealPath(JimfsPath path, PathService pathService, Set<? super LinkOption> options)
            throws IOException {
        checkNotNull(path);
        checkNotNull(options);

        store.readLock().lock();
        try {
            DirectoryEntry entry = lookUp(path, options).requireExists(path);

            List<Name> names = new ArrayList<>();
            names.add(entry.name());
            while (!entry.file().isRootDirectory()) {
                entry = entry.directory().entryInParent();
                names.add(entry.name());
            }

            // names are ordered last to first in the list, so get the reverse view
            List<Name> reversed = Lists.reverse(names);
            Name root = reversed.remove(0);
            return pathService.createPath(root, reversed);
        } finally {
            store.readLock().unlock();
        }
    }

    /**
     * Creates a new directory at the given path. The given attributes will be set on the new file if
     * possible.
     */
    public Directory createDirectory(JimfsPath path, FileAttribute<?>... attrs) throws IOException {
        return (Directory) createFile(path, store.directoryCreator(), true, attrs);
    }

    /**
     * Creates a new symbolic link at the given path with the given target. The given attributes will
     * be set on the new file if possible.
     */
    public SymbolicLink createSymbolicLink(JimfsPath path, JimfsPath target, FileAttribute<?>... attrs)
            throws IOException {
        if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) {
            throw new UnsupportedOperationException();
        }
        return (SymbolicLink) createFile(path, store.symbolicLinkCreator(target), true, attrs);
    }

    /**
     * Creates a new file at the given path if possible, using the given supplier to create the file.
     * Returns the new file. If {@code allowExisting} is {@code true} and a file already exists at
     * the given path, returns that file. Otherwise, throws {@link FileAlreadyExistsException}.
     */
    private File createFile(JimfsPath path, Supplier<? extends File> fileCreator, boolean failIfExists,
            FileAttribute<?>... attrs) throws IOException {
        checkNotNull(path);
        checkNotNull(fileCreator);

        store.writeLock().lock();
        try {
            DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS);

            if (entry.exists()) {
                if (failIfExists) {
                    throw new FileAlreadyExistsException(path.toString());
                }

                // currently can only happen if getOrCreateFile doesn't find the file with the read lock
                // and then the file is created between when it releases the read lock and when it
                // acquires the write lock; so, very unlikely
                return entry.file();
            }

            Directory parent = entry.directory();

            File newFile = fileCreator.get();
            store.setInitialAttributes(newFile, attrs);
            parent.link(path.name(), newFile);
            parent.updateModifiedTime();
            return newFile;
        } finally {
            store.writeLock().unlock();
        }
    }

    /**
     * Gets the regular file at the given path, creating it if it doesn't exist and the given options
     * specify that it should be created.
     */
    public RegularFile getOrCreateRegularFile(JimfsPath path, Set<OpenOption> options, FileAttribute<?>... attrs)
            throws IOException {
        checkNotNull(path);

        if (!options.contains(CREATE_NEW)) {
            // assume file exists unless we're explicitly trying to create a new file
            RegularFile file = lookUpRegularFile(path, options);
            if (file != null) {
                return file;
            }
        }

        if (options.contains(CREATE) || options.contains(CREATE_NEW)) {
            return getOrCreateRegularFileWithWriteLock(path, options, attrs);
        } else {
            throw new NoSuchFileException(path.toString());
        }
    }

    /**
     * Looks up the regular file at the given path, throwing an exception if the file isn't a regular
     * file. Returns null if the file did not exist.
     */
    @Nullable
    private RegularFile lookUpRegularFile(JimfsPath path, Set<OpenOption> options) throws IOException {
        store.readLock().lock();
        try {
            DirectoryEntry entry = lookUp(path, options);
            if (entry.exists()) {
                File file = entry.file();
                if (!file.isRegularFile()) {
                    throw new FileSystemException(path.toString(), null, "not a regular file");
                }
                return open((RegularFile) file, options);
            } else {
                return null;
            }
        } finally {
            store.readLock().unlock();
        }
    }

    /**
     * Gets or creates a new regular file with a write lock (assuming the file does not exist).
     */
    private RegularFile getOrCreateRegularFileWithWriteLock(JimfsPath path, Set<OpenOption> options,
            FileAttribute<?>[] attrs) throws IOException {
        store.writeLock().lock();
        try {
            File file = createFile(path, store.regularFileCreator(), options.contains(CREATE_NEW), attrs);
            // the file already existed but was not a regular file
            if (!file.isRegularFile()) {
                throw new FileSystemException(path.toString(), null, "not a regular file");
            }
            return open((RegularFile) file, options);
        } finally {
            store.writeLock().unlock();
        }
    }

    /**
     * Opens the given regular file with the given options, truncating it if necessary and
     * incrementing its open count. Returns the given file.
     */
    private static RegularFile open(RegularFile file, Set<OpenOption> options) {
        if (options.contains(TRUNCATE_EXISTING) && options.contains(WRITE)) {
            file.writeLock().lock();
            try {
                file.truncate(0);
            } finally {
                file.writeLock().unlock();
            }
        }

        // must be opened while holding a file store lock to ensure no race between opening and
        // deleting the file
        file.opened();

        return file;
    }

    /**
     * Returns the target of the symbolic link at the given path.
     */
    public JimfsPath readSymbolicLink(JimfsPath path) throws IOException {
        if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) {
            throw new UnsupportedOperationException();
        }

        SymbolicLink symbolicLink = (SymbolicLink) lookUpWithLock(path, Options.NOFOLLOW_LINKS)
                .requireSymbolicLink(path).file();

        return symbolicLink.target();
    }

    /**
     * Checks access to the file at the given path for the given modes. Since access controls are not
     * implemented for this file system, this just checks that the file exists.
     */
    public void checkAccess(JimfsPath path) throws IOException {
        // just check that the file exists
        lookUpWithLock(path, Options.FOLLOW_LINKS).requireExists(path);
    }

    /**
     * Creates a hard link at the given link path to the regular file at the given path. The existing
     * file must exist and must be a regular file. The given file system view must belong to the same
     * file system as this view.
     */
    public void link(JimfsPath link, FileSystemView existingView, JimfsPath existing) throws IOException {
        checkNotNull(link);
        checkNotNull(existingView);
        checkNotNull(existing);

        if (!store.supportsFeature(Feature.LINKS)) {
            throw new UnsupportedOperationException();
        }

        if (!isSameFileSystem(existingView)) {
            throw new FileSystemException(link.toString(), existing.toString(),
                    "can't link: source and target are in different file system instances");
        }

        Name linkName = link.name();

        // existingView is in the same file system, so just one lock is needed
        store.writeLock().lock();
        try {
            // we do want to follow links when finding the existing file
            File existingFile = existingView.lookUp(existing, Options.FOLLOW_LINKS).requireExists(existing).file();
            if (!existingFile.isRegularFile()) {
                throw new FileSystemException(link.toString(), existing.toString(),
                        "can't link: not a regular file");
            }

            Directory linkParent = lookUp(link, Options.NOFOLLOW_LINKS).requireDoesNotExist(link).directory();

            linkParent.link(linkName, existingFile);
            linkParent.updateModifiedTime();
        } finally {
            store.writeLock().unlock();
        }
    }

    /**
     * Deletes the file at the given absolute path.
     */
    public void deleteFile(JimfsPath path, DeleteMode deleteMode) throws IOException {
        store.writeLock().lock();
        try {
            DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS).requireExists(path);
            delete(entry, deleteMode, path);
        } finally {
            store.writeLock().unlock();
        }
    }

    /**
     * Deletes the given directory entry from its parent directory.
     */
    private void delete(DirectoryEntry entry, DeleteMode deleteMode, JimfsPath pathForException)
            throws IOException {
        Directory parent = entry.directory();
        File file = entry.file();

        checkDeletable(file, deleteMode, pathForException);
        parent.unlink(entry.name());
        parent.updateModifiedTime();

        file.deleted();
    }

    /**
     * Mode for deleting. Determines what types of files can be deleted.
     */
    public enum DeleteMode {
        /**
         * Delete any file.
         */
        ANY,
        /**
         * Only delete non-directory files.
         */
        NON_DIRECTORY_ONLY,
        /**
         * Only delete directory files.
         */
        DIRECTORY_ONLY
    }

    /**
     * Checks that the given file can be deleted, throwing an exception if it can't.
     */
    private void checkDeletable(File file, DeleteMode mode, Path path) throws IOException {
        if (file.isRootDirectory()) {
            throw new FileSystemException(path.toString(), null, "can't delete root directory");
        }

        if (file.isDirectory()) {
            if (mode == DeleteMode.NON_DIRECTORY_ONLY) {
                throw new FileSystemException(path.toString(), null, "can't delete: is a directory");
            }

            checkEmpty(((Directory) file), path);
        } else if (mode == DeleteMode.DIRECTORY_ONLY) {
            throw new FileSystemException(path.toString(), null, "can't delete: is not a directory");
        }

        if (file == workingDirectory && !path.isAbsolute()) {
            // this is weird, but on Unix at least, the file system seems to be happy to delete the
            // working directory if you give the absolute path to it but fail if you use a relative path
            // that resolves to the working directory (e.g. "" or ".")
            throw new FileSystemException(path.toString(), null, "invalid argument");
        }
    }

    /**
     * Checks that given directory is empty, throwing {@link DirectoryNotEmptyException} if not.
     */
    private void checkEmpty(Directory dir, Path pathForException) throws FileSystemException {
        if (!dir.isEmpty()) {
            throw new DirectoryNotEmptyException(pathForException.toString());
        }
    }

    /**
     * Copies or moves the file at the given source path to the given dest path.
     */
    public void copy(JimfsPath source, FileSystemView destView, JimfsPath dest, Set<CopyOption> options,
            boolean move) throws IOException {
        checkNotNull(source);
        checkNotNull(destView);
        checkNotNull(dest);
        checkNotNull(options);

        boolean sameFileSystem = isSameFileSystem(destView);

        lockBoth(store.writeLock(), destView.store.writeLock());
        try {
            DirectoryEntry sourceEntry = lookUp(source, options).requireExists(source);
            DirectoryEntry destEntry = destView.lookUp(dest, Options.NOFOLLOW_LINKS);

            Directory sourceParent = sourceEntry.directory();
            File sourceFile = sourceEntry.file();

            Directory destParent = destEntry.directory();

            if (move && sourceFile.isDirectory()) {
                if (sameFileSystem) {
                    checkMovable(sourceFile, source);
                    checkNotAncestor(sourceFile, destParent, destView);
                } else {
                    // move to another file system is accomplished by copy-then-delete, so the source file
                    // must be deletable to be moved
                    checkDeletable(sourceFile, DeleteMode.ANY, source);
                }
            }

            if (destEntry.exists()) {
                if (destEntry.file().equals(sourceFile)) {
                    return;
                } else if (options.contains(REPLACE_EXISTING)) {
                    destView.delete(destEntry, DeleteMode.ANY, dest);
                } else {
                    throw new FileAlreadyExistsException(dest.toString());
                }
            }

            // can only do an actual move within one file system instance
            // otherwise we have to copy and delete
            if (move && sameFileSystem) {
                sourceParent.unlink(source.name());
                sourceParent.updateModifiedTime();

                destParent.link(dest.name(), sourceFile);
                destParent.updateModifiedTime();
            } else {
                // copy
                boolean copyAttributes = options.contains(COPY_ATTRIBUTES) && !move;
                File copy = destView.store.copy(sourceFile, copyAttributes);
                destParent.link(dest.name(), copy);
                destParent.updateModifiedTime();

                if (move) {
                    store.copyBasicAttributes(sourceFile, copy);
                    delete(sourceEntry, DeleteMode.ANY, source);
                }
            }
        } finally {
            destView.store.writeLock().unlock();
            store.writeLock().unlock();
        }
    }

    private void checkMovable(File file, JimfsPath path) throws FileSystemException {
        if (file.isRootDirectory()) {
            throw new FileSystemException(path.toString(), null, "can't move root directory");
        }
    }

    /**
     * Acquires both write locks in a way that attempts to avoid the possibility of deadlock. Note
     * that typically (when only one file system instance is involved), both locks will be the same
     * lock and there will be no issue at all.
     */
    private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) {
        while (true) {
            sourceWriteLock.lock();
            if (destWriteLock.tryLock()) {
                return;
            } else {
                sourceWriteLock.unlock();
            }

            destWriteLock.lock();
            if (sourceWriteLock.tryLock()) {
                return;
            } else {
                destWriteLock.unlock();
            }
        }
    }

    /**
     * Checks that source is not an ancestor of dest, throwing an exception if it is.
     */
    private void checkNotAncestor(File source, Directory destParent, FileSystemView destView) throws IOException {
        // if dest is not in the same file system, it couldn't be in source's subdirectories
        if (!isSameFileSystem(destView)) {
            return;
        }

        Directory current = destParent;
        while (true) {
            if (current.equals(source)) {
                throw new IOException("invalid argument: can't move directory into a subdirectory of itself");
            }

            if (current.isRootDirectory()) {
                return;
            } else {
                current = current.parent();
            }
        }
    }

    /**
     * Returns a file attribute view using the given lookup callback.
     */
    @Nullable
    public <V extends FileAttributeView> V getFileAttributeView(FileLookup lookup, Class<V> type) {
        return store.getFileAttributeView(lookup, type);
    }

    /**
     * Returns a file attribute view for the given path in this view.
     */
    @Nullable
    public <V extends FileAttributeView> V getFileAttributeView(final JimfsPath path, Class<V> type,
            final Set<? super LinkOption> options) {
        return store.getFileAttributeView(new FileLookup() {
            @Override
            public File lookup() throws IOException {
                return lookUpWithLock(path, options).requireExists(path).file();
            }
        }, type);
    }

    /**
     * Reads attributes of the file located by the given path in this view as an object.
     */
    public <A extends BasicFileAttributes> A readAttributes(JimfsPath path, Class<A> type,
            Set<? super LinkOption> options) throws IOException {
        File file = lookUpWithLock(path, options).requireExists(path).file();
        return store.readAttributes(file, type);
    }

    /**
     * Reads attributes of the file located by the given path in this view as a map.
     */
    public ImmutableMap<String, Object> readAttributes(JimfsPath path, String attributes,
            Set<? super LinkOption> options) throws IOException {
        File file = lookUpWithLock(path, options).requireExists(path).file();
        return store.readAttributes(file, attributes);
    }

    /**
     * Sets the given attribute to the given value on the file located by the given path in this
     * view.
     */
    public void setAttribute(JimfsPath path, String attribute, Object value, Set<? super LinkOption> options)
            throws IOException {
        File file = lookUpWithLock(path, options).requireExists(path).file();
        store.setAttribute(file, attribute, value);
    }
}