Java tutorial
/* * 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); } }