/* * Copyright 2015 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.module.filesystem; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.module.Module; import org.terasology.module.ModuleEnvironment; import org.terasology.naming.Name; import java.io.IOException; import java.net.URI; import java.nio.channels.SeekableByteChannel; import java.nio.file.AccessMode; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; 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.NotDirectoryException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.ProviderMismatchException; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; import java.nio.file.spi.FileSystemProvider; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.ReentrantLock; /** * A provider for ModuleFileSystems - the factory for ModuleFileSystem. * <p> * The FileSystemProvider in addition to producing FileSystems, also provides the low level methods that drive many file operations. ModuleFileSystemProvider does this * by delegating down to the underlying file systems, translating ModulePaths to real paths and back again as necessary. In situations where a file occurs in multiple * locations in a module the first one found is used. * </p><p> * The ModuleFileSystem does support WatchServices/WatchKeys, but only for locations on the default file system. If no such location exists then WatchKeys produced will be * return false from isValid on creation. * </p> * * @author Immortius */ public class ModuleFileSystemProvider extends FileSystemProvider { public static final String SCHEME = "module"; public static final String MODULE_ENVIRONMENT = "environment"; public static final String ROOT = "/"; public static final String SEPARATOR = "/"; private static final Logger logger = LoggerFactory.getLogger(ModuleFileSystemProvider.class); private ConcurrentMap<ModuleEnvironment, ModuleFileSystem> moduleFileSystems = new ConcurrentHashMap<>(); private final ReentrantLock creationLock = new ReentrantLock(); @Override public String getScheme() { return SCHEME; } @Override public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException { Object moduleEnvironmentRaw = env.get(MODULE_ENVIRONMENT); if (moduleEnvironmentRaw == null || !(moduleEnvironmentRaw instanceof ModuleEnvironment)) { throw new IllegalArgumentException("Module environment is required"); } return newFileSystem((ModuleEnvironment) moduleEnvironmentRaw); } @Override public FileSystem getFileSystem(URI uri) { throw new FileSystemNotFoundException("Module file systems cannot be discovered by URI"); } /** * Creates a new file system for the given module. * * @param environment The module environment to create a file system for * @return A new filesystem for the module environment * @throws java.nio.file.FileSystemAlreadyExistsException If a filesystem already exists for the given module environment */ public ModuleFileSystem newFileSystem(ModuleEnvironment environment) { creationLock.lock(); try { if (moduleFileSystems.containsKey(environment)) { throw new FileSystemAlreadyExistsException("Already created filesystem for '" + environment + "'"); } ModuleFileSystem newFileSystem = new ModuleFileSystem(this, environment); moduleFileSystems.put(environment, newFileSystem); return newFileSystem; } finally { creationLock.unlock(); } } @Override public Path getPath(URI uri) { throw new IllegalArgumentException("URI lookup not supported by this file system"); } @Override public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { Path underlying = getUnderlyingPath(path); return underlying.getFileSystem().provider().newByteChannel(underlying, options, attrs); } @Override public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException { ModulePath modulePath = getModulePath(dir); if (!Files.exists(modulePath) || !Files.isDirectory(modulePath)) { throw new NotDirectoryException(dir.toString()); } return new ModuleDirectoryStream(modulePath, filter); } @Override public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException { throw new UnsupportedOperationException(); } @Override public void delete(Path path) throws IOException { throw new UnsupportedOperationException(); } @Override public void copy(Path source, Path target, CopyOption... options) throws IOException { throw new UnsupportedOperationException(); } @Override public void move(Path source, Path target, CopyOption... options) throws IOException { throw new UnsupportedOperationException(); } @Override public boolean isSameFile(Path path, Path path2) throws IOException { if (Objects.equals(path, path2)) { return true; } if (path == null || path2 == null) { return false; } Path resolvedPath1 = getUnderlyingPath(path); Path resolvedPath2 = getUnderlyingPath(path2); return resolvedPath1.getFileSystem().equals(resolvedPath2.getFileSystem()) && resolvedPath1.getFileSystem().provider().isSameFile(resolvedPath1, resolvedPath2); } @Override public boolean isHidden(Path path) throws IOException { Path underlying = getUnderlyingPath(path); return underlying.getFileSystem().provider().isHidden(underlying); } @Override public FileStore getFileStore(Path path) throws IOException { throw new UnsupportedOperationException(); } @Override public void checkAccess(Path path, AccessMode... modes) throws IOException { Path underlying = getUnderlyingPath(path); underlying.getFileSystem().provider().checkAccess(underlying, modes); } @Override public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) { try { Path underlying = getUnderlyingPath(path); return underlying.getFileSystem().provider().getFileAttributeView(underlying, type, options); } catch (IOException e) { return null; } } @Override public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException { Path underlying = getUnderlyingPath(path); return underlying.getFileSystem().provider().readAttributes(underlying, type, options); } @Override public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException { Path underlying = getUnderlyingPath(path); return underlying.getFileSystem().provider().readAttributes(underlying, attributes, options); } @Override public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { throw new UnsupportedOperationException(); } /** * Called when a module file system is closed, removes it from the provider. It can be recreated if desired. * * @param moduleFileSystem The filesystem to remove */ void removeFileSystem(ModuleFileSystem moduleFileSystem) { creationLock.lock(); try { moduleFileSystems.remove(moduleFileSystem.getEnvironment()); } finally { creationLock.unlock(); } } private ModulePath getModulePath(Path path) { if (path instanceof ModulePath) { return (ModulePath) path; } throw new ProviderMismatchException(); } private Path getUnderlyingPath(Path path) throws IOException { ModulePath modulePath = getModulePath(path); Optional<Path> underlying = modulePath.getUnderlyingPath(); if (underlying.isPresent()) { return underlying.get(); } else { throw new IOException("Path does not exist: " + path.toString()); } } /** * A DirectoryStream that streams across all the locations composing a module */ private static class ModuleDirectoryStream implements DirectoryStream<Path> { private Set<Path> contents = Sets.newLinkedHashSet(); public ModuleDirectoryStream(ModulePath modulePath, Filter<? super Path> filter) throws NotDirectoryException { ModuleEnvironment moduleEnvironment = modulePath.getFileSystem().getEnvironment(); ModulePath normalisedPath = modulePath.toAbsolutePath().normalize(); String moduleName = normalisedPath.getName(0).toString(); Module module = moduleEnvironment.get(new Name(moduleName)); if (module == null) { throw new NotDirectoryException("No such module in the environment: " + moduleName); } for (Path location : module.getLocations()) { try { if (Files.isDirectory(location)) { Path actualLocation = ModulePath.applyModulePathToActual(location, normalisedPath); if (Files.exists(actualLocation)) { Filter<? super Path> wrappedFilter = new ModulePathWrappedFilter(filter, moduleName, actualLocation, modulePath.getFileSystem()); try (DirectoryStream<Path> stream = actualLocation.getFileSystem().provider().newDirectoryStream(actualLocation, wrappedFilter)) { for (Path content : stream) { contents.add(ModulePath.convertFromActualToModulePath(modulePath.getFileSystem(), moduleName, location, content)); } } } } else if (Files.isRegularFile(location)) { FileSystem moduleArchive = modulePath.getFileSystem().getRealFileSystem(location); for (Path archiveRoot : moduleArchive.getRootDirectories()) { Path actualLocation = ModulePath.applyModulePathToActual(archiveRoot, normalisedPath); if (Files.exists(actualLocation)) { Filter<? super Path> wrappedFilter = new ModulePathWrappedFilter(filter, moduleName, actualLocation, modulePath.getFileSystem()); try (DirectoryStream<Path> stream = actualLocation.getFileSystem().provider().newDirectoryStream(actualLocation, wrappedFilter)) { for (Path content : stream) { contents.add(ModulePath.convertFromActualToModulePath(modulePath.getFileSystem(), moduleName, archiveRoot, content)); } } } } } } catch (IOException e) { logger.error("Failed to access module location " + location, e); } } } @Override public Iterator<Path> iterator() { return contents.iterator(); } @Override public void close() throws IOException { } } /** * Wraps a filter provided so that it receives ModulePaths rather than Paths belonging to different file systems. */ private static class ModulePathWrappedFilter implements DirectoryStream.Filter<Path> { private DirectoryStream.Filter<? super Path> innerFilter; private Path basePath; private ModuleFileSystem moduleFileSystem; private String moduleName; public ModulePathWrappedFilter(DirectoryStream.Filter<? super Path> innerFilter, String moduleName, Path basePath, ModuleFileSystem moduleFileSystem) { this.innerFilter = innerFilter; this.basePath = basePath; this.moduleFileSystem = moduleFileSystem; this.moduleName = moduleName; } @Override public boolean accept(Path entry) throws IOException { return innerFilter.accept(ModulePath.convertFromActualToModulePath(moduleFileSystem, moduleName, basePath, entry)); } } }