package net.sf.cotta;
import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An object presentation of path to mainly used by the implemenation of Cotta classes.
* The methods on TPath has been exposed through TFile and TDirectory
*/
public final class TPath implements Comparable<TPath> {
private final String headElement;
private final String[] elements;
private final int offset;
private final int count;
private int hash; // Default to 0
private static final String WINDOWS_SEPARATOR_PATTERN = "\\\\";
private static final char NATIVE_SEPERATOR_CHAR = '/';
private static final String NATIVE_SEPARATOR = "/";
private static final String ROOT_HEAD = "";
private static final String WINDOWS_NETWORK_ROOT_HEAD = "\\\\";
private static final String CURRENT_DIR_HEAD = ".";
/**
* Creates an instance of TPath with the given path elements
*
* @param headElement head element
* @param elements path elements
* @see #parse(String)
*/
private TPath(String headElement, String[] elements) {
int size = elements.length;
this.elements = new String[size];
System.arraycopy(elements, 0, this.elements, 0, size);
this.offset = 0;
this.count = size;
this.headElement = headElement;
}
/**
* Shares the element array for speed.
*
* @param headElement head element
* @param offset the offset from which to begin in the element array
* @param count the length
* @param elements path elements
*/
private TPath(String headElement, int offset, int count, String[] elements) {
this.elements = elements;
this.offset = offset;
this.count = count;
this.headElement = headElement;
}
/**
* The name of the head element, e.g. "" (unix root), "c:", "."
*
* @return the name of the head element
*/
public String headElement() {
return headElement;
}
/**
* The name of the last element, used by TFile and TDirectory to get the name
*
* @return the name of the last element
* @see TFile#name()
* @see TDirectory#name()
*/
public String lastElementName() {
if (count == 0) {
return headElement;
}
return elements[offset + count - 1];
}
/**
* The path of the parent, used by TFile and TDirectory to get the parent
*
* @return parent path
* @see TFile#parent()
* @see TDirectory#parent()
*/
public TPath parent() {
if (count == 0) {
return null;
}
return subpath(0, count - 1);
}
/**
* The root path of the current path
* @return root path of the path
*/
public TPath root() {
return subpath(0, 0);
}
/**
* Join with a path element to form a new path. Used by TDirectory to get subdirectory or file
*
* @param name The name of the path element to join
* @return The resulting path under current path
* @see TDirectory#file(String)
* @see TDirectory#dir(String)
*/
public TPath join(String name) {
String[] newElements = new String[count + 1];
System.arraycopy(elements, offset, newElements, 0, count);
newElements[count] = name;
return new TPath(headElement, newElements);
}
/**
* Join with another relative path. Used by TDirectory to get directory or file based on relative path.
* This will also normalize the path so that there are no current-dir or parent-dir references.
*
* @param path The relative path to join
* @return The result of the join.
* @throws IllegalArgumentException if the path passed in is not a relative path
* @see TDirectory#file(TPath)
* @see TDirectory#dir(TPath)
*/
public TPath join(TPath path) {
return append(path).normalize();
}
private TPath normalize() {
Stack<String> result = new Stack<String>();
int off = offset;
for (int i = 0; i < count; i++ ) {
String element = elements[off++];
if (isCurrentDirectoryReference(element)) {
// do nothing
} else if (isParentDirectoryReference(element)) {
if (result.isEmpty()) {
if (!isRelative()) {
throw new IllegalArgumentException("Cannot normalize <" + toPathString() + ">");
}
result.push(element);
}
else {
result.pop();
}
} else {
result.push(element);
}
}
return new TPath(headElement, result.toArray(new String[result.size()]));
}
/**
* Append with another path, without normalizing.
*
* @param path The relative path to append
* @return The result of the append
*/
public TPath append(TPath path) {
int length = count + path.count;
String[] joined = new String[length];
System.arraycopy(elements, offset, joined, 0, count);
System.arraycopy(path.elements, path.offset, joined, count, path.count);
return new TPath(headElement, joined);
}
public TPath intern() {
if (offset == 0 && count == elements.length) {
return this;
}
String[] newElements = new String[count];
System.arraycopy(elements, offset, newElements, 0, count);
return new TPath(headElement, newElements);
}
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof TPath) {
TPath tPath = (TPath) o;
if (headElement == tPath.headElement || headElement.equals(tPath.headElement)) {
int n = count;
if (n == tPath.count) {
String[] e1 = elements;
String[] e2 = tPath.elements;
int i = offset;
int j = tPath.offset;
while (n-- != 0) {
if (!e1[i++].equals(e2[j++])) {
return false;
}
}
return true;
}
}
}
return false;
}
public int hashCode() {
int h = hash;
if (h == 0) {
h = 29 * h + headElement.hashCode();
int off = offset;
for (int i = 0; i < count; i++) {
h = 29 * h + elements[off++].hashCode();
}
hash = h;
}
return hash;
}
/**
* String representation of the path. To get the path string understood by most other application
* please use toPathString()
*
* @return path string
* @see #toPathString()
*/
public String toString() {
return toPathString();
}
/**
* Parses a path string into a TPath object. Both '/' and '\' are treated as the file separator
*
* @param pathString The path string that represents the path
* @return The path object that match to the path string
* @throws IllegalArgumentException if the path string is null
*/
public static TPath parse(String pathString) {
if (pathString == null || pathString.length() == 0) {
throw new IllegalArgumentException("null or empty path string is not allowed");
}
Pattern currentDir = Pattern.compile("(\\.)|(\\.(\\\\|/)(.*))");
Pattern windowsRoot = Pattern.compile("([A-Z|a-z]:)[\\\\|/]?(.*)");
String headElement;
Matcher matcher;
if (pathString.startsWith(WINDOWS_NETWORK_ROOT_HEAD)) {
headElement = WINDOWS_NETWORK_ROOT_HEAD;
pathString = pathString.substring(WINDOWS_NETWORK_ROOT_HEAD.length());
}
else if (pathString.startsWith("\\") || pathString.startsWith("/")) {
headElement = ROOT_HEAD;
pathString = pathString.substring(1);
}
else if ((matcher = currentDir.matcher(pathString)).matches()) {
headElement = CURRENT_DIR_HEAD;
pathString = matcher.group(4);
if (pathString == null) {
pathString = "";
}
}
else if ((matcher = windowsRoot.matcher(pathString)).matches()) {
headElement = matcher.group(1);
pathString = matcher.group(2);
}
else {
headElement = CURRENT_DIR_HEAD;
}
pathString = pathString.replaceAll(WINDOWS_SEPARATOR_PATTERN, NATIVE_SEPARATOR);
List<String> list = new ArrayList<String>();
for (StringTokenizer tokenizer = new StringTokenizer(pathString, NATIVE_SEPARATOR); tokenizer.hasMoreTokens();) {
list.add(tokenizer.nextToken());
}
return new TPath(headElement, list.toArray(new String[list.size()]));
}
public String toPathString() {
return toPathStringImpl(NATIVE_SEPERATOR_CHAR);
}
public String toPathString(PathSeparator pathSeparator) {
return toPathStringImpl(pathSeparator.getValue());
}
public String toSystemPathString() {
return toPathStringImpl(File.separatorChar);
}
private String toPathStringImpl(char seperator) {
StringBuilder buffer = new StringBuilder();
if (headElement == ROOT_HEAD) {
if (count == 0) {
buffer.append(seperator);
}
}
else {
buffer.append(headElement);
}
int off = offset;
for (int i = 0; i < count; i++) {
buffer.append(seperator).append(elements[off++]);
}
return buffer.toString();
}
/**
* Check if current path is the child of the given path. This used by TFile and TDirectory
* to see if it is under another directory
*
* @param path The path to check to see if current path is its child
* @return true if current path is the child of the given path
* @see TFile#isChildOf(TDirectory)
* @see TDirectory#isChildOf(TDirectory)
*/
public boolean isChildOf(TPath path) {
if (count <= path.count) {
return false;
}
return checkCommonElements(path) == path.count;
}
private boolean isParentDirectoryReference(String element) {
return "..".equals(element);
}
private boolean isCurrentDirectoryReference(String element) {
return ".".equals(element);
}
/**
* Check is the current path is a relative path or absolute path
*
* @return true if the current path is a relative path.
*/
public boolean isRelative() {
return headElement == CURRENT_DIR_HEAD;
}
/**
* Dericve the relative path from the other path, to be used by TFile and TDirectory
*
* @param path The other path to derive relative path from
* @return The relative path from the other path
*/
public TPath pathFrom(TPath path) {
if (isRelative() ^ path.isRelative()) {
throw new IllegalArgumentException("path passed in should be the same kind of relative or absolute path:" + path);
}
int index = checkCommonElements(path);
int numberOfThisExcessElements = count - index;
int numberOfThatExcessElements = path.count - index;
String[] relativePath = new String[numberOfThatExcessElements + numberOfThisExcessElements];
for (int i = 0; i < numberOfThatExcessElements; i++) {
relativePath[i] = "..";
}
System.arraycopy(elements, index, relativePath, numberOfThatExcessElements, numberOfThisExcessElements);
return new TPath(CURRENT_DIR_HEAD, relativePath);
}
private int checkCommonElements(TPath path) {
if (!headElement.equals(path.headElement)) {
return 0;
}
int i = 0;
int max = Math.min(count, path.count);
while (i < max && elements[offset + i].equals(path.elements[path.offset + i])) {
i++;
}
return i;
}
/**
* Compare two path by comparing each elements.
*
* @param that the other path
* @return comparing result
*/
public int compareTo(TPath that) {
int len1 = count;
int len2 = that.count;
int n = Math.min(len1, len2);
String v1[] = elements;
String v2[] = that.elements;
int i = offset;
int j = that.offset;
if (i == j) {
int k = i;
int lim = n + i;
while (k < lim) {
String s1 = v1[k];
String s2 = v2[k];
if (!s1.equals(s2)) {
return s1.compareTo(s2);
}
k++;
}
} else {
while (n-- != 0) {
String s1 = v1[i++];
String s2 = v2[j++];
if (!s1.equals(s2)) {
return s1.compareTo(s2);
}
}
}
return len1 - len2;
}
public TPath subpath(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new ArrayIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new ArrayIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new ArrayIndexOutOfBoundsException(endIndex - beginIndex);
}
if (beginIndex == 0 && endIndex == count) {
return this;
}
else {
String head;
if (beginIndex == 0) {
head = this.headElement;
}
else {
head = CURRENT_DIR_HEAD;
}
return new TPath(head, offset + beginIndex, endIndex - beginIndex, elements);
}
}
public TPath subpath(int beginIndex) {
return subpath(beginIndex, count);
}
public TPath trim() {
int len = count;
int st = 0;
while (st < len && isCurrentDirectoryReference(elements[offset + st])) {
st++;
}
while (st < len && isCurrentDirectoryReference(elements[offset + len - 1])) {
len--;
}
return (st > 0 || len < count) ? subpath(st, len) : this;
}
public String elementAt(int i) {
return elements[i];
}
public int length() {
return count;
}
String[] toElementArray() {
String[] result = new String[count];
System.arraycopy(elements, offset, result, 0, count);
return result;
}
}