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

com.github.marschall.memoryfilesystem.MemoryFileSystem Maven / Gradle / Ivy

package com.github.marschall.memoryfilesystem;

import static com.github.marschall.memoryfilesystem.AutoReleaseLock.autoRelease;
import static java.nio.file.AccessMode.WRITE;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.CREATE_NEW;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.channels.FileChannel;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemException;
import java.nio.file.FileSystemLoopException;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.NotLinkException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.StandardOpenOption;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.spi.FileSystemProvider;
import java.text.Collator;
import java.time.Instant;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MemoryFileSystem extends FileSystem implements FileSystemContext {

  private static final Set UNSUPPORTED_INITIAL_ATTRIBUTES;

  private static final Set NO_OPEN_OPTIONS = Collections.emptySet();

  private static final FileAttribute[] NO_FILE_ATTRIBUTES = new FileAttribute[0];

  private final String key;

  private final String separator;

  private final MemoryFileSystemProvider provider;

  private final MemoryFileStore store;

  private final Iterable stores;

  private final ClosedFileSystemChecker checker;

  private volatile Map roots;

  private volatile Map rootByKey;

  private final ConcurrentMap> watchKeys;

  private volatile AbstractPath defaultPath;

  private final MemoryUserPrincipalLookupService userPrincipalLookupService;

  private final PathParser pathParser;

  private final EmptyPath emptyPath;

  // computes the file name to be stored of a file
  private final StringTransformer storeTransformer;

  // computes the look up key of a file name
  private final StringTransformer lookUpTransformer;

  private final Collator collator;

  private final Set> additionalViews;

  private final Set supportedFileAttributeViews;

  /**
   * Operations involving multiple paths (copy and move) need to use
   * lock ordering to avoid deadlocks. During the lock acquisition phase
   * no changes must be made that might affect the ordering. This involves
   * deleting and creating files (with a different capitalization) or links.
   * We assume deleting is less common so we block this operation.
   */
  private final ReadWriteLock pathOrderingLock;

  private final Set umask;

  private final TemporalUnit resolution;

  private final boolean supportFileChannelOnDirectory;

  static {
    Set unsupported = new HashSet<>(3);
    unsupported.add("lastAccessTime");
    unsupported.add("creationTime");
    unsupported.add("lastModifiedTime");

    UNSUPPORTED_INITIAL_ATTRIBUTES = Collections.unmodifiableSet(unsupported);
  }

  MemoryFileSystem(String key, String separator, PathParser pathParser, MemoryFileSystemProvider provider, MemoryFileStore store,
          MemoryUserPrincipalLookupService userPrincipalLookupService, ClosedFileSystemChecker checker, StringTransformer storeTransformer,
          StringTransformer lookUpTransformer, Collator collator, Set> additionalViews,
          Set umask, TemporalUnit resolution, boolean supportDirectoryFileChannelHack) {
    this.key = key;
    this.separator = separator;
    this.pathParser = pathParser;
    this.provider = provider;
    this.store = store;
    this.userPrincipalLookupService = userPrincipalLookupService;
    this.checker = checker;
    this.storeTransformer = storeTransformer;
    this.lookUpTransformer = lookUpTransformer;
    this.collator = collator;
    this.additionalViews = additionalViews;
    this.umask = umask;
    this.resolution = resolution;
    this.supportFileChannelOnDirectory = supportDirectoryFileChannelHack;
    this.stores = Collections.singletonList(store);
    this.watchKeys = new ConcurrentHashMap<>(1);
    this.emptyPath = new EmptyPath(this);
    this.supportedFileAttributeViews = this.buildSupportedFileAttributeViews(additionalViews);
    this.pathOrderingLock = new ReentrantReadWriteLock();
  }

  private Set buildSupportedFileAttributeViews(Set> additionalViews) {
    if (additionalViews.isEmpty()) {
      return Collections.singleton(FileAttributeViews.BASIC);
    } else {
      Set views = new HashSet<>(additionalViews.size() + 2);
      views.add(FileAttributeViews.BASIC);
      for (Class viewClass : additionalViews) {
        if (FileOwnerAttributeView.class.isAssignableFrom(viewClass)) {
          views.add(FileAttributeViews.OWNER);
        }
        if (viewClass != FileOwnerAttributeView.class) {
          views.add(FileAttributeViews.mapAttributeView(viewClass));
        }
      }
      return Collections.unmodifiableSet(views);
    }
  }

  String getKey() {
    return this.key;
  }

  Set getUmask() {
    return this.umask;
  }

  EntryCreationContext newEntryCreationContext(Path path, FileAttribute[] attributes) throws IOException {
    Set permissions = EnumSet.allOf(PosixFilePermission.class);

    for (FileAttribute attribute: attributes) {
      if (attribute instanceof PosixFileAttributes) {
        permissions = ((PosixFileAttributes) attribute).permissions();
        break;
      }
    }

    UserPrincipal user = this.getCurrentUser();
    GroupPrincipal group = this.getGroupOf(user);
    return new EntryCreationContext(this.additionalViews, permissions, user, group, this, path);
  }

  private UserPrincipal getCurrentUser() {
    UserPrincipal currentUser = CurrentUser.get();
    if (currentUser != null) {
      return currentUser;
    } else {
      return this.userPrincipalLookupService.getDefaultUser();
    }
  }

  private GroupPrincipal getGroupOf(UserPrincipal user) throws IOException {
    // TODO is this always true?
    return this.userPrincipalLookupService.lookupPrincipalByGroupName(user.getName());
  }

  EmptyPath getEmptyPath() {
    return this.emptyPath;
  }


  /**
   * Sets the root directories.
   *
   * 

This is a bit annoying.

* * @param rootDirectories the root directories, not {@code null}, * should not be modified, no defensive copy will be made */ void setRootDirectories(Map rootDirectories) { this.roots = rootDirectories; this.rootByKey = this.buildRootsByKey(rootDirectories.keySet()); } private Map buildRootsByKey(Collection rootDirectories) { if (rootDirectories.isEmpty()) { throw new IllegalArgumentException("a file system root must be present"); } else if (rootDirectories.size() == 1) { Root root = rootDirectories.iterator().next(); String key = this.lookUpTransformer.transform(root.getKey()); return Collections.singletonMap(key, root); } else { Map map = new HashMap<>(rootDirectories.size()); for (Root root : rootDirectories) { String key = this.lookUpTransformer.transform(root.getKey()); map.put(key, root); } return map; } } /** * Sets the current working directory. * *

This is used to resolve relative paths. This has to be set * after {@link #setRootDirectories(Map)}

. * * @param currentWorkingDirectory the default directory path */ void setCurrentWorkingDirectory(String currentWorkingDirectory) { this.defaultPath = this.getPath(currentWorkingDirectory); if (!this.defaultPath.isAbsolute()) { throw new IllegalArgumentException("current working directory must be absolute"); } } AbstractPath getDefaultPath() { return this.defaultPath; } FileChannel newFileChannel(AbstractPath path, Set options, FileAttribute... attrs) throws IOException { this.checker.check(); MemoryEntry entry = this.getEntry(path, options, attrs); if (entry instanceof MemoryFile) { return ((MemoryFile) entry).newChannel(options, path); } if (entry instanceof MemoryDirectory) { boolean isRead = options.contains(StandardOpenOption.READ); boolean isWrite = options.contains(StandardOpenOption.WRITE); if (this.supportFileChannelOnDirectory && isRead && !isWrite) { return new DirectoryChannel(); } } throw new FileSystemException(path.toAbsolutePath().toString(), null, "is not a file"); } private static Set toOptionSet(Set defaultOptions, OpenOption... options) { if (options == null || options.length == 0) { return defaultOptions; } else { Set optionsSet = new HashSet<>(options.length); Collections.addAll(optionsSet, options); return optionsSet; } } InputStream newInputStream(AbstractPath path, OpenOption... options) throws IOException { this.checker.check(); Set optionsSet = toOptionSet(Collections.emptySet(), options); MemoryFile file = this.getFile(path, optionsSet); return file.newInputStream(optionsSet, path); } OutputStream newOutputStream(AbstractPath path, OpenOption... options) throws IOException { this.checker.check(); Set optionsSet = toOptionSet(DefaultOpenOptions.INSTANCE, options); MemoryFile file = this.getFile(path, optionsSet); return file.newOutputStream(optionsSet, path); } private static void checkSupportedInitialAttributes(FileAttribute... attrs) { if (attrs != null) { for (FileAttribute attribute : attrs) { String attributeName = attribute.name(); if (UNSUPPORTED_INITIAL_ATTRIBUTES.contains(attributeName)) { throw new UnsupportedOperationException("'" + attributeName + "' not supported as initial attribute"); } } } } private GetEntryResult getEntry(final AbstractPath path, final Set options, FileAttribute[] attrs, final boolean followSymLinks, final Set encounteredSymlinks) throws IOException { final FileAttribute[] newAttributes = this.applyUmask(attrs); // TODO lazy final AbstractPath absolutePath = (AbstractPath) path.toAbsolutePath().normalize(); MemoryDirectory rootDirectory = this.getRootDirectory(absolutePath); if (absolutePath.isRoot()) { return new GetEntryResult(rootDirectory); } final AbstractPath parent = (AbstractPath) absolutePath.getParent(); return this.withWriteLockOnLastDo(rootDirectory, parent, followSymLinks, encounteredSymlinks, new MemoryDirectoryBlock() { @Override public GetEntryResult value(MemoryDirectory directory) throws IOException { ElementPath elementPath = (ElementPath) absolutePath; boolean isCreateNew = options.contains(CREATE_NEW); String fileName = elementPath.getLastNameElement(); String key = MemoryFileSystem.this.lookUpTransformer.transform(fileName); EntryCreationContext creationContext = MemoryFileSystem.this.newEntryCreationContext(absolutePath, newAttributes); if (isCreateNew) { MemoryFile file = this.createEntryOnAccess(path, newAttributes, directory, elementPath, creationContext); return new GetEntryResult(file); } else { MemoryEntry storedEntry = directory.getEntry(key); if (storedEntry == null) { boolean isCreate = options.contains(CREATE); if (isCreate) { MemoryFile file = this.createEntryOnAccess(path, newAttributes, directory, elementPath, creationContext); return new GetEntryResult(file); } else { throw new NoSuchFileException(path.toString()); } } if (storedEntry instanceof MemorySymbolicLink && followSymLinks) { MemorySymbolicLink link = (MemorySymbolicLink) storedEntry; if (!encounteredSymlinks.add(link)) { throw new FileSystemLoopException(path.toString()); } AbstractPath linkTarget = link.getTarget(); if (linkTarget.isAbsolute()) { return new GetEntryResult(linkTarget); } else { return new GetEntryResult((AbstractPath) parent.resolve(linkTarget)); } } else { return new GetEntryResult(storedEntry); } } } private MemoryFile createEntryOnAccess(final AbstractPath path, final FileAttribute[] newAttributes, MemoryDirectory directory, ElementPath elementPath, EntryCreationContext creationContext) throws IOException { String fileName = elementPath.getLastNameElement(); String key = MemoryFileSystem.this.lookUpTransformer.transform(fileName); String name = MemoryFileSystem.this.storeTransformer.transform(fileName); MemoryFile file = new MemoryFile(name, creationContext); checkSupportedInitialAttributes(newAttributes); AttributeAccessors.setAttributes(file, newAttributes); directory.checkAccess(WRITE); // will throw an exception if already present directory.addEntry(key, file, path); return file; } }); } static final class GetEntryResult { final MemoryEntry entry; final AbstractPath linkTarget; GetEntryResult(MemoryEntry entry) { this.entry = entry; this.linkTarget = null; } GetEntryResult(AbstractPath linkTarget) { this.entry = null; this.linkTarget = linkTarget; } } private MemoryFile getFile(AbstractPath path, Set options, FileAttribute... attrs) throws IOException { MemoryEntry file = this.getEntry(path, options); if (file instanceof MemoryFile) { return (MemoryFile) file; } else { throw new FileSystemException(path.toString().toString(), null, "is not a file"); } } private MemoryEntry getEntry(AbstractPath path, Set options, FileAttribute... attrs) throws IOException { boolean followSymLinks = Options.isFollowSymLinks(options); Set encounteredSymlinks; if (followSymLinks) { // we don't expect to encounter many symlinks so we initialize to a lower than the default value of 16 // TODO optimized set encounteredSymlinks = new HashSet<>(4); } else { encounteredSymlinks = Collections.emptySet(); } GetEntryResult result = this.getEntry(path, options, attrs, followSymLinks, encounteredSymlinks); while (result.entry == null) { result = this.getEntry(result.linkTarget, options, attrs, followSymLinks, encounteredSymlinks); } return result.entry; } private FileAttribute[] applyUmask(FileAttribute[] attributes) { if (this.umask.isEmpty()) { return attributes; } int length = attributes.length; boolean changed = false; FileAttribute[] copy = null; for (int i = 0; i < length; i++) { FileAttribute attribute = attributes[i]; if (changed) { copy[i] = attribute; continue; } if ("posix:permissions".equals(attribute.name())) { copy = new FileAttribute[length]; changed = true; if (i > 0) { System.arraycopy(attributes, 0, copy, 0, i - 1); } Set perms = (Set) attribute.value(); Set newPerms = EnumSet.copyOf(perms); newPerms.removeAll(this.umask); copy[i] = PosixFilePermissions.asFileAttribute(newPerms); } } if (changed) { return copy; } else { // umask is set so we can reasonably sure posix permissions are supported // add permissions and set the to the umask FileAttribute[] withPermissions = new FileAttribute[length + 1]; System.arraycopy(attributes, 0, withPermissions, 0, length); EnumSet permissions = EnumSet.allOf(PosixFilePermission.class); permissions.removeAll(this.umask); withPermissions[length] = PosixFilePermissions.asFileAttribute(permissions); return withPermissions; } } DirectoryStream newDirectoryStream(final AbstractPath abstractPath, final Filter filter) throws IOException { return this.accessFileReading(abstractPath, true, entry -> { if (!(entry instanceof MemoryDirectory)) { throw new NotDirectoryException(abstractPath.toString()); } MemoryDirectory directory = (MemoryDirectory) entry; return directory.newDirectoryStream(abstractPath, filter); }); } void createDirectory(final AbstractPath path, final FileAttribute... attrs) throws IOException { final FileAttribute[] masked = this.applyUmask(attrs); this.createFile(path, name -> { EntryCreationContext context = MemoryFileSystem.this.newEntryCreationContext(path, masked); MemoryDirectory directory = new MemoryDirectory(name, context); AttributeAccessors.setAttributes(directory, masked); return directory; }); } void createSymbolicLink(final AbstractPath link, final AbstractPath target, final FileAttribute... attrs) throws IOException { final FileAttribute[] masked = this.applyUmask(attrs); this.createFile(link, name -> { EntryCreationContext context = MemoryFileSystem.this.newEntryCreationContext(link, masked); MemorySymbolicLink symbolicLink = new MemorySymbolicLink(name, target, context); AttributeAccessors.setAttributes(symbolicLink, masked); return symbolicLink; }); } void createLink(final AbstractPath link, AbstractPath existing) throws IOException { final MemoryFile existingFile = this.getFile(existing); if (existingFile == null) { throw new FileSystemException(link.toString(), existing.toString(), "hard links are only supported for regular files"); } this.createFile(link, name -> { EntryCreationContext context = MemoryFileSystem.this.newEntryCreationContext(link, NO_FILE_ATTRIBUTES); return existingFile.createLink(name, context); }); } boolean isSameFile(AbstractPath path, AbstractPath path2) throws IOException { final MemoryFile file = this.getFile(path); if (file == null) { return false; } final MemoryFile file2 = this.getFile(path2); if (file2 == null) { return false; } return file.hasSameInodeAs(file2); } private MemoryFile getFile(AbstractPath existing) throws IOException { return this.accessFileReading(existing, true, entry -> { if (entry instanceof MemoryFile) { return (MemoryFile) entry; } return null; }); } private void createFile(final AbstractPath path, final MemoryEntryCreator creator) throws IOException { this.checker.check(); AbstractPath absolutePath = (AbstractPath) path.toAbsolutePath().normalize(); if (absolutePath.isRoot()) { throw new FileAlreadyExistsException(path.toString(), null, "can not create root"); } final ElementPath elementPath = (ElementPath) absolutePath; this.accessDirectoryWriting((AbstractPath) elementPath.getParent(), true, directory -> { String name = MemoryFileSystem.this.storeTransformer.transform(elementPath.getLastNameElement()); MemoryEntry newEntry = creator.create(name); String key = MemoryFileSystem.this.lookUpTransformer.transform(newEntry.getOriginalName()); directory.checkAccess(WRITE); directory.addEntry(key, newEntry, path); return null; }); } AbstractPath toRealPath(AbstractPath abstractPath, LinkOption... options) throws IOException { this.checker.check(); AbstractPath absolutePath = (AbstractPath) abstractPath.toAbsolutePath().normalize(); boolean followSymLinks = Options.isFollowSymLinks(options); Set encounteredSymlinks; if (followSymLinks) { // we don't expect to encounter many symlinks so we initialize to a lower than the default value of 16 // TODO optimized set encounteredSymlinks = new HashSet<>(4); } else { encounteredSymlinks = Collections.emptySet(); } MemoryDirectory root = this.getRootDirectory(absolutePath); return this.toRealPath(root, absolutePath, encounteredSymlinks, followSymLinks); } private AbstractPath toRealPath(MemoryDirectory root, AbstractPath path, Set encounteredLinks, boolean followSymLinks) throws IOException { if (path.isRoot()) { return (AbstractPath) path.getRoot(); } else if (path instanceof ElementPath) { ElementPath elementPath = (ElementPath) path; List nameElements = elementPath.getNameElements(); int pathElementCount = nameElements.size(); List realPath = new ArrayList<>(pathElementCount); List locks = new ArrayList<>(pathElementCount + 1); try { locks.add(root.readLock()); MemoryDirectory parent = root; for (int i = 0; i < pathElementCount; ++i) { String fileName = nameElements.get(i); String key = this.lookUpTransformer.transform(fileName); MemoryEntry current = parent.getEntryOrException(key, path); locks.add(current.readLock()); realPath.add(current.getOriginalName()); if (followSymLinks && current instanceof MemorySymbolicLink) { MemorySymbolicLink link = (MemorySymbolicLink) current; if (!encounteredLinks.add(link)) { throw new FileSystemLoopException(path.toString()); } Path symLinkTarget = link.getTarget(); Path newLookUpPath; if (symLinkTarget.isAbsolute()) { newLookUpPath = symLinkTarget; } else { newLookUpPath = path.getRoot(); for (int j = 0; j < i; ++j) { newLookUpPath = newLookUpPath.resolve(nameElements.get(j)); } newLookUpPath = newLookUpPath.resolve(symLinkTarget); } for (int j = i + 1; j < pathElementCount; ++j) { newLookUpPath = newLookUpPath.resolve(nameElements.get(j)); } return this.toRealPath(root, (AbstractPath) newLookUpPath, encounteredLinks, followSymLinks); } if (current instanceof MemoryDirectory) { parent = (MemoryDirectory) current; } else if (i < (pathElementCount - 1)) { // all except last must be a directory throw new NotDirectoryException(path.toString()); } } } finally { for (int i = locks.size() - 1; i >= 0; --i) { AutoRelease lock = locks.get(i); lock.close(); } } return AbstractPath.createAbsolute(this, (Root) path.getRoot(), realPath); } else { throw new IllegalArgumentException("unknown path type" + path); } } void checkAccess(AbstractPath path, final AccessMode... modes) throws IOException { this.checker.check(); // java.nio.file.spi.FileSystemProvider#checkAccess(Path, AccessMode...) // says we should follow symbolic links this.accessFileReading(path, true, entry -> { entry.checkAccess(modes); return null; }); } boolean isDirectory(AbstractPath path) { try { return this.accessFileReading(path, false, entry -> entry.isDirectory()); } catch (IOException e) { // use for URI and URL construction, we do not want to throw in case of non-existing files return false; } } A readAttributes(AbstractPath path, final Class type, LinkOption... options) throws IOException { this.checker.check(); return this.accessFileReading(path, Options.isFollowSymLinks(options), entry -> entry.readAttributes(type)); } V getLazyFileAttributeView(AbstractPath path, Class type, LinkOption... options) { if (type != BasicFileAttributeView.class && !this.additionalViews.contains(type)) { // unsupported view, specification requires null return null; } InvocationHandler handler = new LazyFileAttributeView(path, type, options); Object proxy = Proxy.newProxyInstance(MemoryFileSystem.class.getClassLoader(), new Class[]{type}, handler); return type.cast(proxy); } V getFileAttributeView(AbstractPath path, final Class type, LinkOption... options) throws IOException { return this.accessFileReading(path, Options.isFollowSymLinks(options), entry -> entry.getFileAttributeView(type)); } Map readAttributes(AbstractPath path, final String attributes, LinkOption... options) throws IOException { this.checker.check(); return this.accessFileReading(path, Options.isFollowSymLinks(options), entry -> AttributeAccessors.readAttributes(entry, attributes)); } void setAttribute(AbstractPath path, final String attribute, final Object value, LinkOption... options) throws IOException { this.checker.check(); this.accessFileWriting(path, Options.isFollowSymLinks(options), entry -> { // TODO write lock? AttributeAccessors.setAttribute(entry, attribute, value); return null; }); } private R accessFileReading(AbstractPath path, boolean followSymLinks, MemoryEntryBlock callback) throws IOException { return this.accessFile(path, followSymLinks, LockType.READ, callback); } private R accessFileWriting(AbstractPath path, boolean followSymLinks, MemoryEntryBlock callback) throws IOException { return this.accessFile(path, followSymLinks, LockType.WRITE, callback); } private R accessFile(AbstractPath path, boolean followSymLinks, LockType lockType, MemoryEntryBlock callback) throws IOException { this.checker.check(); AbstractPath absolutePath = (AbstractPath) path.toAbsolutePath().normalize(); MemoryDirectory root = this.getRootDirectory(absolutePath); Set encounteredSymlinks; if (followSymLinks) { encounteredSymlinks = new HashSet<>(4); } else { encounteredSymlinks = Collections.emptySet(); } return this.withLockDo(root, absolutePath, encounteredSymlinks, followSymLinks, lockType, callback); } private R withWriteLockOnLastDo(MemoryDirectory root, final AbstractPath path, boolean followSymLinks, final MemoryDirectoryBlock callback) throws IOException { Set encounteredSymlinks; if (followSymLinks) { encounteredSymlinks = new HashSet<>(4); } else { encounteredSymlinks = Collections.emptySet(); } return this.withWriteLockOnLastDo(root, path, followSymLinks, encounteredSymlinks, callback); } private R withWriteLockOnLastDo(MemoryDirectory root, final AbstractPath path, boolean followSymLinks, Set encounteredSymlinks, final MemoryDirectoryBlock callback) throws IOException { return this.withLockDo(root, path, encounteredSymlinks, followSymLinks, LockType.WRITE, entry -> { if (!(entry instanceof MemoryDirectory)) { throw new NotDirectoryException(path.toString()); } return callback.value((MemoryDirectory) entry); }); } private R accessDirectoryWriting(final AbstractPath path, boolean followSymLinks, final MemoryDirectoryBlock callback) throws IOException { return this.accessFileWriting(path, followSymLinks, entry -> { if (!(entry instanceof MemoryDirectory)) { throw new NotDirectoryException(path.toString()); } return callback.value((MemoryDirectory) entry); }); } private R withReadLockDo(MemoryDirectory root, AbstractPath path, boolean followSymLinks, MemoryEntryBlock callback) throws IOException { Set encounteredSymlinks; if (followSymLinks) { encounteredSymlinks = new HashSet<>(4); } else { encounteredSymlinks = Collections.emptySet(); } return this.withLockDo(root, path, encounteredSymlinks, followSymLinks, LockType.READ, callback); } private R withLockDo(MemoryDirectory root, AbstractPath path, Set encounteredLinks, boolean followSymLinks, LockType lockType, MemoryEntryBlock callback) throws IOException { if (path.isRoot()) { try (AutoRelease lock = root.lock(lockType)) { return callback.value(root); } } else if (path instanceof ElementPath) { R result = null; Path newLookUpPath = null; ElementPath elementPath = (ElementPath) path; List nameElements = elementPath.getNameElements(); int pathElementCount = nameElements.size(); List locks = new ArrayList<>(pathElementCount + 1); try { locks.add(root.readLock()); MemoryDirectory parent = root; for (int i = 0; i < pathElementCount; ++i) { String fileName = nameElements.get(i); String key = this.lookUpTransformer.transform(fileName); MemoryEntry current = parent.getEntryOrException(key, path); boolean isLast = i == pathElementCount - 1; if (isLast) { locks.add(current.lock(lockType)); } else { locks.add(current.readLock()); } if (followSymLinks && current instanceof MemorySymbolicLink) { MemorySymbolicLink link = (MemorySymbolicLink) current; if (!encounteredLinks.add(link)) { throw new FileSystemLoopException(path.toString()); } Path symLinkTarget = link.getTarget(); if (symLinkTarget.isAbsolute()) { newLookUpPath = symLinkTarget; } else { newLookUpPath = path.getRoot(); for (int j = 0; j < i; ++j) { newLookUpPath = newLookUpPath.resolve(nameElements.get(j)); } newLookUpPath = newLookUpPath.resolve(symLinkTarget); } for (int j = i + 1; j < pathElementCount; ++j) { newLookUpPath = newLookUpPath.resolve(nameElements.get(j)); } break; } if (isLast) { result = callback.value(current); } else if (current instanceof MemoryDirectory) { parent = (MemoryDirectory) current; } else { throw new NotDirectoryException(path.toString()); } } } finally { for (int i = locks.size() - 1; i >= 0; --i) { AutoRelease lock = locks.get(i); lock.close(); } } if (newLookUpPath == null) { return result; } else { return this.withLockDo(root, (AbstractPath) newLookUpPath, encounteredLinks, followSymLinks, lockType, callback); } } else { throw new IllegalArgumentException("unknown path type" + path); } } private MemoryDirectory getRootDirectory(AbstractPath path) throws IOException { Path root = path.getRoot(); MemoryDirectory directory = this.roots.get(root); if (directory == null) { throw new NoSuchFileException(path.toString(), null, "the root doesn't exist"); } return directory; } void copyOrMove(AbstractPath source, AbstractPath target, TwoPathOperation operation, CopyOption... options) throws IOException { try (AutoRelease autoRelease = autoRelease(this.pathOrderingLock.writeLock())) { EndPointCopyContext sourceContext = this.buildEndpointCopyContext(source); EndPointCopyContext targetContext = this.buildEndpointCopyContext(target); int order = this.orderPaths(sourceContext, targetContext); final CopyContext copyContext = buildCopyContext(sourceContext, targetContext, operation, options, order); AbstractPath firstParent = copyContext.first.parent; final AbstractPath secondParent = copyContext.second.parent; if (firstParent == null && secondParent == null) { // both of the involved paths is a root // simply ignore return; } if (firstParent == null || secondParent == null) { // only one of the involved paths is a root throw new FileSystemException(toStringOrNull(firstParent), toStringOrNull(secondParent), "can't copy or move root directory"); } MemoryDirectory firstRoot = this.getRootDirectory(copyContext.first.path); final MemoryDirectory secondRoot = this.getRootDirectory(copyContext.second.path); this.withWriteLockOnLastDo(firstRoot, firstParent, copyContext.firstFollowSymLinks, firstDirectory -> { MemoryFileSystem.this.withWriteLockOnLastDo(secondRoot, secondParent, copyContext.secondFollowSymLinks, secondDirectory -> { handleTwoPathOperation(copyContext, firstDirectory, secondDirectory); return null; }); return null; }); } } static void copyOrMoveBetweenFileSystems(MemoryFileSystem sourceFileSystem, MemoryFileSystem targetFileSystem, AbstractPath source, AbstractPath target, TwoPathOperation operation, CopyOption... options) throws IOException { EndPointCopyContext sourceContext = sourceFileSystem.buildEndpointCopyContext(source); EndPointCopyContext targetContext = targetFileSystem.buildEndpointCopyContext(target); int order = orderFileSystems(sourceContext, targetContext); final CopyContext copyContext = buildCopyContext(sourceContext, targetContext, operation, options, order); AbstractPath firstParent = copyContext.first.parent; final AbstractPath secondParent = copyContext.second.parent; if (firstParent == null || secondParent == null) { throw new FileSystemException(toStringOrNull(firstParent), toStringOrNull(secondParent), "can't move ore copy the file system root"); } MemoryDirectory firstRoot = sourceFileSystem.getRootDirectory(copyContext.first.path); final MemoryDirectory secondRoot = targetFileSystem.getRootDirectory(copyContext.second.path); copyContext.first.path.getMemoryFileSystem().withWriteLockOnLastDo(firstRoot, firstParent, copyContext.firstFollowSymLinks, firstDirectory -> { copyContext.second.path.getMemoryFileSystem().withWriteLockOnLastDo(secondRoot, secondParent, copyContext.secondFollowSymLinks, secondDirectory -> { handleTwoPathOperation(copyContext, firstDirectory, secondDirectory); return null; }); return null; }); } private static String toStringOrNull(AbstractPath path) { return Objects.toString(path, null); } private int orderPaths(EndPointCopyContext source, EndPointCopyContext target) { int parentOrder; if (source.parent == null) { parentOrder = target.parent == null ? 0 : -1; } else if (target.parent == null) { parentOrder = 1; } else { parentOrder = source.parent.compareTo(target.parent); } if (parentOrder != 0) { return parentOrder; } else { if (source.elementName == null) { return target.elementName == null ? 0 : -1; } else if (target.elementName == null) { return 1; } else { return MemoryFileSystem.this.collator.compare(source.elementName, target.elementName); } } } private static int orderFileSystems(EndPointCopyContext source, EndPointCopyContext target) { MemoryFileSystem sourceFileSystem = source.path.getMemoryFileSystem(); MemoryFileSystem targetFileSystem = target.path.getMemoryFileSystem(); String sourceKey = sourceFileSystem.getKey(); String targetKey = targetFileSystem.getKey(); int comparison = sourceKey.compareTo(targetKey); if (comparison != 0) { return comparison; } else { throw new AssertionError("the two file system keys " + sourceKey + " and " + targetKey + " compare equal."); } } private EndPointCopyContext buildEndpointCopyContext(AbstractPath path) { AbstractPath absolutePath = (AbstractPath) path.toAbsolutePath().normalize(); if (absolutePath.isRoot()) { return new EndPointCopyContext(absolutePath, null, null); } else { ElementPath elementPath = (ElementPath) absolutePath; AbstractPath parent = (AbstractPath) elementPath.getParent(); String elementName = elementPath.getLastNameElement(); return new EndPointCopyContext(elementPath, parent, elementName); } } static final class EndPointCopyContext { final AbstractPath path; final AbstractPath parent; final String elementName; EndPointCopyContext(AbstractPath path, AbstractPath parent, String elementName) { this.path = path; this.parent = parent; this.elementName = elementName; } } private static CopyContext buildCopyContext(EndPointCopyContext source, EndPointCopyContext target, TwoPathOperation operation, CopyOption[] options, int order) { boolean followSymLinks = Options.isFollowSymLinks(options); boolean replaceExisting = Options.isReplaceExisting(options); boolean copyAttributes = Options.isCopyAttributes(options); EndPointCopyContext first; EndPointCopyContext second; boolean firstFollowSymLinks; boolean secondFollowSymLinks; boolean inverted; if (order <= 0) { first = source; second = target; firstFollowSymLinks = followSymLinks; secondFollowSymLinks = false; inverted = false; } else { first = target; second = source; firstFollowSymLinks = false; secondFollowSymLinks = followSymLinks; inverted = true; } return new CopyContext(operation, source, target, first, second, firstFollowSymLinks, secondFollowSymLinks, inverted, replaceExisting, copyAttributes); } static final class CopyContext { final EndPointCopyContext source; final EndPointCopyContext target; final EndPointCopyContext first; final EndPointCopyContext second; final boolean firstFollowSymLinks; final boolean secondFollowSymLinks; private final boolean inverted; final boolean replaceExisting; final boolean copyAttributes; final TwoPathOperation operation; CopyContext(TwoPathOperation operation, EndPointCopyContext source, EndPointCopyContext target, EndPointCopyContext first, EndPointCopyContext second, boolean firstFollowSymLinks, boolean secondFollowSymLinks, boolean inverted, boolean replaceExisting, boolean copyAttributes) { this.operation = operation; this.source = source; this.target = target; this.first = first; this.second = second; this.firstFollowSymLinks = firstFollowSymLinks; this.secondFollowSymLinks = secondFollowSymLinks; this.inverted = inverted; this.replaceExisting = replaceExisting; this.copyAttributes = copyAttributes; } boolean isSourceFollowSymLinks() { if (this.inverted) { return this.secondFollowSymLinks; } else { return this.firstFollowSymLinks; } } MemoryDirectory getSourceParent(MemoryDirectory firstDirectory, MemoryDirectory secondDirectory) { if (!this.inverted) { return firstDirectory; } else { return secondDirectory; } } MemoryDirectory getTargetParent(MemoryDirectory firstDirectory, MemoryDirectory secondDirectory) { if (!this.inverted) { return secondDirectory; } else { return firstDirectory; } } } void delete(final AbstractPath abstractPath) throws IOException { try (AutoRelease autoRelease = autoRelease(this.pathOrderingLock.readLock())) { AbstractPath absolutePath = (AbstractPath) abstractPath.toAbsolutePath().normalize(); if (absolutePath.isRoot()) { throw new FileSystemException(abstractPath.toString(), null, "can not delete root"); } final ElementPath elementPath = (ElementPath) absolutePath; final AbstractPath parent = (AbstractPath) elementPath.getParent(); this.accessDirectoryWriting(parent, true, directory -> { String fileName = elementPath.getLastNameElement(); String key = MemoryFileSystem.this.lookUpTransformer.transform(fileName); MemoryEntry child = directory.getEntryOrException(key, abstractPath); try (AutoRelease lock = child.writeLock()) { if (child instanceof MemoryDirectory) { MemoryDirectory childDirectory = (MemoryDirectory) child; childDirectory.checkEmpty(abstractPath); } if (child instanceof MemoryFile) { MemoryFile file = (MemoryFile) child; if (file.openCount() > 0) { throw new FileSystemException(abstractPath.toString(), null, "file still open"); } file.markForDeletion(); } directory.checkAccess(WRITE); directory.removeEntry(key); } return null; }); } } @Override public FileSystemProvider provider() { this.checker.check(); return this.provider; } @Override @javax.annotation.PreDestroy @jakarta.annotation.PreDestroy public void close() { // avoid throws IOException // https://github.com/marschall/memoryfilesystem/issues/76 if (this.checker.close()) { // closing twice is explicitly allowed by the contract this.checker.close(); this.provider.close(this); } } @Override public boolean isOpen() { return this.checker.isOpen(); } @Override public boolean isReadOnly() { this.checker.check(); return this.store.isReadOnly(); } @Override public String getSeparator() { this.checker.check(); return this.separator; } @Override public Iterable getRootDirectories() { this.checker.check(); // this is fine because the iterator does not support modification return (Iterable) ((Object) this.roots.keySet()); } @Override public Iterable getFileStores() { this.checker.check(); return this.stores; } @Override public Set supportedFileAttributeViews() { this.checker.check(); return this.supportedFileAttributeViews; } @Override public AbstractPath getPath(String first, String... more) { this.checker.check(); // TODO check for maximum length return this.pathParser.parse(this.rootByKey, first, more); } AbstractPath getPathFromUri(String uri) { this.checker.check(); // TODO check for maximum length return this.pathParser.parseUri(this.rootByKey, uri); } @Override public PathMatcher getPathMatcher(String syntaxAndPattern) { this.checker.check(); int colonIndex = syntaxAndPattern.indexOf(':'); if (colonIndex <= 0 || colonIndex == syntaxAndPattern.length() - 1) { throw new IllegalArgumentException("syntaxAndPattern must have form \"syntax:pattern\" but was \"" + syntaxAndPattern + "\""); } String syntax = syntaxAndPattern.substring(0, colonIndex); String pattern = syntaxAndPattern.substring(colonIndex + 1); if (syntax.equalsIgnoreCase(GlobPathMatcher.name())) { return this.pathParser.transpileGlob(pattern, this.lookUpTransformer.getRegexFlags()); } if (syntax.equalsIgnoreCase(RegexPathMatcher.name())) { return this.pathParser.compileRegex(pattern, this.lookUpTransformer.getRegexFlags()); } throw new UnsupportedOperationException("unsupported syntax \"" + syntax + "\""); } @Override public MemoryUserPrincipalLookupService getUserPrincipalLookupService() { this.checker.check(); return this.userPrincipalLookupService; } @Override public WatchService newWatchService() { this.checker.check(); // TODO make configurable if (true) { throw new UnsupportedOperationException(); } return new MemoryFileSystemWatchService(this); } void register(MemoryWatchKey watchKey) { this.checker.check(); AbsolutePath absolutePath = (AbsolutePath) watchKey.watchable().toAbsolutePath(); List keys = this.watchKeys.get(absolutePath); if (keys == null) { keys = new CopyOnWriteArrayList<>(); List previous = this.watchKeys.putIfAbsent(absolutePath, keys); if (previous != null) { keys = previous; } } keys.add(watchKey); } FileStore getFileStore() { return this.store; } Collator getCollator() { return this.collator; } @Override public Instant truncate(Instant instant) { if (instant == null || this.resolution == null) { return instant; } return instant.truncatedTo(this.resolution); } @Override public UserPrincipal getDefaultUser() { return this.getUserPrincipalLookupService().getDefaultUser(); } boolean isHidden(AbstractPath abstractPath) throws IOException { if (this.supportedFileAttributeViews.contains(FileAttributeViews.POSIX)) { return this.accessFileReading(abstractPath, false, entry -> { // Posix seems to check only the file name String originalName = entry.getOriginalName(); return !originalName.isEmpty() && originalName.charAt(0) == '.'; }); } else if (this.supportedFileAttributeViews.contains(FileAttributeViews.DOS)) { return this.readAttributes(abstractPath, DosFileAttributes.class).isHidden(); } else { return false; } } private MemoryEntry copyEntry(Path absoluteTargetPath, MemoryEntry sourceEntry, String targetElementName) throws IOException { if (sourceEntry instanceof MemoryFile) { MemoryFile sourceFile = (MemoryFile) sourceEntry; try (AutoRelease lock = sourceFile.readLock()) { EntryCreationContext context = this.newEntryCreationContext(absoluteTargetPath, NO_FILE_ATTRIBUTES); return new MemoryFile(targetElementName, context, sourceFile); } } else { if (sourceEntry instanceof MemoryDirectory) { MemoryDirectory sourceDirectory = (MemoryDirectory) sourceEntry; try (AutoRelease lock = sourceDirectory.readLock()) { EntryCreationContext context = MemoryFileSystem.this.newEntryCreationContext(absoluteTargetPath, NO_FILE_ATTRIBUTES); return new MemoryDirectory(targetElementName, context); } } else if (sourceEntry instanceof MemorySymbolicLink) { MemorySymbolicLink sourceLink = (MemorySymbolicLink) sourceEntry; try (AutoRelease lock = sourceLink.readLock()) { EntryCreationContext context = MemoryFileSystem.this.newEntryCreationContext(absoluteTargetPath, NO_FILE_ATTRIBUTES); return new MemorySymbolicLink(targetElementName, sourceLink.getTarget(), context); } } else { throw new AssertionError("unknown entry type:" + sourceEntry); } } } Path readSymbolicLink(final AbstractPath path) throws IOException { // look up the parent following symlinks // then look up the child not following symlinks AbstractPath parent = (AbstractPath) path.toAbsolutePath().getParent(); return this.accessFileReading(parent, true, parentEntry -> { if (!(parentEntry instanceof MemoryDirectory)) { throw new FileSystemException(path.toString(), null, "parent is not a directory"); } MemoryDirectory directory = (MemoryDirectory) parentEntry; return MemoryFileSystem.this.withReadLockDo(directory, (AbstractPath) path.getFileName(), false, entry -> { if (!(entry instanceof MemorySymbolicLink)) { throw new NotLinkException("file is not a symbolic link"); } return ((MemorySymbolicLink) entry).getTarget(); }); }); } static void handleTwoPathOperation(CopyContext copyContext, MemoryDirectory firstDirectory, MemoryDirectory secondDirectory) throws IOException { EndPointCopyContext sourceContext = copyContext.source; EndPointCopyContext targetContext = copyContext.target; MemoryDirectory sourceParent = copyContext.getSourceParent(firstDirectory, secondDirectory); MemoryDirectory targetParent = copyContext.getTargetParent(firstDirectory, secondDirectory); StringTransformer sourceTransformer = sourceContext.path.getMemoryFileSystem().lookUpTransformer; String sourceElementName = sourceTransformer.transform(sourceContext.elementName); MemoryEntry sourceEntry = sourceParent.getEntryOrException(sourceElementName, sourceContext.path); StringTransformer targetTransformer = targetContext.path.getMemoryFileSystem().lookUpTransformer; String targetElementName = targetTransformer.transform(targetContext.elementName); MemoryEntry targetEntry = targetParent.getEntry(targetElementName); if (sourceEntry == targetEntry) { // source and target are the same, do nothing // the way I read Files#copy this is the intention of the spec return; } if (sourceEntry == targetParent) { // copy or move "folder" -> "folder/sub" throw new FileSystemException(sourceContext.path.toString(), targetContext.path.toString(), "invalid argument"); } // have to check permission first targetParent.checkAccess(WRITE); if (copyContext.operation.isMove()) { sourceParent.checkAccess(WRITE); } if (targetEntry != null) { if (!copyContext.replaceExisting) { throw new FileAlreadyExistsException(targetContext.path.toString()); } if (targetEntry instanceof MemoryDirectory) { MemoryDirectory targetDirectory = (MemoryDirectory) targetEntry; try (AutoRelease lock = targetDirectory.readLock()) { targetDirectory.checkEmpty(targetContext.path); } } // TODO target should become symlink targetParent.removeEntry(targetElementName); } if (copyContext.operation.isMove()) { sourceParent.removeEntry(sourceElementName); targetParent.addEntry(targetElementName, sourceEntry, copyContext.target.path); String newOriginalName = targetContext.path.getMemoryFileSystem().storeTransformer.transform(targetContext.elementName); sourceEntry.setOriginalName(newOriginalName); } else { MemoryEntry toCopy = getCopySource(copyContext, sourceEntry); MemoryEntry copy = targetContext.path.getMemoryFileSystem().copyEntry(targetContext.path, toCopy, targetElementName); if (copyContext.copyAttributes) { copy.initializeAttributes(toCopy); } targetParent.addEntry(targetElementName, copy, copyContext.target.path); } } private static MemoryEntry getCopySource(CopyContext copyContext, MemoryEntry sourceEntry) throws IOException { MemoryEntry toCopy; if (sourceEntry instanceof MemorySymbolicLink && copyContext.isSourceFollowSymLinks()) { AbstractPath linkTarget = ((MemorySymbolicLink) sourceEntry).getTarget(); // TODO requires reentrant lock, should build return value object MemoryFileSystem sourceFileSystem = copyContext.source.path.getFileSystem(); AbstractPath lookupPath; if (linkTarget.isAbsolute()) { lookupPath = linkTarget; } else { lookupPath = (AbstractPath) copyContext.source.parent.resolve(linkTarget); } toCopy = sourceFileSystem.getFile(lookupPath, NO_OPEN_OPTIONS, NO_FILE_ATTRIBUTES); } else { toCopy = sourceEntry; } return toCopy; } @Override public String toString() { return MemoryFileSystem.class.getSimpleName() + '[' + this.key + ']'; } static class LazyFileAttributeView implements InvocationHandler { static final AtomicReferenceFieldUpdater ATTRIBUTE_VIEW_UPDATER = AtomicReferenceFieldUpdater.newUpdater(LazyFileAttributeView.class, FileAttributeView.class, "attributeView"); private final AbstractPath path; private final LinkOption[] options; private final Class type; @SuppressWarnings("unused") // ATTRIBUTE_VIEW_UPDATER private volatile FileAttributeView attributeView; LazyFileAttributeView(AbstractPath path, Class type, LinkOption... options) { this.path = path; this.options = options; this.type = type; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); switch (methodName) { case "name": if (args != null && args.length > 0) { throw new AssertionError("#name() not expected to have any arguments"); } return FileAttributeViews.mapAttributeView(this.type); case "toString": if (args != null && args.length > 0) { throw new AssertionError("#toString() not expected to have any arguments"); } return this.type.toString(); case "equals": if (args == null || args.length != 1) { throw new AssertionError("#equals() expected to exactly one argument"); } return proxy == args[0]; case "hashCode": if (args != null && args.length > 0) { throw new AssertionError("#hashCode() not expected to have any arguments"); } return System.identityHashCode(proxy); default: try { return method.invoke(this.getView(), args); } catch (InvocationTargetException e) { throw e.getCause(); } } } private FileAttributeView getView() throws IOException { FileAttributeView view = ATTRIBUTE_VIEW_UPDATER.get(this); if (view != null) { return view; } else { FileAttributeView newValue = this.path.getMemoryFileSystem().getFileAttributeView(this.path, this.type, this.options); boolean successful = ATTRIBUTE_VIEW_UPDATER.compareAndSet(this, null, newValue); if (successful) { return newValue; } else { return ATTRIBUTE_VIEW_UPDATER.get(this); } } } } @FunctionalInterface interface MemoryEntryBlock { R value(MemoryEntry entry) throws IOException; } @FunctionalInterface interface MemoryDirectoryBlock { R value(MemoryDirectory entry) throws IOException; } @FunctionalInterface interface MemoryEntryCreator { MemoryEntry create(String name) throws IOException; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy