/*-
* 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);
}
*/
}