package com.github.marschall.memoryfilesystem;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.SeekableByteChannel;
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.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.spi.FileSystemProvider;
import java.text.Collator;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* Creates memory file systems instances.
*
* <p>This class should not be used directly. Instead
* {@link java.nio.file.FileSystems#newFileSystem(URI, Map)}
* should be used to create instances.</p>
*/
public final class MemoryFileSystemProvider extends FileSystemProvider {
static final String SCHEME = "memory";
private final ConcurrentMap<String, MemoryFileSystem> fileSystems;
private final ExecutorService workExecutor;
private final ExecutorService callbackExecutor;
public MemoryFileSystemProvider() {
this.fileSystems = new ConcurrentHashMap<>();
this.workExecutor = Executors.newFixedThreadPool(1, new NamedDaemonThreadFactory("memory-file-system-worker"));
this.callbackExecutor = Executors.newFixedThreadPool(1, new NamedDaemonThreadFactory("memory-file-system-callback"));
}
@Override
public String getScheme() {
return SCHEME;
}
@Override
public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
this.valideUri(uri);
String key = this.getFileSystemKey(uri);
EnvironmentParser parser = new EnvironmentParser(env);
MemoryFileSystem fileSystem = this.createNewFileSystem(key, parser);
MemoryFileSystem previous = this.fileSystems.putIfAbsent(key, fileSystem);
if (previous != null) {
String message = "File system " + uri.getScheme() + ':' + key + " already exists";
throw new FileSystemAlreadyExistsException(message);
} else {
return fileSystem;
}
}
private void valideUri(URI uri) {
String schemeSpecificPart = uri.getSchemeSpecificPart();
if (schemeSpecificPart.isEmpty()) {
throw new IllegalArgumentException("scheme specific part must not be empty");
}
String host = uri.getHost();
if (host != null) {
throw new IllegalArgumentException("host must not be set");
}
String authority = uri.getAuthority();
if (authority != null) {
throw new IllegalArgumentException("authority must not be set");
}
String userInfo = uri.getUserInfo();
if (userInfo != null) {
throw new IllegalArgumentException("userInfo must not be set");
}
int port = uri.getPort();
if (port != -1) {
throw new IllegalArgumentException("port must not be set");
}
String path = uri.getPath();
if (path != null) {
throw new IllegalArgumentException("path must not be set");
}
String query = uri.getQuery();
if (query != null) {
throw new IllegalArgumentException("query must not be set");
}
String fragment = uri.getFragment();
if (fragment != null) {
throw new IllegalArgumentException("fragment must not be set");
}
}
private MemoryFileSystem createNewFileSystem(String key, EnvironmentParser parser) throws IOException {
ClosedFileSystemChecker checker = new ClosedFileSystemChecker();
String separator = parser.getSeparator();
StringTransformer storeTransformer = parser.getStoreTransformer();
StringTransformer lookUpTransformer = parser.getLookUpTransformer();
Collator collator = parser.getCollator();
MemoryFileStore memoryStore = new MemoryFileStore(key, checker);
Set<Class<? extends FileAttributeView>> additionalViews = parser.getAdditionalViews();
MemoryUserPrincipalLookupService userPrincipalLookupService = this.createUserPrincipalLookupService(parser, checker);
PathParser pathParser = this.buildPathParser(parser);
Set<PosixFilePermission> umask = parser.getUmask();
if (!additionalViews.contains(PosixFileAttributeView.class)) {
umask = Collections.emptySet();
}
MemoryFileSystem fileSystem = new MemoryFileSystem(key, separator, pathParser, this, memoryStore,
userPrincipalLookupService, checker, storeTransformer, lookUpTransformer, collator, additionalViews, umask);
fileSystem.setRootDirectories(this.buildRootsDirectories(parser, fileSystem, additionalViews, umask));
String defaultDirectory = parser.getDefaultDirectory();
fileSystem.setCurrentWorkingDirectory(defaultDirectory);
AbstractPath defaultPath = fileSystem.getDefaultPath();
if (!defaultPath.isRoot()) {
// TODO configure owner and permissions
Files.createDirectories(defaultPath);
}
return fileSystem;
}
private MemoryUserPrincipalLookupService createUserPrincipalLookupService(EnvironmentParser parser,
ClosedFileSystemChecker checker) {
List<String> userNames = parser.getUserNames();
List<String> groupNames = parser.getGroupNames();
StringTransformer nameTransfomer = parser.getPrincipalNameTransfomer();
return new MemoryUserPrincipalLookupService(userNames, groupNames, nameTransfomer, checker);
}
@Override
public FileSystem getFileSystem(URI uri) {
String key = this.getFileSystemKey(uri);
FileSystem fileSystem = this.fileSystems.get(key);
if (fileSystem == null) {
String message = "File system " + uri.getScheme() + ':' + key + " does not exist";
throw new FileSystemNotFoundException(message);
}
return fileSystem;
}
private PathParser buildPathParser(EnvironmentParser parser) {
String separator = parser.getSeparator();
if (parser.isSingleEmptyRoot()) {
return new SingleEmptyRootPathParser(separator, parser.getForbiddenCharacters());
} else {
return new MultipleNamedRootsPathParser(separator, parser.getStoreTransformer(), parser.getForbiddenCharacters());
}
}
private Map<Root, MemoryDirectory> buildRootsDirectories(EnvironmentParser parser,
MemoryFileSystem fileSystem, Set<Class<? extends FileAttributeView>> additionalViews,
Set<PosixFilePermission> perms) throws IOException {
final FileAttribute<?>[] attributes = new FileAttribute<?>[]{ PosixFilePermissions.asFileAttribute(perms) };
if (parser.isSingleEmptyRoot()) {
Root root = new EmptyRoot(fileSystem);
EntryCreationContext context = fileSystem.newEntryCreationContext(root, attributes);
MemoryDirectory directory = new MemoryDirectory("", context);
directory.initializeRoot();
return Collections.singletonMap(root, directory);
} else {
List<String> roots = parser.getRoots();
Map<Root, MemoryDirectory> paths = new LinkedHashMap<>(roots.size());
for (String root : roots) {
NamedRoot namedRoot = new NamedRoot(fileSystem, root);
MemoryDirectory rootDirectory = new MemoryDirectory(namedRoot.getKey(), fileSystem.newEntryCreationContext(namedRoot, attributes));
rootDirectory.initializeRoot();
paths.put(namedRoot, rootDirectory);
}
return Collections.unmodifiableMap(paths);
}
}
private String getFileSystemKey(URI uri) {
String scheme = uri.getScheme();
if (!this.getScheme().equals(scheme)) {
throw new IllegalArgumentException("Requested unsupported scheme " + scheme + "only scheme: " + this.getScheme() + " is supported");
}
String schemeSpecificPart = uri.getSchemeSpecificPart();
int colonIndex = schemeSpecificPart.indexOf(":/");
if (colonIndex == -1) {
return schemeSpecificPart;
} else {
return schemeSpecificPart.substring(0, colonIndex);
}
}
private String getFileSystemPath(URI uri) {
//REVIEW check for getPath() first()?
String schemeSpecificPart = uri.getSchemeSpecificPart();
int colonIndex = schemeSpecificPart.indexOf(":/");
if (colonIndex == -1) {
return uri.getPath();
} else {
return schemeSpecificPart.substring(colonIndex + ":/".length());
}
}
@Override
public Path getPath(URI uri) {
String key = this.getFileSystemKey(uri);
MemoryFileSystem fileSystem = this.fileSystems.get(key);
if (fileSystem == null) {
throw new FileSystemNotFoundException("memory file system \"" + key + "\" not found");
}
return fileSystem.getPathFromUri(this.getFileSystemPath(uri));
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
return this.newFileChannel(path, options, attrs);
}
@Override
public BlockChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
this.checkSupported(options);
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.newFileChannel(abstractPath, options, attrs);
}
@Override
public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException {
BlockChannel fileChannel = this.newFileChannel(path, options, attrs);
return new AsynchronousBlockChannel(fileChannel,
executor != null ? executor : this.workExecutor, executor != null ? executor : this.callbackExecutor);
}
@Override
public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
this.checkSupported(options);
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.newInputStream(abstractPath, options);
}
@Override
public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
this.checkSupported(options);
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.newOutputStream(abstractPath, options);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
AbstractPath abstractPath = this.castPath(dir);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.newDirectoryStream(abstractPath, filter);
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
AbstractPath abstractPath = this.castPath(dir);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
memoryFileSystem.createDirectory(abstractPath, attrs);
}
@Override
public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
AbstractPath linkPath = this.castPath(link);
AbstractPath targetPath = this.castPath(target);
MemoryFileSystem memoryFileSystem = linkPath.getMemoryFileSystem();
if (memoryFileSystem != targetPath.getMemoryFileSystem()) {
throw new IllegalArgumentException("link and target must be on same file system");
}
memoryFileSystem.createSymbolicLink(linkPath, targetPath, attrs);
}
@Override
public void createLink(Path link, Path existing) throws IOException {
AbstractPath linkPath = this.castPath(link);
AbstractPath targetPath = this.castPath(existing);
MemoryFileSystem memoryFileSystem = linkPath.getMemoryFileSystem();
if (memoryFileSystem != targetPath.getMemoryFileSystem()) {
throw new IllegalArgumentException("link and target must be on same file system");
}
memoryFileSystem.createLink(linkPath, targetPath);
}
@Override
public Path readSymbolicLink(Path link) throws IOException {
AbstractPath linkPath = this.castPath(link);
return linkPath.getMemoryFileSystem().readSymbolicLink(linkPath);
}
@Override
public void delete(Path path) throws IOException {
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
memoryFileSystem.delete(abstractPath);
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
this.copyOrMove(source, target, TwoPathOperation.COPY, options);
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
this.copyOrMove(source, target, TwoPathOperation.MOVE, options);
}
private void copyOrMove(Path source, Path target, TwoPathOperation operation, CopyOption... options) throws IOException {
this.checkSupported(options);
AbstractPath sourcePath = this.castPath(source);
AbstractPath targetPath = this.castPath(target);
MemoryFileSystem sourceFileSystem = sourcePath.getMemoryFileSystem();
MemoryFileSystem targetFileSystem = targetPath.getMemoryFileSystem();
if (sourceFileSystem == targetFileSystem) {
sourceFileSystem.copyOrMove(sourcePath, targetPath, operation, options);
} else {
MemoryFileSystem.copyOrMoveBetweenFileSystems(sourceFileSystem, targetFileSystem, sourcePath, targetPath, operation, options);
}
}
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
FileSystemProvider provider = provider(path);
if (provider != this) {
return Files.isSameFile(path2, path);
}
FileSystemProvider provider2 = provider(path2);
if (provider2 != this) {
return false;
}
if (path.getFileSystem() != path2.getFileSystem()) {
return false;
}
if (path.equals(path2)) {
return true;
}
// isn't atomic but that's fine I guess
if (path.toRealPath().equals(path2.toRealPath())) {
return true;
}
AbstractPath abstractPath = this.castPath(path);
AbstractPath abstractPath2 = this.castPath(path2);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
// have to check for hard links
return memoryFileSystem.isSameFile(abstractPath, abstractPath2);
}
private static FileSystemProvider provider(Path path) {
return path.getFileSystem().provider();
}
@Override
public boolean isHidden(Path path) throws IOException {
AbstractPath abstractPath = this.castPath(path);
return abstractPath.getMemoryFileSystem().isHidden(abstractPath);
}
@Override
public FileStore getFileStore(Path path) throws IOException {
return this.castPath(path).getMemoryFileSystem().getFileStore();
}
private AbstractPath castPath(Path path) {
if (!(path instanceof AbstractPath)) {
throw new ProviderMismatchException("expected a path of provider " + SCHEME + " but got " + path.getFileSystem().provider().getScheme());
}
return (AbstractPath) path;
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
this.checkSupported(modes);
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
memoryFileSystem.checkAccess(abstractPath, modes);
}
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.getLazyFileAttributeView(abstractPath, type, options);
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.readAttributes(abstractPath, type, options);
}
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
return memoryFileSystem.readAttributes(abstractPath, attributes, options);
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
AbstractPath abstractPath = this.castPath(path);
MemoryFileSystem memoryFileSystem = abstractPath.getMemoryFileSystem();
memoryFileSystem.setAttribute(abstractPath, attribute, value, options);
}
void close(MemoryFileSystem fileSystem) {
String key = fileSystem.getKey();
this.fileSystems.remove(key);
}
void close() {
this.workExecutor.shutdownNow();
this.callbackExecutor.shutdownNow();
}
private void checkSupported(CopyOption... options) {
if (options == null) {
return;
}
for (CopyOption copyOption : options) {
if (copyOption != StandardCopyOption.ATOMIC_MOVE
&& copyOption != StandardCopyOption.COPY_ATTRIBUTES
&& copyOption != StandardCopyOption.REPLACE_EXISTING
&& copyOption != LinkOption.NOFOLLOW_LINKS) {
throw new UnsupportedOperationException("copy option: " + copyOption + " not supported");
}
}
}
private void checkSupported(OpenOption... options) {
// TODO implement
}
private void checkSupported(Set<? extends OpenOption> options) {
// TODO implement
}
private void checkSupported(AccessMode... modes) {
if (modes == null || modes.length == 0) {
return;
}
for (AccessMode mode : modes) {
if (!(mode == AccessMode.READ || mode == AccessMode.WRITE || mode == AccessMode.EXECUTE)) {
throw new UnsupportedOperationException("mode " + mode + " not supported");
}
}
}
static final class NamedDaemonThreadFactory implements ThreadFactory {
private final String name;
NamedDaemonThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, this.name);
thread.setDaemon(true);
return thread;
}
}
}