/* * 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.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.terasology.module.Module; import org.terasology.naming.Name; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.ProviderMismatchException; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; /** * The Path implementation used by ModuleFileSystem. * * @author Immortius */ class ModulePath implements Path { private static final String SAME_DIR_INDICATOR = "."; private static final String PREVIOUS_DIR_INDICATOR = ".."; private final String path; private final ModuleFileSystem fileSystem; ModulePath(String path, ModuleFileSystem fileSystem) { Preconditions.checkNotNull(path); Preconditions.checkNotNull(fileSystem); this.path = path; this.fileSystem = fileSystem; } @Override public ModuleFileSystem getFileSystem() { return fileSystem; } @Override public boolean isAbsolute() { return !path.isEmpty() && path.startsWith(fileSystem.getSeparator()); } @Override public ModulePath getRoot() { if (isAbsolute()) { return new ModulePath(fileSystem.getSeparator(), fileSystem); } return null; } @Override public ModulePath getFileName() { if (path.isEmpty()) { return this; } if (path.equals(fileSystem.getSeparator())) { return null; } List<String> parts = getPathPartsExcludingRoot(); return new ModulePath(parts.get(parts.size() - 1), fileSystem); } @Override public ModulePath getParent() { List<String> parts = getPathPartsIncludingRoot(); if (parts.size() <= 1) { return null; } return newPathFromParts(fileSystem, parts, 0, parts.size() - 1); } @Override public int getNameCount() { return getPathPartsExcludingRoot().size(); } @Override public ModulePath getName(int index) { List<String> parts = getPathPartsExcludingRoot(); if (index < 0 || index >= parts.size()) { throw new IllegalArgumentException("Index out of bounds, '" + index + "' not in range 0 <= index < " + parts.size()); } return fileSystem.getPath(parts.get(index)); } @Override public ModulePath subpath(int beginIndex, int endIndex) { List<String> parts = getPathPartsExcludingRoot(); if (beginIndex < 0 || beginIndex >= parts.size()) { throw new IllegalArgumentException("beginIndex out of bounds: '" + beginIndex + "' not in range 0 <= beginIndex < " + parts.size()); } if (endIndex <= beginIndex || endIndex > parts.size()) { throw new IllegalArgumentException("endIndex out of bounds: '" + endIndex + "' not in range " + beginIndex + " < endIndex <= " + parts.size()); } return newPathFromParts(fileSystem, parts, beginIndex, endIndex); } @Override public boolean startsWith(Path other) { if (other instanceof ModulePath) { ModulePath otherModulePath = (ModulePath) other; if (!fileSystem.equals(otherModulePath.getFileSystem()) || isAbsolute() != other.isAbsolute()) { return false; } List<String> parts = getPathPartsExcludingRoot(); List<String> otherParts = otherModulePath.getPathPartsExcludingRoot(); if (otherParts.size() > parts.size()) { return false; } for (int i = 0; i < otherParts.size(); ++i) { if (!parts.get(i).equals(otherParts.get(i))) { return false; } } return true; } return false; } @Override public boolean startsWith(String other) { return startsWith(fileSystem.getPath(other)); } @Override public boolean endsWith(Path other) { if (other instanceof ModulePath) { ModulePath otherModulePath = (ModulePath) other; if (!fileSystem.equals(otherModulePath.getFileSystem())) { return false; } List<String> parts = getPathPartsIncludingRoot(); List<String> otherParts = otherModulePath.getPathPartsIncludingRoot(); if (otherParts.size() > parts.size()) { return false; } int offset = parts.size() - otherParts.size(); for (int i = 0; i < otherParts.size(); ++i) { if (!parts.get(i + offset).equals(otherParts.get(i))) { return false; } } return true; } return false; } @Override public boolean endsWith(String other) { return endsWith(fileSystem.getPath(other)); } @Override public ModulePath normalize() { List<String> parts = getPathPartsIncludingRoot(); List<String> normalisedParts = Lists.newArrayListWithCapacity(parts.size()); for (String part : parts) { if (part.equals(PREVIOUS_DIR_INDICATOR)) { if (normalisedParts.isEmpty() || PREVIOUS_DIR_INDICATOR.equals(normalisedParts.get(normalisedParts.size() - 1))) { normalisedParts.add(part); } else if (!fileSystem.getSeparator().equals(normalisedParts.get(normalisedParts.size() - 1))) { normalisedParts.remove(normalisedParts.size() - 1); } } else if (!SAME_DIR_INDICATOR.equals(part)) { normalisedParts.add(part); } } if (normalisedParts.size() == parts.size()) { return this; } else if (normalisedParts.size() == 0) { return fileSystem.getPath(""); } return newPathFromParts(fileSystem, normalisedParts); } @Override public ModulePath resolve(Path other) { if (other instanceof ModulePath && fileSystem.equals(other.getFileSystem())) { if (other.isAbsolute()) { return (ModulePath) other; } List<String> newParts = getPathPartsIncludingRoot(); newParts.addAll(((ModulePath) other).getPathPartsExcludingRoot()); return newPathFromParts(fileSystem, newParts); } else { throw new IllegalArgumentException("Cannot resolve path from another file system"); } } @Override public ModulePath resolve(String other) { return resolve(fileSystem.getPath(other)); } @Override public Path resolveSibling(Path other) { Path parentPath = getParent(); if (parentPath == null || other.isAbsolute()) { return other; } return parentPath.resolve(other); } @Override public Path resolveSibling(String other) { return resolveSibling(fileSystem.getPath(other)); } @Override public ModulePath relativize(Path other) { if (other instanceof ModulePath && other.getFileSystem().equals(fileSystem)) { ModulePath otherModulePath = (ModulePath) other; List<String> parts = normalize().getPathPartsExcludingRoot(); List<String> otherParts = otherModulePath.normalize().getPathPartsExcludingRoot(); int commonPathEnd = 0; while (commonPathEnd < parts.size() && commonPathEnd < otherParts.size() && parts.get(commonPathEnd).equals(otherParts.get(commonPathEnd))) { commonPathEnd++; } List<String> relativizedPath = Lists.newArrayList(); for (int i = commonPathEnd; i < parts.size(); ++i) { relativizedPath.add(".."); } if (commonPathEnd < otherParts.size()) { relativizedPath.addAll(otherParts.subList(commonPathEnd, otherParts.size())); } if (relativizedPath.isEmpty()) { relativizedPath.add(""); } return newPathFromParts(fileSystem, relativizedPath); } else { throw new IllegalArgumentException("Cannot relativize path from another file system"); } } @Override public URI toUri() { throw new UnsupportedOperationException("Module filesystem does not support URIs"); } @Override public ModulePath toAbsolutePath() { if (isAbsolute()) { return this; } else { return fileSystem.getPath("/").resolve(this); } } @Override public Path toRealPath(LinkOption... options) throws IOException { Optional<Module> module = getModule(); if (!module.isPresent()) { throw new IOException("Path does not exist: " + toString()); } ModulePath normalisedPath = toAbsolutePath().normalize(); for (Path location : module.get().getLocations()) { if (Files.isDirectory(location)) { Path actualLocation = applyModulePathToActual(location, normalisedPath); if (Files.exists(actualLocation)) { return convertFromActualToModulePath(getFileSystem(), module.get().getId().toString(), location, actualLocation); } } else if (Files.isRegularFile(location)) { FileSystem moduleArchive = fileSystem.getRealFileSystem(location); for (Path archiveRoot : moduleArchive.getRootDirectories()) { Path actualLocation = applyModulePathToActual(archiveRoot, normalisedPath); if (Files.exists(actualLocation)) { return convertFromActualToModulePath(getFileSystem(), module.get().getId().toString(), archiveRoot, actualLocation); } } } } throw new IOException("Path does not exist: " + toString()); } @Override public File toFile() { throw new UnsupportedOperationException("ModulePath does not support toFile()"); } @Override public WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException { if (watcher instanceof ModuleWatchService) { ModuleWatchService moduleWatcher = (ModuleWatchService) watcher; return moduleWatcher.register(this, events, modifiers); } else { throw new ProviderMismatchException("WatchService belongs to a different file system"); } } @Override public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException { return register(watcher, events, new WatchEvent.Modifier[0]); } @Override public Iterator<Path> iterator() { return Collections2.transform(getPathPartsExcludingRoot(), new Function<String, Path>() { @Nullable @Override public Path apply(String input) { return fileSystem.getPath(input); } }).iterator(); } @Override public int compareTo(Path other) { if (!(other instanceof ModulePath) || !((ModulePath) other).fileSystem.equals(fileSystem)) { throw new IllegalArgumentException("Cannot compare with path from another file system"); } ModulePath otherModulePath = (ModulePath) other; return path.compareTo(otherModulePath.path); } @Override public String toString() { return path; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof ModulePath) { ModulePath other = (ModulePath) obj; return other.fileSystem.equals(fileSystem) && this.compareTo(other) == 0; } return false; } @Override public int hashCode() { return Objects.hash(fileSystem, path.toLowerCase(Locale.ENGLISH)); } Optional<Module> getModule() { ModulePath normalisedPath = toAbsolutePath().normalize(); if (normalisedPath.getNameCount() > 0) { return Optional.ofNullable(fileSystem.getEnvironment().get(new Name(normalisedPath.getName(0).toString()))); } return Optional.empty(); } /** * @return A list of the name components of the path, including the root */ List<String> getPathPartsIncludingRoot() { String[] parts = path.split(fileSystem.getSeparator(), 0); if (isAbsolute()) { List<String> result = Lists.newArrayListWithCapacity(parts.length); result.add("/"); if (parts.length > 1) { result.addAll(Arrays.asList(parts).subList(1, parts.length)); } return result; } else { return Lists.newArrayList(parts); } } /** * @return A list of the name components of the path, excluding the root */ List<String> getPathPartsExcludingRoot() { String[] parts = path.split(fileSystem.getSeparator(), 0); if (isAbsolute()) { if (parts.length > 1) { return Arrays.asList(parts).subList(1, parts.length); } else { return Collections.emptyList(); } } else { return Lists.newArrayList(parts); } } /** * Provides the underlying path (from one of the underlying filesystems/locations composing the module) for this path. * If this path doesn't exist, returns null. * If multiple paths exist, the first one discovered is returned. * * @return The real, underlying path for this path, or null if it doesn't exist. * @throws IOException */ Optional<Path> getUnderlyingPath() throws IOException { Optional<Module> module = getModule(); if (!module.isPresent()) { return Optional.empty(); } ModulePath normalisedPath = toAbsolutePath().normalize(); for (Path location : module.get().getLocations()) { if (Files.isDirectory(location)) { Path actualLocation = applyModulePathToActual(location, normalisedPath); if (Files.exists(actualLocation)) { return Optional.of(actualLocation); } } else if (Files.isRegularFile(location)) { FileSystem moduleArchive = fileSystem.getRealFileSystem(location); for (Path archiveRoot : moduleArchive.getRootDirectories()) { Path actualLocation = applyModulePathToActual(archiveRoot, normalisedPath); if (Files.exists(actualLocation)) { return Optional.of(actualLocation); } } } } return Optional.empty(); } /** * Provides all the underlying paths (from one of the underlying filesystems/locations composing the module) for this path. * * @return A set of all the underlying paths from all locations composing the module * @throws IOException */ Set<Path> getUnderlyingPaths() throws IOException { Set<Path> underlyingPaths = Sets.newLinkedHashSet(); Optional<Module> module = getModule(); if (!module.isPresent()) { return underlyingPaths; } ModulePath normalisedPath = toAbsolutePath().normalize(); for (Path location : module.get().getLocations()) { if (Files.isDirectory(location)) { Path actualLocation = applyModulePathToActual(location, normalisedPath); if (Files.exists(actualLocation)) { underlyingPaths.add(actualLocation); } } else if (Files.isRegularFile(location)) { FileSystem moduleArchive = fileSystem.getRealFileSystem(location); for (Path archiveRoot : moduleArchive.getRootDirectories()) { Path actualLocation = applyModulePathToActual(archiveRoot, normalisedPath); if (Files.exists(actualLocation)) { underlyingPaths.add(actualLocation); } } } } return underlyingPaths; } /** * Applies a modulePath on top of an external path - used to produce the underlying path. * * @param actualRoot The root path in a different filesystem * @param modulePath The module path to convert to a real path * @return The real path. This will be the actualRoot with the name parts of the modulePath appended. */ static Path applyModulePathToActual(Path actualRoot, ModulePath modulePath) { Path result = actualRoot; List<String> parts = modulePath.getPathPartsExcludingRoot(); if (parts.size() < 2) { return result; } for (String part : parts.subList(1, parts.size())) { result = result.resolve(part); } return result; } /** * @param fileSystem The fileSystem the produced path will belong to * @param parts The parts to construct the path from * @return A new module path belonging to the given fileSystem and with the given name parts */ static ModulePath newPathFromParts(ModuleFileSystem fileSystem, List<String> parts) { return newPathFromParts(fileSystem, parts, 0, parts.size()); } /** * @param fileSystem The fileSystem the produced path will belong to * @param parts The parts to construct the path from * @return A new module path belonging to the given fileSystem and with the given subset of name parts */ static ModulePath newPathFromParts(ModuleFileSystem fileSystem, List<String> parts, int beginIndex, int endIndex) { String first = parts.get(beginIndex); String[] remainder = new String[endIndex - beginIndex - 1]; for (int i = 0; i < remainder.length; ++i) { remainder[i] = parts.get(beginIndex + i + 1); } return fileSystem.getPath(first, remainder); } /** * @param fileSystem The module filesystem the new path should belong to * @param root The root path the actual path is relative to * @param actualPath The path to convert * @return The module path equivalent of a given path * @throws IOException */ static Path convertFromActualToModulePath(ModuleFileSystem fileSystem, String moduleName, Path root, Path actualPath) throws IOException { Path actualRealPath = actualPath.toRealPath(); Path relative = root.toRealPath().relativize(actualRealPath); List<String> pathParts = Lists.newArrayListWithCapacity(relative.getNameCount() + 1); pathParts.add(ModuleFileSystemProvider.SEPARATOR); pathParts.add(moduleName); for (Path part : relative) { pathParts.add(part.toString()); } return ModulePath.newPathFromParts(fileSystem, pathParts); } }