/*- * Copyright (C) 2008 Erik Larsson * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.catacombae.storage.fs; import java.util.LinkedList; import org.catacombae.util.Util; /** * @author <a href="http://www.catacombae.org/" target="_top">Erik Larsson</a> */ public abstract class FileSystemHandler { private static final boolean DEBUG = Util.booleanEnabledByProperties(false, "org.catacombae.debug", "org.catacombae.storage.debug", "org.catacombae.storage.fs.debug", "org.catacombae.storage.fs." + FileSystemHandler.class.getSimpleName() + ".debug"); public abstract FileSystemCapability[] getCapabilities(); public boolean hasCapability(FileSystemCapability c) { for(FileSystemCapability cur : getCapabilities()) { if(cur == c) return true; } return false; } /** * Lists all entries present under the <code>path</code> supplied. Path must * point to a folder, and is composed of a variable arguments list with each * pathname component as a separate String.<br> * Invoking this method with no arguments gives the file list of the * root directory of the file system.<br> * Examples: * <ul> * <li> * Getting the contents of <code>/usr/bin</code> in a UNIX-style filesystem: * <code>listFiles("usr", "bin");</code></li> * <li> * Getting the contents of <code>\Windows\System32</code> in a Windows-style * file system: <code>listFiles("Windows", "System32");</code> * </li> * <li> * Getting the contents of <code>Users:joe</code> in an old Macintosh-style * file system: <code>listFiles("Users", "joe");</code> * </li> * </ul> * * @param path the path to the requested folder with each path component * as a separate string. The first component under the root dir will be * leftmost in the argument list. * @return an array with file system entries that represents the contents of * the requested folder, or <code>null</code> if the folder can't be found. */ public abstract FSEntry[] list(String... path); /** * Returns the root folder of the file system hierarchy. * * @return the root folder of the file system hierarchy. */ public abstract FSFolder getRoot(); /** * Returns the FSEntry present on the location <code>path</code>. If the * path is invalid in this file system, <code>null</code> is returned. * * @param path the file system path to the requested entry, path element by * path element (ex. <code>getEntry("usr", "local", "bin", "java");</code>). * @return the FSEntry present on the location <code>path</code>, or * <code>null</code> if no such entry exists. */ public abstract FSEntry getEntry(String... path); /** * Looks up the FSEntry denoted by the supplied POSIX path. Since POSIX * paths may be relative, a root folder is needed to resolve the relative * path structure. If the POSIX pathname is absolute, the root folder * parameter will not be used. * * @param posixPath the POSIX pathname. * @param rootFolderPath path to the root folder from which we should * start resolving the path. * @return the FSEntry corresponding to the supplied POSIX path, or <code> * null</code> if no such pathname could be found. * @throws java.lang.IllegalArgumentException if <code>posixPath</code> is * an invalid pathname. */ public FSEntry getEntryByPosixPath(final String posixPath, final String... rootFolderPath) throws IllegalArgumentException { /* final String prefix = globalPrefix; globalPrefix += " "; try { */ //log(prefix + "getEntryByPosixPath(" + posixPath + ", " + Util.concatenateStrings(rootFolderPath, "/") + ");"); String[] path = getTruePathFromPosixPath(posixPath, rootFolderPath); if(path != null) { //log(prefix + " getEntryByPosixPath: path = " + Util.concatenateStrings(path, "/")); FSEntry entry = getEntry(path); return entry; } else return null; //} finally { log(prefix + "Returning from getEntryByPosixPath"); globalPrefix = prefix; } } /** * Returns an absolute, canonical path name in the current file system for the given POSIX path * string. * * @param posixPath * @param rootFolderPath * @return an absolute, canonical path name for the given POSIX path. * @throws java.lang.IllegalArgumentException */ public String[] getTruePathFromPosixPath(final String posixPath, final String... rootFolderPath) throws IllegalArgumentException { /* final String prefix = globalPrefix; globalPrefix += " "; log(prefix + "getTruePathFromPosixPath(\"" + posixPath + "\", { \"" + Util.concatenateStrings(rootFolderPath, "\", \"") + "\" });"); try { */ String[] components = posixPath.split("/"); int i = 0; //FSEntry curEntry; LinkedList<String> pathStack = new LinkedList<String>(); LinkedList<String[]> visitedLinks = null; // If we encounter a '/' as the first character, we have an absolute path if(posixPath.startsWith("/")) { i = 1; } else { for(String pathComponent : rootFolderPath) pathStack.addLast(pathComponent); } FSEntry curEntry2 = null; for(; i < components.length; ++i) { String[] curPath = null; //log(prefix + " gtpfpp: curPath=\"" + Util.concatenateStrings(curPath, "\", \"") + "\""); if(curEntry2 == null) { if(curPath == null) curPath = pathStack.toArray(new String[pathStack.size()]); curEntry2 = getEntry(curPath); //log(prefix + " gtpfpp: curEntry2=" + curEntry2); } FSFolder curFolder; if(curEntry2 instanceof FSFolder) curFolder = (FSFolder) curEntry2; else if(curEntry2 instanceof FSLink) { FSLink curLink = (FSLink) curEntry2; //log(prefix + " gtpfpp: It was a link!"); // Resolve links. if(visitedLinks == null) visitedLinks = new LinkedList<String[]>(); else visitedLinks.clear(); if(curPath == null) curPath = pathStack.toArray(new String[pathStack.size()]); FSEntry linkTarget = resolveLinks(curPath, curLink, visitedLinks); //log(prefix + " gtpfpp: Before test."); if(linkTarget == null) return null; // Invalid link target. if(linkTarget instanceof FSFolder) curFolder = (FSFolder) linkTarget; else if(linkTarget instanceof FSFile) return null; // Invalid intermediate path component else throw new RuntimeException("Unknown type: " + linkTarget.getClass()); visitedLinks.clear(); } else return null; // Invalid pathname String curPathComponent = components[i]; //log(prefix + " gtpfpp: curPathComponent=" + curPathComponent); if(curPathComponent.length() == 0 || curPathComponent.equals(".")) { // We allow empty components (multiple slashes between components) } else if(curPathComponent.equals("..")) { if(pathStack.size() > 0) { pathStack.removeLast(); curEntry2 = null; // Triggers a parse from dir stack } else return null; // Invalid pathname (trying to reference pathname above root) } else { String fsPathnameComponent = parsePosixPathnameComponent(curPathComponent); //log(prefix + " gtpfpp: fsPathnameComponent=" + fsPathnameComponent); FSEntry nextEntry = curFolder.getChild(fsPathnameComponent); /*for(FSEntry entry : curFolder.list()) { //log(prefix + " gtpfpp: Checking if " + entry.getName() + " matches..."); if(entry.getName().equals(fsPathnameComponent)) { nextEntry = entry; //log(prefix + " gtpfpp: Match found!!"); break; } }*/ //log(prefix + " gtpfpp: nextEntry=" + nextEntry); if(nextEntry != null) { curEntry2 = nextEntry; pathStack.add(nextEntry.getName()); } else return null; // Invalid pathname } } final String[] res = pathStack.toArray(new String[pathStack.size()]); //log(prefix + " gtpfpp: Returning " + Util.concatenateStrings(res, "/")); return res; /* } finally { log(prefix + "Returning from getTruePathFromPosixPath."); globalPrefix = prefix; } */ } public FSEntry resolveLinks(String[] linkPath, FSLink link) { return resolveLinks(linkPath, link, new LinkedList<String[]>()); } private FSEntry resolveLinks(String[] curPath, FSLink curLink, LinkedList<String[]> visitedLinks) { if(DEBUG) { System.err.println("resolveLinks(" + Util.concatenateStrings(curPath, "/") + ", " + curLink.getLinkTargetString() + ", ...);"); } //String prefix = globalPrefix; /* * Pseudo-code: * * Input: * curPath : The path to the link we are trying to resolve * curLink : The link structure * Semi-local: * visitedLinks : A list of pathnames that we have visited. * Local: * linkTarget : * * */ //log(prefix + " gtpfpp: It was a link!"); // Resolve links. // We have visited ourselves. visitedLinks.add(curPath); FSEntry linkTarget = null; String[] curLinkPath = curPath; while(curLinkPath != null) { // We must resolve the current link against its parent directory. String[] parentPath = Util.arrayCopy(curLinkPath, 0, new String[curLinkPath.length - 1], 0, curLinkPath.length - 1); if(DEBUG) { System.err.println(" Resolving " + curLink.getLinkTargetString() + " from " + Util.concatenateStrings(parentPath, "/")); } // Resolve the current link target path. String[] linkTargetPath = getTargetPath(curLink, parentPath); if(linkTargetPath == null) return null; //log(prefix + " linkTargetPath=" + Util.concatenateStrings(linkTargetPath, "/")); if(Util.contains(visitedLinks, linkTargetPath)) return null; // We have been here before! Circular linking... linkTarget = getEntry(linkTargetPath); //? //curLink.getLinkTarget(parentPath); //log(prefix + " gtpfpp: Result: " + linkTarget); if(linkTarget != null && linkTarget instanceof FSLink) { curLink = (FSLink) linkTarget; curLinkPath = linkTargetPath; visitedLinks.add(curLinkPath); // Check the visited list to see if we have been here before. //if(Util.contains(visitedLinks, curLinkPath)) // return null; // We have been here before! Circular linking... } else curLinkPath = null; } return linkTarget; } /** * Converts the supplied POSIX pathname component into the proper file * system pathname component form. For example, the ':' character in HFS+ * POSIX pathname components represent the character '/' in the file system. * <br> * For a strictly POSIX file system, this method should just bounce the * input string. * * @param posixPathnameComponent * @return a <b>proper</b> pathname component. */ public abstract String parsePosixPathnameComponent(String posixPathnameComponent); /** * Converts the supplied file system pathname component into a corresponding * POSIX pathname component. This may involve converting certain * POSIX-incompatible characters into suitable replacements, such as * pathname components containing the character '/'. * <br> * For a strictly POSIX file system, this method should just bounce the * input string. * * @param fsPathnameComponent * @return a <b>POSIX compatible</b> pathname component. */ public abstract String generatePosixPathnameComponent(String fsPathnameComponent); /** * Returns the path to the link's target in absolute form. * * @param link the link to resolve. * @return the path to the link's target in absolute form. */ public abstract String[] getTargetPath(FSLink link, String[] parentDir); /** * Returns the predefined fork types that this file system recognizes and * supports. Note that this does not mean that every file in the file system * will have these fork types, it just means that these forks may be present * in any file. Any fork type not returned by this method is unknown to the * file system handler and has no meaning in its context.<br> * <b>Note:</b> All implementations must support the FSForkType.DATA type, * as the main fork, the data fork, must always be defined for a file. * * @return the predefined fork types that this file system recognizes and * supports. */ public abstract FSForkType[] getSupportedForkTypes(); /** * Closes the file system handler and frees allocated resources. */ public abstract void close(); // Debug stuff... TODO remove /* public String globalPrefix = ""; public void log(String message) { System.err.println(message); } */ }