/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.brixcms;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
/**
* The {@link Path} class represents a path that is a string of segments joined together by a <code>/</code> separator.
* The {@link Path} class provides various operations that can be performed on such a string of segments. Path is
* immutable.
*
* @author igor.vaynberg
*/
public final class Path implements Iterable<String>, Serializable {
private static final String SEPARATOR = "/";
public static final Path ROOT = new Path(SEPARATOR);
public static final Comparator<Path> SMALLEST_FIRST_COMPARATOR = new Comparator<Path>() {
public int compare(Path o1, Path o2) {
if (o1 == o2) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
int s1 = o1.size();
int s2 = o2.size();
if (s1 == s2) {
return o1.path.compareTo(o2.path);
} else {
return o1.size() - o2.size();
}
}
}
};
public static final Comparator<Path> LARGEST_FIRST_COMPARATOR = new Comparator<Path>() {
public int compare(Path o1, Path o2) {
if (o1 == o2) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
int s1 = o1.size();
int s2 = o2.size();
if (s1 == s2) {
return o1.path.compareTo(o2.path);
} else {
return o1.size() - o2.size();
}
}
}
};
private final String path;
public Path(String path) {
this(path, true);
}
public Path(String path, boolean canonize) {
if (path == null || path.length() == 0) {
throw new IllegalArgumentException("Argument 'path' cannot be null or empty");
}
if (canonize) {
path = new Path(path, false).canonical().path;
}
if (!path.equals("/") && path.endsWith(SEPARATOR)) {
this.path = path.substring(0, path.length() - 1);
} else {
this.path = path;
}
}
public Path canonical() {
if (isCanonical()) {
return this;
} else {
return doCanonical();
}
}
public boolean isCanonical() {
int offset = 0;
// whether a text segment (not "..") has been found
boolean text = false;
// until empty paths are supported
if (path.equals(".")) {
return true;
}
if (!isRoot() && path.endsWith("/")) {
return false;
}
while (offset < path.length()) {
String sub = path.substring(offset);
if (sub.equals("/") || sub.equals("")) {
break;
}
if (sub.startsWith("./") || sub.equals(".")) {
return false;
}
boolean up = sub.startsWith("../") || sub.equals("..");
if (text && up) {
return false;
}
if (up == false) {
text = true;
}
int next = sub.indexOf("/");
if (next == -1) {
break;
} else if (next == 0 && offset != 0) {
// situation when there are // in the middle of string
return false;
} else {
offset += next + 1;
}
}
return true;
}
public boolean isRoot() {
return path.equals(SEPARATOR);
}
private Path doCanonical() {
boolean absolute = isAbsolute();
int prepend = 0;
List<String> segments = new ArrayList<String>(10);
for (String segment : this) {
if (segment.equals("") || segment.equals(".")) {
} else if (segment.equals("..")) {
if (segments.size() > 0) {
segments.remove(segments.size() - 1);
} else {
++prepend;
}
} else {
segments.add(segment);
}
}
StringBuilder res = new StringBuilder(path.length());
while (absolute == false && prepend > 0) {
res.append("../");
--prepend;
}
if (absolute == true) {
res.append("/");
}
for (int i = 0; i < segments.size(); ++i) {
res.append(segments.get(i));
if (i != segments.size() - 1) {
res.append("/");
}
}
if (res.length() == 0) {
return new Path(".");
} else {
return new Path(res.toString());
}
}
public boolean isAbsolute() {
return path.startsWith("/");
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Path other = (Path) obj;
if (path == null) {
if (other.path != null) {
return false;
}
} else {
return path.equals(other.path);
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((path == null) ? 0 : path.hashCode());
return result;
}
@Override
public String toString() {
return path;
}
public Iterator<String> iterator() {
return new Iterator<String>() {
int idx = 0;
public boolean hasNext() {
return idx < size();
}
public String next() {
return part(idx++);
}
public void remove() {
throw new UnsupportedOperationException("Path parts iterator is read only");
}
};
}
public String getName() {
if (path.equals(SEPARATOR)) {
return path;
} else {
int last = path.lastIndexOf(SEPARATOR);
return path.substring(last + 1);
}
}
public boolean isAncestorOf(Path other) {
return other.isDescendantOf(this);
}
public boolean isDescendantOf(Path other) {
if (other.isRoot()) {
if (isRoot()) {
return false;
} else if (isAbsolute()) {
return true;
} else {
return false;
}
} else {
return path.startsWith(other.path + SEPARATOR) && path.length() > other.path.length();
}
}
public boolean isParentOf(Path other) {
return other.isChildOf(this);
}
public boolean isChildOf(Path other) {
return isDescendantOf(other) && size() == other.size() + 1;
}
public int size() {
if (isRoot()) {
return 0;
}
int size = 0;
int pos = 0;
while (pos >= 0) {
size++;
pos = path.indexOf(SEPARATOR, pos + 1);
}
return size;
}
public Path parent() {
if (isRoot()) {
return null;
} else {
return new Path(path + "/..");
}
}
public String part(int index) {
if (index < 0) {
throw new IndexOutOfBoundsException();
}
int start = isAbsolute() ? 1 : 0;
for (int i = 0; i < index; i++) {
start = path.indexOf(SEPARATOR, start);
if (start < 0) {
throw new IndexOutOfBoundsException();
}
start++;
}
int end = path.indexOf(SEPARATOR, start);
if (end < 0) {
end = path.length();
}
return path.substring(start, end);
}
public Path subpath(int idx) {
if (idx <= 0) {
throw new IndexOutOfBoundsException();
}
final int size = size();
final boolean abs = isAbsolute();
int chunks = (abs) ? size + 1 : size;
if (idx > chunks) {
throw new IndexOutOfBoundsException();
}
if (idx == chunks) {
// shortcut in case we want the entire path
return this;
}
int iterations = ((abs) ? idx - 1 : idx);
int end = 1;
for (int i = 0; i < iterations; i++) {
end = path.indexOf(SEPARATOR, end + 1);
}
if (end == 0) {
return new Path("/");
}
String newPath = path.substring(0, end);
return new Path(newPath);
}
public Path toAbsolute() {
if (!isAbsolute()) {
return ROOT.append(this);
} else {
return this;
}
}
public Path append(Path relative) {
if (relative == null) {
throw new IllegalArgumentException("Argument 'relative' cannot be null");
}
if (relative.isAbsolute()) {
throw new IllegalArgumentException("Cannot append an absolute path");
}
StringBuilder appended = new StringBuilder(path.length() + 1 + relative.path.length());
appended.append(path);
if (!path.endsWith(SEPARATOR)) {
appended.append("/");
}
appended.append(relative.path);
return new Path(appended.toString());
}
public Path toRelative(Path ancestor) {
if (isRoot()) {
throw new IllegalStateException("Cannot make root path relative");
}
if (!isDescendantOf(ancestor)) {
throw new IllegalArgumentException("Cannot create relative path because this path: " +
this + " is not descendant of ancestor argument: " + ancestor);
}
Path fragment = new Path(path.substring(ancestor.path.length()), false);
if (fragment.isAbsolute()) {
fragment = fragment.toRelative(new Path("/"));
}
return fragment;
}
}