package diskCacheV111.util;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Immutable absolute abstract path in the dCache name space.
*
* An FsPath represents a path in the dCache name space. It is always
* absolute, meaning it is rooted at the name space root.
*
* Self and parent references are eliminated during construction, which
* means an FsPath never contains '.' or '..' elements. The elimination
* is done on the abstract path and not on the actual name space, meaning
* that /a/b/.. becomes /a even if b is a symbolic link, a file or when
* it doesn't exist.
*/
public abstract class FsPath implements Serializable
{
private static final long serialVersionUID = -4852395244086808172L;
/* An FsPath is implemented as a single linked list from the tail to the
* root. All operations are implemented recursively and modifying operations
* return a new FsPath rather than changing the existing FsPath.
*/
/**
* FsPath representing the file system root. This is the only instance.
*/
public static final FsPath ROOT = new Root();
public static FsPath create(String path)
{
checkArgument(path.startsWith("/"));
return ROOT.resolve(path.substring(1));
}
/**
* Provided for compatibility with type conversion libraries that rely
* on a static valueOf function. Use {@code create} when creating an FsPath
* directly.
*/
public static FsPath valueOf(String path)
{
return create(path);
}
@Override
public String toString()
{
return appendTo(new StringBuilder()).toString();
}
/**
* Append the path represented by this FsPath to {@code path}.
*
* @param path StringBuilder to append to
* @return The {@code path} StringBuilder
*/
protected abstract StringBuilder appendTo(StringBuilder path);
/**
* A version of {@code appendTo} that terminates {@code path} with a slash.
*
* @param path StringBuilder to append to
* @return The {@code path} StringBuilder
*/
protected abstract StringBuilder appendTo2(StringBuilder path);
/**
* Drops the length of {@code path} elements from this FsPath and
* returns the remaining prefix. Returns null if {@code path}
* is longer than this FsPath.
*/
protected abstract FsPath tryDropLength(FsPath path);
/**
* Drops the length of {@code path} elements from this FsPath and
* returns the remaining prefix.
*
* Equivalent to {@code drop(path.length())}.
*/
protected abstract FsPath dropLength(FsPath path);
/**
* Returns true if {@code path} forms a suffix of this FsPath.
*
* @param path Path elements in reverse order (child before parent)
* @return True iff {@code path} forms a suffix of this FsPath.
*/
protected abstract boolean isSuffix(Iterator<String> path);
/**
* Returns true if {@code path} is fully contained within this FsPath.
*
* @param path Path elements in reverse order (child before parent).
* @return True iff {@code path} is fully contained within this FsPath.
*/
protected abstract boolean contains(List<String> path);
/**
* Returns true if {@code path} is fully contained within this FsPath.
*
* Path elements in {@code path} are separated by a slash. Empty path
* elements are stripped.
*
* @param path Slash separated path to check for inclusion.
* @return True iff {@code path} is fully contained within this FsPath.
*/
public boolean contains(String path)
{
return contains(Lists.reverse(Splitter.on("/").omitEmptyStrings().splitToList(path)));
}
/**
* Returns true if this is the path of the file system root, false otherwise.
*
* @return True iff this is the path of the file system root.
*/
public abstract boolean isRoot();
/**
* Returns the path of this FsPath's parent.
*
* @return The path of this FsPath's parent.
* @throws IllegalArgumentException if this is a root path
*/
public abstract FsPath parent();
/**
* Returns the name of the file or directory denoted by this FsPath.
*
* @return The name of the file or directory denoted by this FsPath.
*/
public abstract String name();
/**
* Returns the number of elements of this FsPath.
*
* The length of the path of the file system root is 0.
*
* @return The number of elements of this FsPath.
*/
public abstract int length();
/**
* Returns the path obtained by dropping up to {@code n} elements of this FsPath.
*
* @return The path obtained by dropping up to {@code n} elements of this FsPath.
*/
public abstract FsPath drop(int n);
/**
* Returns the path obtained by resolving {@code path} relative to this FsPath.
*
* If {@code path} is a relative path, the path is effectively appended.
*/
public FsPath resolve(String path)
{
int i = path.lastIndexOf('/');
switch (i) {
case -1:
return resolvePathElement(ROOT, path);
case 0:
return ROOT.resolvePathElement(ROOT, path.substring(1));
default:
return resolve(path.substring(0, i)).resolvePathElement(ROOT, path.substring(i + 1));
}
}
/**
* Returns the path obtained by resolving {@code path} relative to this FsPath.
*
* Differs from {@code resolve} in that this FsPath is considered the root to resolve
* {@code path} against. This means that {@code path} is appended to this FsPath even
* if {@code path} is an absolute path. If {@code path} contains .. elements, these
* never walk above this FsPath.
*/
public FsPath chroot(String path)
{
int i = path.lastIndexOf('/');
switch (i) {
case -1:
return resolvePathElement(this, path);
case 0:
return resolvePathElement(this, path.substring(1));
default:
return chroot(path.substring(0, i)).resolvePathElement(this, path.substring(i + 1));
}
}
private FsPath resolvePathElement(FsPath root, String name)
{
switch (name) {
case "":
case ".":
return this;
case "..":
return (this == root) ? this : parent();
default:
return new Child(this, name);
}
}
/**
* Returns an FsPath representing the path of the named {@code name} of the path
* represented by this FsPath.
*/
public FsPath child(String name)
{
checkArgument(!name.isEmpty(), "Name must be non-empty.");
checkArgument(!name.contains("/"), "Name must not contain '/'.");
checkArgument(!name.equals("."), "Name must not be '.'.");
checkArgument(!name.equals(".."), "Name must not be '..'.");
return new Child(this, name);
}
/**
* Returns true if the path of {@code prefix} is a prefix of the path of this FsPath.
*/
public boolean hasPrefix(FsPath prefix)
{
/* The purpose of delta is just to describe how much longer this path is than prefix - the
* actual path in delta is of no interest. It's just a clever way to implement the prefix
* in O(n + m) steps.
*/
FsPath delta = tryDropLength(prefix);
return delta != null && prefix.equals(dropLength(delta));
}
/**
* Strips {@code prefix} from the beginning of this FsPath and returns the string form of
* the remaining suffix. The suffix starts with a slash.
*
* @throws IllegalArgumentException if {@code prefix} is not a prefix of this FsPath.
*/
public String stripPrefix(FsPath prefix)
{
FsPath delta = dropLength(prefix);
return appendSuffix(prefix, delta, new StringBuilder()).toString();
}
/**
* Appends the string representation of the suffix of this FsPath with the same length as
* {@code length} to {@code s}.
*
* @param prefix A prefix of this path.
* @param length An FsPath of the same length as the suffix to append to {@code s}.
* @param s A StringBuilder to append the path to.
* @return The {@code path} StringBuilder
* @throws IllegalArgumentException if {@code prefix} is not a prefix of this FsPath or if the
* the length of this FsPath is not the sum of the length of
* {@code prefix} and {@code length}.
*/
protected abstract StringBuilder appendSuffix(FsPath prefix, FsPath length, StringBuilder s);
/**
* A version of {@code appendSuffix} that terminates {@code path} with a slash.
*
* @param prefix A prefix of this path.
* @param length An FsPath of the same length as the suffix to append to {@code s}.
* @param s A StringBuilder to append the path to.
* @return The {@code path} StringBuilder
* @throws IllegalArgumentException if {@code prefix} is not a prefix of this FsPath or if the
* the length of this FsPath is not the sum of the length of
* {@code prefix} and {@code length}.
*/
protected abstract StringBuilder appendSuffix2(FsPath prefix, FsPath length, StringBuilder s);
/**
* Helper method to write a path to a stream.
*
* @param out Stream to write the path to
* @param len The length of the suffix of this path being written
* @throws IOException
*/
protected abstract void write(ObjectOutput out, int len) throws IOException;
/**
* Helper method to read a path from a stream.
*
* @param in Stream to read the path from
* @param len The length of the suffix of this path being read
* @throws IOException
*/
protected FsPath read(ObjectInput in, int len) throws IOException
{
return (len == 0) ? this : child(in.readUTF()).read(in, len - 1);
}
private Object writeReplace()
throws ObjectStreamException
{
return new SerializedPath(this);
}
private static final class Root extends FsPath
{
@Override
protected StringBuilder appendTo(StringBuilder path)
{
return appendTo2(path);
}
@Override
protected StringBuilder appendTo2(StringBuilder path)
{
return path.append('/');
}
@Override
public boolean isRoot()
{
return true;
}
@Override
public FsPath parent()
{
throw new IllegalStateException("Root does not have a parent");
}
@Override
public int hashCode()
{
return 0;
}
@Override
public boolean equals(Object o)
{
return (o instanceof Root);
}
@Override
public String name()
{
return "/";
}
@Override
public int length()
{
return 0;
}
@Override
public FsPath drop(int n)
{
checkArgument(n >= 0);
return this;
}
@Override
protected FsPath tryDropLength(FsPath path)
{
return path.isRoot() ? this : null;
}
@Override
protected FsPath dropLength(FsPath path)
{
return this;
}
@Override
protected boolean isSuffix(Iterator<String> path)
{
return !path.hasNext();
}
@Override
protected boolean contains(List<String> path)
{
return path.isEmpty();
}
@Override
protected StringBuilder appendSuffix(FsPath prefix, FsPath length, StringBuilder s)
{
return appendSuffix2(prefix, length, s);
}
@Override
protected StringBuilder appendSuffix2(FsPath prefix, FsPath length, StringBuilder s)
{
if (!prefix.isRoot()) {
throw new IllegalArgumentException(prefix + " is not a prefix of " + this);
}
return s.append('/');
}
@Override
protected void write(ObjectOutput out, int len) throws IOException
{
out.writeShort(len);
}
}
private static final class Child extends FsPath
{
private final FsPath parent;
private final String name;
private Child(FsPath parent, String name)
{
this.parent = parent;
this.name = name;
}
@Override
protected StringBuilder appendTo(StringBuilder path)
{
return parent.appendTo2(path).append(name);
}
@Override
protected StringBuilder appendTo2(StringBuilder path)
{
return appendTo(path).append('/');
}
@Override
public boolean isRoot()
{
return false;
}
@Override
public FsPath parent()
{
return parent;
}
@Override
public int hashCode()
{
return 31 * parent.hashCode() + name.hashCode();
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof FsPath)) {
return false;
}
FsPath other = (FsPath) o;
return !other.isRoot() && name().equals(other.name()) && parent().equals(other.parent());
}
@Override
public String name()
{
return name;
}
@Override
public int length()
{
return parent().length() + 1;
}
@Override
public FsPath drop(int n)
{
checkArgument(n >= 0);
return (n == 0) ? this : parent().drop(n - 1);
}
@Override
protected FsPath tryDropLength(FsPath path)
{
return path.isRoot() ? this : parent().tryDropLength(path.parent());
}
@Override
protected FsPath dropLength(FsPath path)
{
return path.isRoot() ? this : parent().dropLength(path.parent());
}
@Override
protected boolean isSuffix(Iterator<String> path)
{
return !path.hasNext() || path.next().equals(name()) && parent().isSuffix(path);
}
@Override
protected boolean contains(List<String> path)
{
return isSuffix(path.iterator()) || parent().contains(path);
}
@Override
protected StringBuilder appendSuffix(FsPath prefix, FsPath length, StringBuilder s)
{
if (length.isRoot()) {
if (!equals(prefix)) {
throw new IllegalArgumentException(prefix + " is not a prefix of " + this);
}
return s;
}
return parent().appendSuffix2(prefix, length.parent(), s).append(name());
}
@Override
protected StringBuilder appendSuffix2(FsPath prefix, FsPath length, StringBuilder s)
{
return appendSuffix(prefix, length, s).append('/');
}
@Override
protected void write(ObjectOutput out, int len) throws IOException
{
parent.write(out, len + 1);
out.writeUTF(name);
}
}
private static class SerializedPath implements Externalizable
{
private static final long serialVersionUID = 2301167077307955976L;
private transient FsPath path;
public SerializedPath()
{
}
public SerializedPath(FsPath path)
{
this.path = path;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
path.write(out, 0);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
{
this.path = ROOT.read(in, in.readShort());
}
private Object readResolve()
{
return path;
}
}
}