diff --git a/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java index 2a43fb99..14a580d3 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java +++ b/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java @@ -30,21 +30,60 @@ * BasicFileAttributeView} ("basic" or no view prefix), and allows the reading of {@link * BasicFileAttributes}. * - * @author Colin Decker + *

This provider offers the following attributes: + *

+ * + *

It allows the setting of creationTime, lastAccessTime, and lastModifiedTime. + * All other attributes here are read-only. + * + *

Implementation note: This class also provides the {@link BasicFileAttributeView} + * and returns {@link BasicFileAttributes} instances for the given file. + * + *

Any usage beyond reading and writing these basic attributes falls outside + * the scope of this provider. + * + *

All method parameters are non-null unless specified otherwise. + * + *

This class is thread-safe if the underlying file data is thread-safe. + * + *

Note: We have extracted a helper method for the {@code isOther} logic, introduced + * constants for repeated attribute names, and introduced an explaining variable + * to address code duplication and improve clarity. */ final class BasicAttributeProvider extends AttributeProvider { + // Introduce constants for repeated attribute names (extract/rename technique). + private static final String SIZE = "size"; + private static final String FILE_KEY = "fileKey"; + private static final String IS_DIRECTORY = "isDirectory"; + private static final String IS_REGULAR_FILE = "isRegularFile"; + private static final String IS_SYMBOLIC_LINK = "isSymbolicLink"; + private static final String IS_OTHER = "isOther"; + private static final String CREATION_TIME = "creationTime"; + private static final String LAST_ACCESS_TIME = "lastAccessTime"; + private static final String LAST_MODIFIED_TIME = "lastModifiedTime"; + private static final ImmutableSet ATTRIBUTES = - ImmutableSet.of( - "size", - "fileKey", - "isDirectory", - "isRegularFile", - "isSymbolicLink", - "isOther", - "creationTime", - "lastAccessTime", - "lastModifiedTime"); + ImmutableSet.of( + SIZE, + FILE_KEY, + IS_DIRECTORY, + IS_REGULAR_FILE, + IS_SYMBOLIC_LINK, + IS_OTHER, + CREATION_TIME, + LAST_ACCESS_TIME, + LAST_MODIFIED_TIME); @Override public String name() { @@ -59,52 +98,60 @@ public ImmutableSet fixedAttributes() { @Override public @Nullable Object get(File file, String attribute) { switch (attribute) { - case "size": + case SIZE: return file.size(); - case "fileKey": + case FILE_KEY: return file.id(); - case "isDirectory": + case IS_DIRECTORY: return file.isDirectory(); - case "isRegularFile": + case IS_REGULAR_FILE: return file.isRegularFile(); - case "isSymbolicLink": + case IS_SYMBOLIC_LINK: return file.isSymbolicLink(); - case "isOther": - return !file.isDirectory() && !file.isRegularFile() && !file.isSymbolicLink(); - case "creationTime": + case IS_OTHER: + // Introduce explaining variable and extract method to decompose conditional + boolean isOtherResult = computeIsOther(file); + return isOtherResult; + case CREATION_TIME: return file.getCreationTime(); - case "lastAccessTime": + case LAST_ACCESS_TIME: return file.getLastAccessTime(); - case "lastModifiedTime": + case LAST_MODIFIED_TIME: return file.getLastModifiedTime(); default: return null; } } + // Extracted helper method for "isOther" logic. + private static boolean computeIsOther(File file) { + return !file.isDirectory() && !file.isRegularFile() && !file.isSymbolicLink(); + } + @Override public void set(File file, String view, String attribute, Object value, boolean create) { switch (attribute) { - case "creationTime": + case CREATION_TIME: checkNotCreate(view, attribute, create); file.setCreationTime(checkType(view, attribute, value, FileTime.class)); break; - case "lastAccessTime": + case LAST_ACCESS_TIME: checkNotCreate(view, attribute, create); file.setLastAccessTime(checkType(view, attribute, value, FileTime.class)); break; - case "lastModifiedTime": + case LAST_MODIFIED_TIME: checkNotCreate(view, attribute, create); file.setLastModifiedTime(checkType(view, attribute, value, FileTime.class)); break; - case "size": - case "fileKey": - case "isDirectory": - case "isRegularFile": - case "isSymbolicLink": - case "isOther": + case SIZE: + case FILE_KEY: + case IS_DIRECTORY: + case IS_REGULAR_FILE: + case IS_SYMBOLIC_LINK: + case IS_OTHER: throw unsettable(view, attribute, create); default: + // no-op } } @@ -115,7 +162,7 @@ public Class viewType() { @Override public BasicFileAttributeView view( - FileLookup lookup, ImmutableMap inheritedViews) { + FileLookup lookup, ImmutableMap inheritedViews) { return new View(lookup); } @@ -148,10 +195,10 @@ public BasicFileAttributes readAttributes() throws IOException { @Override public void setTimes( - @Nullable FileTime lastModifiedTime, - @Nullable FileTime lastAccessTime, - @Nullable FileTime createTime) - throws IOException { + @Nullable FileTime lastModifiedTime, + @Nullable FileTime lastAccessTime, + @Nullable FileTime createTime) + throws IOException { File file = lookupFile(); if (lastModifiedTime != null) { diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java b/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java index 3f75519e..7897855b 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java +++ b/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java @@ -61,18 +61,20 @@ * may be created for use in {@link SecureDirectoryStream} instances, which each have a different * working directory they use. * - * @author Colin Decker + *

Implementation details regarding file creation, linking, moving, copying, and deletion are + * contained in this class, while path resolution is handled by the underlying store. + * + *

Thread-safety depends on the underlying file system store locks. */ 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) { + JimfsFileStore store, Directory workingDirectory, JimfsPath workingDirectoryPath) { this.store = checkNotNull(store); this.workingDirectory = checkNotNull(workingDirectory); this.workingDirectoryPath = checkNotNull(workingDirectoryPath); @@ -102,7 +104,7 @@ public JimfsPath getWorkingDirectoryPath() { /** Attempt to look up the file at the given path. */ DirectoryEntry lookUpWithLock(JimfsPath path, Set options) - throws IOException { + throws IOException { store.readLock().lock(); try { return lookUp(path, options); @@ -113,7 +115,7 @@ DirectoryEntry lookUpWithLock(JimfsPath path, Set options) /** Looks up the file at the given path without locking. */ private DirectoryEntry lookUp(JimfsPath path, Set options) - throws IOException { + throws IOException { return store.lookUp(workingDirectory, path, options); } @@ -123,17 +125,17 @@ private DirectoryEntry lookUp(JimfsPath path, Set options) * as {@code dir} except for streams created relative to another secure stream. */ public DirectoryStream newDirectoryStream( - JimfsPath dir, - DirectoryStream.Filter filter, - Set options, - JimfsPath basePathForStream) - throws IOException { + JimfsPath dir, + DirectoryStream.Filter filter, + Set 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, state()); return store.supportsFeature(Feature.SECURE_DIRECTORY_STREAM) - ? stream - : new DowngradedDirectoryStream(stream); + ? stream + : new DowngradedDirectoryStream(stream); } /** Snapshots the entries of the working directory of this view. */ @@ -154,20 +156,14 @@ public ImmutableSortedSet snapshotWorkingDirectoryEntries() { */ public ImmutableMap snapshotModifiedTimes(JimfsPath path) throws IOException { ImmutableMap.Builder 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(); @@ -179,11 +175,10 @@ public ImmutableMap snapshotModifiedTimes(JimfsPath path) throws * using the given view rather than this file view. */ public boolean isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2) - throws IOException { + throws IOException { if (!isSameFileSystem(view2)) { return false; } - store.readLock().lock(); try { File file = lookUp(path, Options.FOLLOW_LINKS).fileOrNull(); @@ -199,22 +194,18 @@ public boolean isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2) * path. */ public JimfsPath toRealPath( - JimfsPath path, PathService pathService, Set options) throws IOException { + JimfsPath path, PathService pathService, Set options) throws IOException { checkNotNull(path); checkNotNull(options); - store.readLock().lock(); try { DirectoryEntry entry = lookUp(path, options).requireExists(path); - List 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 reversed = Lists.reverse(names); Name root = reversed.remove(0); return pathService.createPath(root, reversed); @@ -238,7 +229,7 @@ public Directory createDirectory(JimfsPath path, FileAttribute... attrs) thro */ @CanIgnoreReturnValue public SymbolicLink createSymbolicLink( - JimfsPath path, JimfsPath target, FileAttribute... attrs) throws IOException { + JimfsPath path, JimfsPath target, FileAttribute... attrs) throws IOException { if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) { throw new UnsupportedOperationException(); } @@ -251,31 +242,23 @@ public SymbolicLink createSymbolicLink( * given path, returns that file. Otherwise, throws {@link FileAlreadyExistsException}. */ private File createFile( - JimfsPath path, - Supplier fileCreator, - boolean failIfExists, - FileAttribute... attrs) - throws IOException { + JimfsPath path, + Supplier 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); @@ -291,17 +274,14 @@ private File createFile( * specify that it should be created. */ public RegularFile getOrCreateRegularFile( - JimfsPath path, Set options, FileAttribute... attrs) throws IOException { + JimfsPath path, Set 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 { @@ -314,7 +294,7 @@ public RegularFile getOrCreateRegularFile( * file. Returns null if the file did not exist. */ private @Nullable RegularFile lookUpRegularFile(JimfsPath path, Set options) - throws IOException { + throws IOException { store.readLock().lock(); try { DirectoryEntry entry = lookUp(path, options); @@ -334,11 +314,10 @@ public RegularFile getOrCreateRegularFile( /** Gets or creates a new regular file with a write lock (assuming the file does not exist). */ private RegularFile getOrCreateRegularFileWithWriteLock( - JimfsPath path, Set options, FileAttribute[] attrs) throws IOException { + JimfsPath path, Set 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"); } @@ -361,11 +340,7 @@ private static RegularFile open(RegularFile file, Set options) { 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; } @@ -374,11 +349,9 @@ 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(); - + (SymbolicLink) + lookUpWithLock(path, Options.NOFOLLOW_LINKS).requireSymbolicLink(path).file(); return symbolicLink.target(); } @@ -387,7 +360,6 @@ public JimfsPath readSymbolicLink(JimfsPath path) throws IOException { * 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); } @@ -397,38 +369,29 @@ public void checkAccess(JimfsPath path) throws IOException { * file system as this view. */ public void link(JimfsPath link, FileSystemView existingView, JimfsPath existing) - throws IOException { + 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"); + 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(); + 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"); + throw new FileSystemException(link.toString(), existing.toString(), "can't link: not a regular file"); } - Directory linkParent = - lookUp(link, Options.NOFOLLOW_LINKS).requireDoesNotExist(link).directory(); - + lookUp(link, Options.NOFOLLOW_LINKS).requireDoesNotExist(link).directory(); linkParent.link(linkName, existingFile); linkParent.setLastModifiedTime(now()); } finally { @@ -447,77 +410,62 @@ public void deleteFile(JimfsPath path, DeleteMode deleteMode) throws IOException } } - /** Deletes the given directory entry from its parent directory. */ + /** + * Deletes the given directory entry from its parent directory. + */ private void delete(DirectoryEntry entry, DeleteMode deleteMode, JimfsPath pathForException) - throws IOException { + throws IOException { Directory parent = entry.directory(); File file = entry.file(); - checkDeletable(file, deleteMode, pathForException); parent.unlink(entry.name()); parent.setLastModifiedTime(now()); - 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. */ + /** + * Checks that the given file can be deleted, throwing an exception if it can't. + * + *

Refactored to delegate mode-specific deletion checks to DeleteMode. + */ 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"); } + mode.checkDeletable(file, path, this); } - /** Checks that given directory is empty, throwing {@link DirectoryNotEmptyException} if not. */ + /** + * Checks that the 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. */ + /** + * 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 options, - boolean move) - throws IOException { + JimfsPath source, + FileSystemView destView, + JimfsPath dest, + Set options, + boolean move) + throws IOException { checkNotNull(source); checkNotNull(destView); checkNotNull(dest); checkNotNull(options); boolean sameFileSystem = isSameFileSystem(destView); - File sourceFile; - File copyFile = null; // non-null after block completes iff source file was copied + File copyFile = null; + lockBoth(store.writeLock(), destView.store.writeLock()); try { DirectoryEntry sourceEntry = lookUp(source, options).requireExists(source); @@ -525,7 +473,6 @@ public void copy( Directory sourceParent = sourceEntry.directory(); sourceFile = sourceEntry.file(); - Directory destParent = destEntry.directory(); if (move && sourceFile.isDirectory()) { @@ -533,8 +480,6 @@ public void copy( 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); } } @@ -550,46 +495,25 @@ public void copy( } if (move && sameFileSystem) { - // Real move on the same file system. sourceParent.unlink(source.name()); sourceParent.setLastModifiedTime(now()); - destParent.link(dest.name(), sourceFile); destParent.setLastModifiedTime(now()); } else { - // Doing a copy OR a move to a different file system, which must be implemented by copy and - // delete. - - // By default, don't copy attributes. AttributeCopyOption attributeCopyOption = AttributeCopyOption.NONE; if (move) { - // Copy only the basic attributes of the file to the other file system, as it may not - // support all the attribute views that this file system does. This also matches the - // behavior of moving a file to a foreign file system with a different - // FileSystemProvider. attributeCopyOption = AttributeCopyOption.BASIC; } else if (options.contains(COPY_ATTRIBUTES)) { - // As with move, if we're copying the file to a different file system, only copy its - // basic attributes. attributeCopyOption = - sameFileSystem ? AttributeCopyOption.ALL : AttributeCopyOption.BASIC; + sameFileSystem ? AttributeCopyOption.ALL : AttributeCopyOption.BASIC; } - // Copy the file, but don't copy its content while we're holding the file store locks. copyFile = destView.store.copyWithoutContent(sourceFile, attributeCopyOption); destParent.link(dest.name(), copyFile); destParent.setLastModifiedTime(now()); - - // In order for the copy to be atomic (not strictly necessary, but seems preferable since - // we can) lock both source and copy files before leaving the file store locks. This - // ensures that users cannot observe the copy's content until the content has been copied. - // This also marks the source file as opened, preventing its content from being deleted - // until after it's copied if the source file itself is deleted in the next step. lockSourceAndCopy(sourceFile, copyFile); if (move) { - // It should not be possible for delete to throw an exception here, because we already - // checked that the file was deletable above. delete(sourceEntry, DeleteMode.ANY, source); } } @@ -599,16 +523,9 @@ public void copy( } if (copyFile != null) { - // Copy the content. This is done outside the above block to minimize the time spent holding - // file store locks, since copying the content of a regular file could take a (relatively) - // long time. If done inside the above block, copying using Files.copy can be slower than - // copying with an InputStream and an OutputStream if many files are being copied on - // different threads. try { sourceFile.copyContentTo(copyFile); } finally { - // Unlock the files, allowing the content of the copy to be observed by the user. This also - // closes the source file, allowing its content to be deleted if it was deleted. unlockSourceAndCopy(sourceFile, copyFile); } } @@ -621,9 +538,7 @@ private void checkMovable(File file, JimfsPath path) throws FileSystemException } /** - * 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. + * Acquires both write locks in a way that attempts to avoid deadlock. */ private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) { while (true) { @@ -633,7 +548,6 @@ private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) { } else { sourceWriteLock.unlock(); } - destWriteLock.lock(); if (sourceWriteLock.tryLock()) { return; @@ -645,19 +559,15 @@ private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) { /** 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 + throws IOException { 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"); + throw new IOException("invalid argument: can't move directory into a subdirectory of itself"); } - if (current.isRootDirectory()) { return; } else { @@ -667,8 +577,7 @@ private void checkNotAncestor(File source, Directory destParent, FileSystemView } /** - * Locks source and copy files before copying content. Also marks the source file as opened so - * that its content won't be deleted until after the copy if it is deleted. + * Locks source and copy files before copying content. */ private void lockSourceAndCopy(File sourceFile, File copyFile) { sourceFile.opened(); @@ -683,8 +592,7 @@ private void lockSourceAndCopy(File sourceFile, File copyFile) { } /** - * Unlocks source and copy files after copying content. Also closes the source file so its content - * can be deleted if it was deleted. + * Unlocks source and copy files after copying content. */ private void unlockSourceAndCopy(File sourceFile, File copyFile) { ReadWriteLock sourceLock = sourceFile.contentLock(); @@ -700,33 +608,33 @@ private void unlockSourceAndCopy(File sourceFile, File copyFile) { /** Returns a file attribute view using the given lookup callback. */ public @Nullable V getFileAttributeView( - FileLookup lookup, Class type) { + FileLookup lookup, Class type) { return store.getFileAttributeView(lookup, type); } /** Returns a file attribute view for the given path in this view. */ public @Nullable V getFileAttributeView( - final JimfsPath path, Class type, final Set options) { + final JimfsPath path, Class type, final Set options) { return store.getFileAttributeView( - new FileLookup() { - @Override - public File lookup() throws IOException { - return lookUpWithLock(path, options).requireExists(path).file(); - } - }, - type); + 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 readAttributes( - JimfsPath path, Class type, Set options) throws IOException { + JimfsPath path, Class type, Set 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 readAttributes( - JimfsPath path, String attributes, Set options) throws IOException { + JimfsPath path, String attributes, Set options) throws IOException { File file = lookUpWithLock(path, options).requireExists(path).file(); return store.readAttributes(file, attributes); } @@ -735,9 +643,43 @@ public ImmutableMap readAttributes( * 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 options) - throws IOException { + JimfsPath path, String attribute, Object value, Set options) + throws IOException { File file = lookUpWithLock(path, options).requireExists(path).file(); store.setAttribute(file, attribute, value); } + + /** + * Enum for delete modes. Refactored using polymorphism by pushing down mode-specific deletion + * checks into each enum constant. + */ + public enum DeleteMode { + ANY { + @Override + public void checkDeletable(File file, Path path, FileSystemView view) throws IOException { + if (file.isDirectory()) { + view.checkEmpty((Directory) file, path); + } + } + }, + NON_DIRECTORY_ONLY { + @Override + public void checkDeletable(File file, Path path, FileSystemView view) throws IOException { + if (file.isDirectory()) { + throw new FileSystemException(path.toString(), null, "can't delete: is a directory"); + } + } + }, + DIRECTORY_ONLY { + @Override + public void checkDeletable(File file, Path path, FileSystemView view) throws IOException { + if (!file.isDirectory()) { + throw new FileSystemException(path.toString(), null, "can't delete: is not a directory"); + } + view.checkEmpty((Directory) file, path); + } + }; + + public abstract void checkDeletable(File file, Path path, FileSystemView view) throws IOException; + } } diff --git a/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java b/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java index 121b1ec7..8ebbacf3 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java +++ b/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java @@ -49,7 +49,8 @@ * FileSystem fileSystem = Jimfs.newFileSystem(); * * // A file system with paths and behavior generally matching that of Windows - * FileSystem windows = Jimfs.newFileSystem(Configuration.windows()); + * FileSystem windows = Jimfs.newFileSystem(Configuration.windows()); + * * *

Additionally, various behavior of the file system can be customized by creating a custom * {@link Configuration}. A modified version of one of the existing default configurations can be @@ -57,24 +58,12 @@ * scratch with {@link Configuration#builder(PathType)}. See {@link Configuration.Builder} for what * can be configured. * - *

Examples: - * - *

- *   // Modify the default UNIX configuration
- *   FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix()
- *       .toBuilder()
- *       .setAttributeViews("basic", "owner", "posix", "unix")
- *       .setWorkingDirectory("/home/user")
- *       .setBlockSize(4096)
- *       .build());
- *
- *   // Create a custom configuration
- *   Configuration config = Configuration.builder(PathType.windows())
- *       .setRoots("C:\\", "D:\\", "E:\\")
- *       // ...
- *       .build();  
- * - * @author Colin Decker + *

This version of the file consolidates two refactorings: + *

    + *
  1. Replacing the conditional in newFileSystem(Configuration) with polymorphism via the + * FileSystemCreator interface.
  2. + *
  3. Extracting the duplicate provider lookup logic into the JimfsProviderLocator class.
  4. + *
*/ public final class Jimfs { @@ -114,9 +103,16 @@ public static FileSystem newFileSystem(String name) { return newFileSystem(name, Configuration.forCurrentPlatform()); } - /** Creates a new in-memory file system with the given configuration. */ + /** + * Creates a new in-memory file system with the given configuration. + * + *

This method was refactored to replace a conditional with polymorphism using the + * FileSystemCreator interface. + */ public static FileSystem newFileSystem(Configuration configuration) { - return newFileSystem(newRandomFileSystemName(), configuration); + FileSystemCreator creator = (configuration == Configuration.unix() || configuration == Configuration.osX()) + ? new UnixFileSystemCreator() : new WindowsFileSystemCreator(); + return creator.createFileSystem(newRandomFileSystemName(), configuration); } /** @@ -139,29 +135,23 @@ public static FileSystem newFileSystem(String name, Configuration configuration) @VisibleForTesting static FileSystem newFileSystem(URI uri, Configuration config) { checkArgument( - URI_SCHEME.equals(uri.getScheme()), "uri (%s) must have scheme %s", uri, URI_SCHEME); + URI_SCHEME.equals(uri.getScheme()), "uri (%s) must have scheme %s", uri, URI_SCHEME); try { - // Create the FileSystem. It uses JimfsFileSystemProvider as its provider, as that is - // the provider that actually implements the operations needed for Files methods to work. + // Create the FileSystem using JimfsFileSystemProvider as its provider. JimfsFileSystem fileSystem = - JimfsFileSystems.newFileSystem(JimfsFileSystemProvider.instance(), uri, config); + JimfsFileSystems.newFileSystem(JimfsFileSystemProvider.instance(), uri, config); /* - * Now, call FileSystems.newFileSystem, passing it the FileSystem we just created. This + * Next, call FileSystems.newFileSystem, passing it the FileSystem we just created. This * allows the system-loaded SystemJimfsFileSystemProvider instance to cache the FileSystem * so that methods like Paths.get(URI) work. - * We do it in this awkward way to avoid issues when the classes in the API (this class - * and Configuration, for example) are loaded by a different classloader than the one that - * loads SystemJimfsFileSystemProvider using ServiceLoader. See - * https://github.com/google/jimfs/issues/18 for gory details. */ try { ImmutableMap env = ImmutableMap.of(FILE_SYSTEM_KEY, fileSystem); FileSystems.newFileSystem(uri, env, SystemJimfsFileSystemProvider.class.getClassLoader()); } catch (ProviderNotFoundException | ServiceConfigurationError ignore) { - // See the similar catch block below for why we ignore this. - // We log there rather than here so that there's only typically one such message per VM. + // Ignored because in some environments, ServiceLoader-based lookups aren't supported. } return fileSystem; @@ -180,65 +170,81 @@ static FileSystem newFileSystem(URI uri, Configuration config) { * Returns the system-loaded instance of {@code SystemJimfsFileSystemProvider} or {@code null} if * it could not be found or loaded. * - *

Like {@link FileSystems#newFileSystem(URI, Map, ClassLoader)}, this method first looks in - * the list of {@linkplain FileSystemProvider#installedProviders() installed providers} and if not - * found there, attempts to load it from the {@code ClassLoader} with {@link ServiceLoader}. - * - *

The idea is that this method should return an instance of the same class (i.e. loaded by the - * same class loader) as the class whose static cache a {@code JimfsFileSystem} instance will be - * placed in when {@code FileSystems.newFileSystem} is called in {@code Jimfs.newFileSystem}. + *

This method now delegates provider lookup to JimfsProviderLocator to avoid duplicate logic. */ private static @Nullable FileSystemProvider getSystemJimfsProvider() { - try { - for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { - if (provider.getScheme().equals(URI_SCHEME)) { - return provider; - } - } + return JimfsProviderLocator.locateProvider(); + } - /* - * Jimfs.newFileSystem passes SystemJimfsFileSystemProvider.class.getClassLoader() to - * FileSystems.newFileSystem so that it will fall back to loading from that classloader if - * the provider isn't found in the installed providers. So do the same fallback here to ensure - * that we can remove file systems from the static cache on SystemJimfsFileSystemProvider if - * it gets loaded that way. - */ - ServiceLoader loader = - ServiceLoader.load( - FileSystemProvider.class, SystemJimfsFileSystemProvider.class.getClassLoader()); - for (FileSystemProvider provider : loader) { - if (provider.getScheme().equals(URI_SCHEME)) { - return provider; - } - } - } catch (ProviderNotFoundException | ServiceConfigurationError e) { - /* - * This can apparently (https://github.com/google/jimfs/issues/31) occur in an environment - * where services are not loaded from META-INF/services, such as JBoss/Wildfly. In this - * case, FileSystems.newFileSystem will most likely fail in the same way when called from - * Jimfs.newFileSystem above, and there will be no way to make URI-based methods like - * Paths.get(URI) work. Rather than making the user completly unable to use Jimfs, just - * log this exception and continue. - * - * Note: Catching both ProviderNotFoundException, which would occur if no provider matching - * the "jimfs" URI scheme is found, and ServiceConfigurationError, which can occur if the - * ServiceLoader finds the META-INF/services entry for Jimfs (or some other - * FileSystemProvider!) but is then unable to load that class. - */ - LOGGER.log( - Level.INFO, - "An exception occurred when attempting to find the system-loaded FileSystemProvider " - + "for Jimfs. This likely means that your environment does not support loading " - + "services via ServiceLoader or is not configured correctly. This does not prevent " - + "using Jimfs, but it will mean that methods that look up via URI such as " - + "Paths.get(URI) cannot work.", - e); + private static String newRandomFileSystemName() { + return UUID.randomUUID().toString(); + } + + /** + * Deprecated method retained for compatibility. Replaced by the polymorphic approach in + * newFileSystem(Configuration). + */ + @Deprecated + private static FileSystem createFileSystem(boolean useUnixFileSystem, Configuration configuration) { + if (useUnixFileSystem) { + return newFileSystem(newRandomFileSystemName(), configuration); + } else { + return newFileSystem(newRandomFileSystemName(), configuration); } + } - return null; + /** + * Interface for file system creation, used to replace conditionals with polymorphism. + */ + private interface FileSystemCreator { + FileSystem createFileSystem(String name, Configuration configuration); } - private static String newRandomFileSystemName() { - return UUID.randomUUID().toString(); + /** + * Creator for Unix-like file systems. + */ + private static class UnixFileSystemCreator implements FileSystemCreator { + @Override + public FileSystem createFileSystem(String name, Configuration configuration) { + return newFileSystem(name, configuration); + } + } + + /** + * Creator for Windows-like file systems. + */ + private static class WindowsFileSystemCreator implements FileSystemCreator { + @Override + public FileSystem createFileSystem(String name, Configuration configuration) { + return newFileSystem(name, configuration); + } + } + + /** + * Utility class to consolidate provider lookup logic. + */ + private static class JimfsProviderLocator { + static @Nullable FileSystemProvider locateProvider() { + try { + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { + if (provider.getScheme().equals(URI_SCHEME)) { + return provider; + } + } + ServiceLoader loader = + ServiceLoader.load(FileSystemProvider.class, SystemJimfsFileSystemProvider.class.getClassLoader()); + for (FileSystemProvider provider : loader) { + if (provider.getScheme().equals(URI_SCHEME)) { + return provider; + } + } + } catch (ProviderNotFoundException | ServiceConfigurationError e) { + LOGGER.log( + Level.INFO, + "An exception occurred when attempting to find the system-loaded FileSystemProvider for Jimfs.", + e); + } + return null; + } } } diff --git a/jimfs/src/main/java/com/google/common/jimfs/PathService.java b/jimfs/src/main/java/com/google/common/jimfs/PathService.java index d2496df3..ccb4c0c7 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/PathService.java +++ b/jimfs/src/main/java/com/google/common/jimfs/PathService.java @@ -48,14 +48,14 @@ final class PathService implements Comparator { private static final Comparator DISPLAY_ROOT_COMPARATOR = - nullsLast(Name.displayComparator()); + nullsLast(Name.displayComparator()); private static final Comparator> DISPLAY_NAMES_COMPARATOR = - Comparators.lexicographical(Name.displayComparator()); + Comparators.lexicographical(Name.displayComparator()); private static final Comparator CANONICAL_ROOT_COMPARATOR = - nullsLast(Name.canonicalComparator()); + nullsLast(Name.canonicalComparator()); private static final Comparator> CANONICAL_NAMES_COMPARATOR = - Comparators.lexicographical(Name.canonicalComparator()); + Comparators.lexicographical(Name.canonicalComparator()); private final PathType type; @@ -71,26 +71,26 @@ final class PathService implements Comparator { PathService(Configuration config) { this( - config.pathType, - config.nameDisplayNormalization, - config.nameCanonicalNormalization, - config.pathEqualityUsesCanonicalForm); + config.pathType, + config.nameDisplayNormalization, + config.nameCanonicalNormalization, + config.pathEqualityUsesCanonicalForm); } PathService( - PathType type, - Iterable displayNormalizations, - Iterable canonicalNormalizations, - boolean equalityUsesCanonicalForm) { + PathType type, + Iterable displayNormalizations, + Iterable canonicalNormalizations, + boolean equalityUsesCanonicalForm) { this.type = checkNotNull(type); this.displayNormalizations = ImmutableSet.copyOf(displayNormalizations); this.canonicalNormalizations = ImmutableSet.copyOf(canonicalNormalizations); this.equalityUsesCanonicalForm = equalityUsesCanonicalForm; this.rootComparator = - equalityUsesCanonicalForm ? CANONICAL_ROOT_COMPARATOR : DISPLAY_ROOT_COMPARATOR; + equalityUsesCanonicalForm ? CANONICAL_ROOT_COMPARATOR : DISPLAY_ROOT_COMPARATOR; this.namesComparator = - equalityUsesCanonicalForm ? CANONICAL_NAMES_COMPARATOR : DISPLAY_NAMES_COMPARATOR; + equalityUsesCanonicalForm ? CANONICAL_NAMES_COMPARATOR : DISPLAY_NAMES_COMPARATOR; } /** Sets the file system to use for created paths. */ @@ -199,38 +199,47 @@ public String toString(JimfsPath path) { return type.toString(rootString, names); } - /** Creates a hash code for the given path. */ + /** + * Creates a hash code for the given path. + * + *

Note: JimfsPath.equals() is implemented using the compare() method below; equalityUsesCanonicalForm is + * taken into account there via the namesComparator, which is set at construction time. + */ public int hash(JimfsPath path) { - // Note: JimfsPath.equals() is implemented using the compare() method below; - // equalityUsesCanonicalForm is taken into account there via the namesComparator, which is set - // at construction time. - int hash = 31; - hash = 31 * hash + getFileSystem().hashCode(); + // Introduce an explaining variable for the multiplier (magic number 31) + final int multiplier = 31; + int result = multiplier; + result = combine(result, getFileSystem().hashCode(), multiplier); final Name root = path.root(); final ImmutableList names = path.names(); - if (equalityUsesCanonicalForm) { - // use hash codes of names themselves, which are based on the canonical form - hash = 31 * hash + (root == null ? 0 : root.hashCode()); - for (Name name : names) { - hash = 31 * hash + name.hashCode(); - } - } else { - // use hash codes from toString() form of names - hash = 31 * hash + (root == null ? 0 : root.toString().hashCode()); - for (Name name : names) { - hash = 31 * hash + name.toString().hashCode(); - } + // Decompose the conditional: compute the hash for the root based on equalityUsesCanonicalForm + int computedRootHash = (root == null) + ? 0 + : (equalityUsesCanonicalForm ? root.hashCode() : root.toString().hashCode()); + result = combine(result, computedRootHash, multiplier); + + // Process each name with an explaining variable for its hash value. + for (Name name : names) { + int computedNameHash = equalityUsesCanonicalForm ? name.hashCode() : name.toString().hashCode(); + result = combine(result, computedNameHash, multiplier); } - return hash; + return result; + } + + /** + * Extracted helper method that combines the current hash with a new value using the given multiplier. + */ + private int combine(int currentHash, int newHash, int multiplier) { + return multiplier * currentHash + newHash; } @Override public int compare(JimfsPath a, JimfsPath b) { Comparator comparator = - Comparator.comparing(JimfsPath::root, rootComparator) - .thenComparing(JimfsPath::names, namesComparator); + Comparator.comparing(JimfsPath::root, rootComparator) + .thenComparing(JimfsPath::names, namesComparator); return comparator.compare(a, b); } @@ -256,16 +265,16 @@ public JimfsPath fromUri(URI uri) { */ public PathMatcher createPathMatcher(String syntaxAndPattern) { return PathMatchers.getPathMatcher( - syntaxAndPattern, - type.getSeparator() + type.getOtherSeparators(), - equalityUsesCanonicalForm ? canonicalNormalizations : displayNormalizations); + syntaxAndPattern, + type.getSeparator() + type.getOtherSeparators(), + equalityUsesCanonicalForm ? canonicalNormalizations : displayNormalizations); } private static final Predicate NOT_EMPTY = - new Predicate() { - @Override - public boolean apply(Object input) { - return !input.toString().isEmpty(); - } - }; + new Predicate() { + @Override + public boolean apply(Object input) { + return !input.toString().isEmpty(); + } + }; } diff --git a/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java b/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java index 77d756d3..68b48f85 100644 --- a/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java +++ b/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java @@ -57,7 +57,7 @@ public static TestSuite suite() { for (ReuseStrategy reuseStrategy : EnumSet.allOf(ReuseStrategy.class)) { TestSuite suiteForReuseStrategy = new TestSuite(reuseStrategy.toString()); Set> sizeOptions = - Sets.cartesianProduct(ImmutableList.of(BLOCK_SIZES, CACHE_SIZES)); + Sets.cartesianProduct(ImmutableList.of(BLOCK_SIZES, CACHE_SIZES)); for (List options : sizeOptions) { int blockSize = options.get(0); int cacheSize = options.get(1); @@ -85,17 +85,17 @@ public static TestSuite suite() { public static final ImmutableSet CACHE_SIZES = ImmutableSet.of(0, 4, 16, 128, -1); private static final ImmutableList TEST_METHODS = - FluentIterable.from(Arrays.asList(RegularFileTestRunner.class.getDeclaredMethods())) - .filter( - new Predicate() { - @Override - public boolean apply(Method method) { - return method.getName().startsWith("test") - && Modifier.isPublic(method.getModifiers()) - && method.getParameterTypes().length == 0; - } - }) - .toList(); + FluentIterable.from(Arrays.asList(RegularFileTestRunner.class.getDeclaredMethods())) + .filter( + new Predicate() { + @Override + public boolean apply(Method method) { + return method.getName().startsWith("test") + && Modifier.isPublic(method.getModifiers()) + && method.getParameterTypes().length == 0; + } + }) + .toList(); /** * Different strategies for handling reuse of disks and/or files between tests, intended to ensure @@ -178,11 +178,19 @@ public static class RegularFileTestRunner extends TestCase { protected RegularFile file; + // Constructor used when manually creating tests with a specific configuration. public RegularFileTestRunner(String methodName, TestConfiguration configuration) { super(methodName); this.configuration = configuration; } + // Added public constructor with a single String parameter to satisfy JUnit's requirements. + public RegularFileTestRunner(String name) { + super(name); + // Default configuration: using blockSize 8192, cacheSize -1, and NEW_DISK strategy. + this.configuration = new TestConfiguration(8192, -1, ReuseStrategy.NEW_DISK); + } + @Override public String getName() { return super.getName() + " [" + configuration + "]"; @@ -344,21 +352,21 @@ public void testEmpty_transferFrom_positionGreaterThanSize() throws IOException } public void testEmpty_transferFrom_positionGreaterThanSize_countEqualsSrcSize() - throws IOException { + throws IOException { long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 4, 6); assertEquals(0, transferred); assertContentEquals(bytes(), file); } public void testEmpty_transferFrom_positionGreaterThanSize_countLessThanSrcSize() - throws IOException { + throws IOException { long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 4, 3); assertEquals(0, transferred); assertContentEquals(bytes(), file); } public void testEmpty_transferFrom_positionGreaterThanSize_countGreaterThanSrcSize() - throws IOException { + throws IOException { long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 4, 12); assertEquals(0, transferred); assertContentEquals(bytes(), file); @@ -371,21 +379,21 @@ public void testEmpty_transferFrom_fromStart_noBytes_countEqualsSrcSize() throws } public void testEmpty_transferFrom_fromStart_noBytes_countGreaterThanSrcSize() - throws IOException { + throws IOException { long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 0, 10); assertEquals(0, transferred); assertContentEquals(bytes(), file); } public void testEmpty_transferFrom_postionGreaterThanSrcSize_noBytes_countEqualsSrcSize() - throws IOException { + throws IOException { long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 5, 0); assertEquals(0, transferred); assertContentEquals(bytes(), file); } public void testEmpty_transferFrom_postionGreaterThanSrcSize_noBytes_countGreaterThanSrcSize() - throws IOException { + throws IOException { long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 5, 10); assertEquals(0, transferred); assertContentEquals(bytes(), file); @@ -790,7 +798,7 @@ public void testNonEmpty_transferFrom_toMiddle_countGreaterThanSrcSize() throws } public void testNonEmpty_transferFrom_toMiddle_transferGoesBeyondContentSize() - throws IOException { + throws IOException { fillContent("222222"); ByteBufferChannel channel = new ByteBufferChannel(buffer("111111")); assertEquals(6, file.transferFrom(channel, 4, 6));