/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.filestore;
import divconq.schema.IDataExposer;
import divconq.util.IOUtil;
import divconq.util.StringUtil;
// object is to immutable, keep it that way :)
public class CommonPath implements IDataExposer {
static public CommonPath ROOT = new CommonPath("/");
// internal representation
protected String pathparts[] = null;
protected String path = null;
// paths must always start with /
public CommonPath(String pathname) {
if (StringUtil.isEmpty(pathname) || !pathname.startsWith("/"))
throw new IllegalArgumentException("Path not valid content: " + pathname);
this.path = CommonPath.normalizeAndCheck(pathname);
if (StringUtil.isEmpty(this.path))
throw new IllegalArgumentException("Path not valid format: " + pathname);
// TODO really we want the byte length, but this is something
if (this.path.length() > 32000)
throw new IllegalArgumentException("Path to long: " + pathname);
int count = 0;
int index = 0;
boolean cntname = true;
// count names
while (index < this.path.length()) {
char c = this.path.charAt(index);
if (c == '/') {
cntname = true;
}
else if (cntname) {
count++;
cntname = false;
}
index++;
}
// populate array
String[] result = new String[count];
int start = 0;
index = 0;
count = 0;
cntname = true;
while (index < this.path.length()) {
char c = this.path.charAt(index);
if (c == '/') {
if (index > start)
result[count - 1] = this.path.substring(start, index);
start = index + 1;
cntname = true;
}
else if (cntname) {
count++;
cntname = false;
}
index++;
}
// if some name is left
if (!cntname)
result[count - 1] = this.path.substring(start, index);
// don't allow .. or .
for (String pn : result) {
if (!IOUtil.isLegalFilename(pn))
throw new IllegalArgumentException("Path name not valid: " + pathname + " - part: " + pn);
}
this.pathparts = result;
}
// removes redundant slashes and check input for invalid characters
static public String normalizeAndCheck(String input) {
int n = input.length();
char prevChar = 0;
for (int i=0; i < n; i++) {
char c = input.charAt(i);
if ((c == '/') && (prevChar == '/'))
return CommonPath.normalize(input, n, i - 1);
prevChar = c;
}
if (prevChar == '/')
return CommonPath.normalize(input, n, n - 1);
return input;
}
protected static String normalize(String input, int len, int off) {
if (len == 0)
return input;
int n = len;
while ((n > 0) && (input.charAt(n - 1) == '/'))
n--;
if (n == 0)
return "/";
StringBuilder sb = new StringBuilder(input.length());
if (off > 0)
sb.append(input.substring(0, off));
char prevChar = 0;
for (int i=off; i < n; i++) {
char c = input.charAt(i);
if ((c == '/') && (prevChar == '/'))
continue;
sb.append(c);
prevChar = c;
}
return sb.toString();
}
public boolean isRoot() {
return ((this.pathparts == null) || (this.pathparts.length == 0));
}
public String getFileName() {
return this.isRoot() ? null : this.pathparts[this.pathparts.length - 1];
}
public boolean hasFileExtension() {
String fname = this.getFileName();
if (fname == null)
return false;
int pos = fname.lastIndexOf('.');
return (pos != -1);
}
public String getFileExtension() {
String fname = this.getFileName();
if (fname == null)
return null;
int pos = fname.lastIndexOf('.');
if (pos == -1)
return null;
return fname.substring(pos + 1);
}
public String getFileNameMinusExtension() {
String fname = this.getFileName();
if (fname == null)
return null;
int pos = fname.lastIndexOf('.');
if (pos == -1)
return fname;
return fname.substring(0, pos);
}
public CommonPath getParent() {
return this.isRoot() ? CommonPath.ROOT : this.subpath(0, this.pathparts.length - 1);
}
public int getNameCount() {
if (this.pathparts == null)
return 0;
return this.pathparts.length;
}
public String getName(int index) {
if ((this.pathparts == null) || (this.pathparts.length == 0))
return null;
if ((index < 0) || (index >= this.pathparts.length))
return null;
return this.pathparts[index];
}
/**
* If begin is 0 then we keep "absolute"
*
* @param beginIndex starting path part
* @param length number of path parts
* @return the sub path
*/
public CommonPath subpath(int beginIndex, int length) {
if ((this.pathparts == null) || (this.pathparts.length == 0))
return CommonPath.ROOT;
if ((beginIndex < 0) || (beginIndex >= this.pathparts.length))
return CommonPath.ROOT;
if ((length < 1) || (beginIndex + length > this.pathparts.length))
return CommonPath.ROOT;
return new CommonPath("/" + StringUtil.join(this.pathparts, "/", beginIndex, beginIndex + length));
}
public CommonPath subpath(int beginIndex) {
int length = this.pathparts.length - beginIndex;
return this.subpath(beginIndex, length);
}
public CommonPath subpath(CommonPath other) {
if (other.isRoot())
return this;
if (this.path.startsWith(other.path))
return new CommonPath(this.path.substring(other.path.length()));
return CommonPath.ROOT;
}
// provide other path starting with /
public CommonPath resolve(String other) {
if (StringUtil.isEmpty(other))
return null;
if (!other.startsWith("/"))
other = "/" + other;
return this.isRoot() ? new CommonPath(other) : new CommonPath(this.path + other);
}
public CommonPath resolve(CommonPath other) {
return this.isRoot() ? other : new CommonPath(this.path + other.path);
}
public CommonPath resolvePeer(String other) {
if (StringUtil.isEmpty(other))
return null;
if (!other.startsWith("/"))
other = "/" + other;
return this.isRoot() ? new CommonPath(other) : this.getParent().resolve(other);
}
public boolean isParent(CommonPath other) {
if (this.isRoot())
return true;
if (this.pathparts.length >= other.pathparts.length)
return false;
for (int i = 0; i < this.pathparts.length; i++) {
if (!this.pathparts[i].equals(other.pathparts[i]))
return false;
}
return true;
}
/* TODO
// Resolve child against given base
private static byte[] resolve(byte[] base, byte[] child) {
int baseLength = base.length;
int childLength = child.length;
if (childLength == 0)
return base;
if (baseLength == 0 || child[0] == '/')
return child;
byte[] result;
if (baseLength == 1 && base[0] == '/') {
result = new byte[childLength + 1];
result[0] = '/';
System.arraycopy(child, 0, result, 1, childLength);
} else {
result = new byte[baseLength + 1 + childLength];
System.arraycopy(base, 0, result, 0, baseLength);
result[base.length] = '/';
System.arraycopy(child, 0, result, baseLength+1, childLength);
}
return result;
}
UnixPath resolve(byte[] other) {
return resolve(new UnixPath(getFileSystem(), other));
}
@Override
public UnixPath relativize(Path obj) {
UnixPath other = toUnixPath(obj);
if (other.equals(this))
return emptyPath();
// can only relativize paths of the same type
if (this.isAbsolute() != other.isAbsolute())
throw new IllegalArgumentException("'other' is different type of Path");
// this path is the empty path
if (this.isEmpty())
return other;
int bn = this.getNameCount();
int cn = other.getNameCount();
// skip matching names
int n = (bn > cn) ? cn : bn;
int i = 0;
while (i < n) {
if (!this.getName(i).equals(other.getName(i)))
break;
i++;
}
int dotdots = bn - i;
if (i < cn) {
// remaining name components in other
UnixPath remainder = other.subpath(i, cn);
if (dotdots == 0)
return remainder;
// other is the empty path
boolean isOtherEmpty = other.isEmpty();
// result is a "../" for each remaining name in base
// followed by the remaining names in other. If the remainder is
// the empty path then we don't add the final trailing slash.
int len = dotdots*3 + remainder.path.length;
if (isOtherEmpty) {
assert remainder.isEmpty();
len--;
}
byte[] result = new byte[len];
int pos = 0;
while (dotdots > 0) {
result[pos++] = (byte)'.';
result[pos++] = (byte)'.';
if (isOtherEmpty) {
if (dotdots > 1) result[pos++] = (byte)'/';
} else {
result[pos++] = (byte)'/';
}
dotdots--;
}
System.arraycopy(remainder.path, 0, result, pos, remainder.path.length);
return new UnixPath(getFileSystem(), result);
} else {
// no remaining names in other so result is simply a sequence of ".."
byte[] result = new byte[dotdots*3 - 1];
int pos = 0;
while (dotdots > 0) {
result[pos++] = (byte)'.';
result[pos++] = (byte)'.';
// no tailing slash at the end
if (dotdots > 1)
result[pos++] = (byte)'/';
dotdots--;
}
return new UnixPath(getFileSystem(), result);
}
}
@Override
public Path normalize() {
final int count = getNameCount();
if (count == 0)
return this;
boolean[] ignore = new boolean[count]; // true => ignore name
int[] size = new int[count]; // length of name
int remaining = count; // number of names remaining
boolean hasDotDot = false; // has at least one ..
boolean isAbsolute = isAbsolute();
// first pass:
// 1. compute length of names
// 2. mark all occurences of "." to ignore
// 3. and look for any occurences of ".."
for (int i=0; i<count; i++) {
int begin = offsets[i];
int len;
if (i == (offsets.length-1)) {
len = path.length - begin;
} else {
len = offsets[i+1] - begin - 1;
}
size[i] = len;
if (path[begin] == '.') {
if (len == 1) {
ignore[i] = true; // ignore "."
remaining--;
}
else {
if (path[begin+1] == '.') // ".." found
hasDotDot = true;
}
}
}
// multiple passes to eliminate all occurences of name/..
if (hasDotDot) {
int prevRemaining;
do {
prevRemaining = remaining;
int prevName = -1;
for (int i=0; i<count; i++) {
if (ignore[i])
continue;
// not a ".."
if (size[i] != 2) {
prevName = i;
continue;
}
int begin = offsets[i];
if (path[begin] != '.' || path[begin+1] != '.') {
prevName = i;
continue;
}
// ".." found
if (prevName >= 0) {
// name/<ignored>/.. found so mark name and ".." to be
// ignored
ignore[prevName] = true;
ignore[i] = true;
remaining = remaining - 2;
prevName = -1;
} else {
// Case: /<ignored>/.. so mark ".." as ignored
if (isAbsolute) {
boolean hasPrevious = false;
for (int j=0; j<i; j++) {
if (!ignore[j]) {
hasPrevious = true;
break;
}
}
if (!hasPrevious) {
// all proceeding names are ignored
ignore[i] = true;
remaining--;
}
}
}
}
} while (prevRemaining > remaining);
}
// no redundant names
if (remaining == count)
return this;
// corner case - all names removed
if (remaining == 0) {
return isAbsolute ? getFileSystem().rootDirectory() : emptyPath();
}
// compute length of result
int len = remaining - 1;
if (isAbsolute)
len++;
for (int i=0; i<count; i++) {
if (!ignore[i])
len += size[i];
}
byte[] result = new byte[len];
// copy names into result
int pos = 0;
if (isAbsolute)
result[pos++] = '/';
for (int i=0; i<count; i++) {
if (!ignore[i]) {
System.arraycopy(path, offsets[i], result, pos, size[i]);
pos += size[i];
if (--remaining > 0) {
result[pos++] = '/';
}
}
}
return new UnixPath(getFileSystem(), result);
}
@Override
public boolean startsWith(Path other) {
if (!(Objects.requireNonNull(other) instanceof UnixPath))
return false;
UnixPath that = (UnixPath)other;
// other path is longer
if (that.path.length > path.length)
return false;
int thisOffsetCount = getNameCount();
int thatOffsetCount = that.getNameCount();
// other path has no name elements
if (thatOffsetCount == 0 && this.isAbsolute()) {
return that.isEmpty() ? false : true;
}
// given path has more elements that this path
if (thatOffsetCount > thisOffsetCount)
return false;
// same number of elements so must be exact match
if ((thatOffsetCount == thisOffsetCount) &&
(path.length != that.path.length)) {
return false;
}
// check offsets of elements match
for (int i=0; i<thatOffsetCount; i++) {
Integer o1 = offsets[i];
Integer o2 = that.offsets[i];
if (!o1.equals(o2))
return false;
}
// offsets match so need to compare bytes
int i=0;
while (i < that.path.length) {
if (this.path[i] != that.path[i])
return false;
i++;
}
// final check that match is on name boundary
if (i < path.length && this.path[i] != '/')
return false;
return true;
}
@Override
public boolean endsWith(Path other) {
if (!(Objects.requireNonNull(other) instanceof UnixPath))
return false;
UnixPath that = (UnixPath)other;
int thisLen = path.length;
int thatLen = that.path.length;
// other path is longer
if (thatLen > thisLen)
return false;
// other path is the empty path
if (thisLen > 0 && thatLen == 0)
return false;
// other path is absolute so this path must be absolute
if (that.isAbsolute() && !this.isAbsolute())
return false;
int thisOffsetCount = getNameCount();
int thatOffsetCount = that.getNameCount();
// given path has more elements that this path
if (thatOffsetCount > thisOffsetCount) {
return false;
} else {
// same number of elements
if (thatOffsetCount == thisOffsetCount) {
if (thisOffsetCount == 0)
return true;
int expectedLen = thisLen;
if (this.isAbsolute() && !that.isAbsolute())
expectedLen--;
if (thatLen != expectedLen)
return false;
} else {
// this path has more elements so given path must be relative
if (that.isAbsolute())
return false;
}
}
// compare bytes
int thisPos = offsets[thisOffsetCount - thatOffsetCount];
int thatPos = that.offsets[0];
if ((thatLen - thatPos) != (thisLen - thisPos))
return false;
while (thatPos < thatLen) {
if (this.path[thisPos++] != that.path[thatPos++])
return false;
}
return true;
}
*/
public int compareTo(CommonPath other) {
return this.path.compareTo(other.path);
}
@Override
public boolean equals(Object ob) {
if ((ob != null) && (ob instanceof CommonPath)) {
return (this.compareTo((CommonPath)ob) == 0);
}
return false;
}
@Override
public int hashCode() {
return this.path.hashCode();
}
@Override
public String toString() {
return this.path;
}
public String getFull() {
return this.path;
}
@Override
public Object exposeData() {
return this.path;
}
}