// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.util.io; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; /** * Central hub for accessing game-related I/O resources. */ public class FileManager { private static FileManager instance; // Stores whether filesystems use case-sensitive filenames private final HashMap<FileSystem, Boolean> mapCaseSensitive = new HashMap<>(); public static void reset() { instance = null; } /** * Returns a {@link Path} object to the file of the specified path based on the * given {@code root} path. * @param rootPath Limit search to the specified root {@code Path}. * Specify {@code null} to use the current working directory instead. * @param path Relative path to a file or directory. * @param more More optional path elements that are appended to {@code path}. * @return The {@code Path} based on {@code root} and the specified path elements. */ public static Path query(Path rootPath, String path, String... more) { return getInstance()._query(rootPath, path, more); } /** * Returns a {@link Path} to the first matching file of the specified path in one of the listed * root paths. * @param rootPaths List of {@code Path} objects which are searched in order to find {@path}. * Specify {@code null} to use the current working directory instead. * @param path Relative path to a file or directory. * @param more More optional path elements that are appended to {@code path}. * @return The {@code Path} to the first matching file. Returns a {@code Path} based on the * search path of lowest priority if {@code path} does not exist. */ public static Path query(List<Path> rootPaths, String path, String... more) { return getInstance()._query(rootPaths, path, more); } /** * Returns a {@link Path} to the first matching file of the specified path in one of the listed * {@code rootPaths} filtered by {@rootFilter}. * @param rootFilter Limit search to {@code rootPaths} which are based on this root {@code Path}. * Specify {@code null} to ignore {@code rootFilter}. * @param rootPaths List of {@code Path} objects which are searched in order to find {@path}. * @param path Relative path to a file or directory. * @param more More optional path elements that are appended to {@code path}. * @return The {@code Path} to the first matching file. Returns a {@code Path} based on the * search path of lowest priority based on {@code root} if {@code path} does not exist. * Returns a {@code Path} based on the current working path if {@code rootPaths} is empty * after applying {@code rootFilter}. */ public static Path query(Path rootFilter, List<Path> rootPaths, String path, String... more) { return getInstance()._query(rootFilter, rootPaths, path, more); } /** * Returns a {@link Path} object to the file of the specified path based on the * given {@code root} path. * @param rootPath Limit search to the specified root {@code Path}. Specify {@code null} to search * all registered root {@code Path}s. * @param path Relative path to a file or directory. * @param more More optional path elements that are appended to {@code path}. * @return The {@code Path} based on {@code root} and the specified path elements. * Returns {@code null} if {@code path} does not exist. */ public static Path queryExisting(Path rootPath, String path, String... more) { return getInstance()._queryExisting(rootPath, path, more); } /** * Returns a {@link Path} to the first matching file of the specified path in one of the listed * root paths. * @param rootPaths List of {@code Path} objects which are searched in order to find {@path}. * Specify {@code null} to search in registered root {@code Path}s instead. * @param path Relative path to a file or directory. * @param more More optional path elements that are appended to {@code path}. * @return The {@code Path} to the first matching file. * Returns {@code null} if {@code path} does not exist. */ public static Path queryExisting(List<Path> rootPaths, String path, String... more) { return getInstance()._queryExisting(rootPaths, path, more); } /** * Returns a {@link Path} to the first matching file of the specified path in one of the listed * {@code rootPaths} filtered by {@rootFilter}. * @param rootFilter Limit search to {@code rootPaths} which are based on this root {@code Path}. * Specify {@code null} to ignore {@code rootFilter}. * @param rootPaths List of {@code Path} objects which are searched in order to find {@path}. * @param path Relative path to a file or directory. * @param more More optional path elements that are appended to {@code path}. * @return The {@code Path} to the first matching file. * Returns {@code null} if {@code path} does not exist. */ public static Path queryExisting(Path rootFilter, List<Path> rootPaths, String path, String... more) { return getInstance()._queryExisting(rootFilter, rootPaths, path, more); } /** * Attempts to find a path that matches an existing path on both case-sensitive or * case-insensitive filesystems. Non-existing paths are resolved as much as possible. * Non-existing path elements are appended to the resolved base path without further * modifications. * @param path The path to resolve. * @param more More optional path elements. * @return The resolved path or {@code null} on error. */ public static Path resolve(String path, String... more) { if (path != null) { try { return _resolve(FileSystems.getDefault().getPath(path, more)); } catch (Throwable t) { t.printStackTrace(); } } return null; } /** * Attempts to find a path that matches an existing path on case-sensitive or * case-insensitive filesystems. * @param path The path to resolve. * @param more More optional path elements. * @return The resolved path or {@code null} on error or the specified path does not exist. */ public static Path resolveExisting(String path, String... more) { if (path != null) { try { return _resolveExisting(FileSystems.getDefault().getPath(path, more)); } catch (Throwable t) { t.printStackTrace(); } } return null; } /** * Attempts to find a path that matches an existing path on both case-sensitive or * case-insensitive filesystems. Non-existing paths are resolved as much as possible. * Non-existing path elements are appended to the resolved base path without further * modifications. * @param path The path to resolve. * @return The resolved path. */ public static Path resolve(Path path) { return _resolve(path); } /** * Attempts to find a path that matches an existing path on both case-sensitive or * case-insensitive filesystems. * @param path The path to resolve. * @return The resolved path or {@code null} on error or the specified path does not exist. */ public static Path resolveExisting(Path path) { return _resolveExisting(path); } /** * Returns whether the file system the specified {@code path} is pointing to * is restricted to read-only operations. * @param path The path to test. * @return {@code true} if the file system is restricted to read-only operations, * {@code false} otherwise. */ public static boolean isReadOnly(Path path) { if (path != null) { FileSystem fs = path.getFileSystem(); if (fs != null && fs.isOpen()) { return fs.isReadOnly(); } } return true; } /** * Returns whether the specified path points to a location on the default filesystem. * @param path The patch to check. * @return {@code true} if the path points to an existing or non-existing location on the * default filesystem. Returns {@code false} otherwise. */ public static boolean isDefaultFileSystem(Path path) { if (path != null) { return (path.getFileSystem().equals(FileSystems.getDefault())); } return false; } private FileManager() {} private Path _query(Path rootPath, String path, String... more) { List<Path> rootPaths = null; if (rootPath != null) { rootPaths = new ArrayList<>(); rootPaths.add(rootPath); } return _queryPath(false, (Path)null, rootPaths, path, more); } private Path _query(List<Path> rootPaths, String path, String... more) { return _queryPath(false, (Path)null, rootPaths, path, more); } private Path _query(Path rootFilter, List<Path> rootPaths, String path, String... more) { return _queryPath(false, rootFilter, rootPaths, path, more); } private Path _queryExisting(Path rootPath, String path, String... more) { List<Path> rootPaths = null; if (rootPath != null) { rootPaths = new ArrayList<>(); rootPaths.add(rootPath); } return _queryPath(true, (Path)null, rootPaths, path, more); } private Path _queryExisting(List<Path> rootPaths, String path, String... more) { return _queryPath(true, (Path)null, rootPaths, path, more); } private Path _queryExisting(Path rootFilter, List<Path> rootPaths, String path, String... more) { return _queryPath(true, rootFilter, rootPaths, path, more); } private Path _queryPath(boolean mustExist, Path rootFilter, List<Path> rootPaths, String path, String... more) { // path must be defined if (path == null) { return null; } if (rootPaths == null) { rootPaths = new ArrayList<>(); } // filter search if (rootFilter != null) { int idx = 0; while (idx < rootPaths.size()) { Path curPath = rootPaths.get(idx); if (curPath.startsWith(rootFilter)) { rootPaths.remove(idx); } else { idx++; } } } // use current working path as fallback if (rootPaths.isEmpty()) { rootPaths.add(FileSystems.getDefault().getPath("").toAbsolutePath().normalize()); } // ensure that path is relative if (!path.isEmpty()) { if (path.charAt(0) == '/' || path.charAt(0) == '\\') { path = path.substring(1); } } Path curPath = null; boolean exists = false; try { for (final Path curRoot: rootPaths) { try { Path relPath = curRoot.getFileSystem().getPath(path, more).normalize(); if (mustExist) { curPath = _resolveExisting(curRoot.resolve(relPath)); if (curPath != null) { exists = true; break; } } else { curPath = _resolve(curRoot.resolve(relPath)); if (curPath != null && Files.exists(curPath)) { exists = true; break; } } } catch (Exception e) { } } } catch (Throwable t) { curPath = null; t.printStackTrace(); } if (mustExist && !exists) { curPath = null; } return curPath; } private static FileManager getInstance() { if (instance == null) { instance = new FileManager(); } return instance; } // Attempts to find a path which matches an existing path on case-sensitive filesystems. // Simply returns "path" on case-insensitive filesystems. private static Path _resolve(Path path) { Path retVal = path; if (path != null && isFileSystemCaseSensitive(path.getFileSystem())) { boolean found = false; Path curPath = path.normalize().toAbsolutePath(); Path dir = curPath.getRoot(); for (final Path searchPath: curPath) { String searchString = searchPath.getFileName().toString(); found = false; try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (final Path dirPath: ds) { String dirString = dirPath.getFileName().toString(); if (searchString.equalsIgnoreCase(dirString)) { dir = dir.resolve(dirString); found = true; break; } } } catch (Throwable t) { } if (!found) { break; } } if (found) { // use detected path retVal = dir; } else if (dir.getNameCount() < curPath.getNameCount()) { // resolve partial path (needed if filename does not exist in path) retVal = dir.resolve(curPath.subpath(dir.getNameCount(), curPath.getNameCount())); } } return retVal; } private static Path _resolveExisting(Path path) { Path retVal = _resolve(path); if (retVal != null && !Files.exists(retVal)) { retVal = null; } return retVal; } // Returns whether the specified filesystem is case-sensitive private static boolean isFileSystemCaseSensitive(FileSystem fs) { Boolean retVal = Boolean.TRUE; if (fs != null) { retVal = getInstance().mapCaseSensitive.get(fs); if (retVal == null) { final char[] separators = { '/', '\\', ':' }; final String name = "/tmp/aaaBBB"; for (final char sep: separators) { String s = (sep != '/') ? name.replace('/', sep) : name; try { Path path = fs.getPath(s); Path path2 = path.getParent().resolve(path.getFileName().toString().toUpperCase(Locale.ENGLISH)); Path path3 = path.getParent().resolve(path.getFileName().toString().toLowerCase(Locale.ENGLISH)); retVal = Boolean.valueOf(!(path.equals(path2) && path.equals(path3))); getInstance().mapCaseSensitive.put(fs, retVal); break; } catch (Throwable t) { retVal = Boolean.TRUE; } } } } return retVal.booleanValue(); } }