blob: 03822fb155226d09b926c951406f33ff1877f7cb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.oak.commons;
import static com.google.common.collect.Sets.newHashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Utility methods to parse a path.
* <p>
* Each method validates the input, except if the system property
* {packageName}.SKIP_VALIDATION is set, in which case only minimal validation
* takes place within this function, so when the parameter is an illegal path,
* the the result of this method is undefined.
*/
public final class PathUtils {
public static final String ROOT_PATH = "/";
public static final String ROOT_NAME = "";
private static final Pattern SNS_PATTERN =
Pattern.compile("(.+)\\[[1-9][0-9]*\\]$");
private PathUtils() {
// utility class
}
/**
* Whether the path is the root path ("/").
*
* @param path the path
* @return whether this is the root
*/
public static boolean denotesRoot(String path) {
assert isValid(path) : "Invalid path ["+path+"]";
return denotesRootPath(path);
}
private static boolean denotesRootPath(String path) {
return ROOT_PATH.equals(path);
}
/**
* @param element The path segment to check for being the current element
* @return {@code true} if the specified element equals "."; {@code false} otherwise.
*/
public static boolean denotesCurrent(String element) {
return ".".equals(element);
}
/**
* @param element The path segment to check for being the parent element
* @return {@code true} if the specified element equals ".."; {@code false} otherwise.
*/
public static boolean denotesParent(String element) {
return "..".equals(element);
}
/**
* Whether the path is absolute (starts with a slash) or not.
*
* @param path the path
* @return true if it starts with a slash
*/
public static boolean isAbsolute(String path) {
assert isValid(path) : "Invalid path ["+path+"]";
return isAbsolutePath(path);
}
private static boolean isAbsolutePath(String path) {
return !path.isEmpty() && path.charAt(0) == '/';
}
/**
* Get the parent of a path. The parent of the root path ("/") is the root
* path.
*
* @param path the path
* @return the parent path
*/
@NotNull
public static String getParentPath(String path) {
return getAncestorPath(path, 1);
}
/**
* Get the nth ancestor of a path. The 1st ancestor is the parent path,
* 2nd ancestor the grandparent path, and so on...
* <p>
* If {@code nth <= 0}, the path argument is returned as is.
*
* @param path the path
* @param nth indicates the ancestor level for which the path should be
* calculated.
* @return the ancestor path
*/
@NotNull
public static String getAncestorPath(String path, int nth) {
assert isValid(path) : "Invalid path ["+path+"]";
if (path.isEmpty() || denotesRootPath(path)
|| nth <= 0) {
return path;
}
int end = path.length() - 1;
int pos = -1;
while (nth-- > 0) {
pos = path.lastIndexOf('/', end);
if (pos > 0) {
end = pos - 1;
} else if (pos == 0) {
return ROOT_PATH;
} else {
return "";
}
}
return path.substring(0, pos);
}
/**
* Get the last element of the (absolute or relative) path. The name of the
* root node ("/") and the name of the empty path ("") is the empty path.
*
* @param path the complete path
* @return the last element
*/
@NotNull
public static String getName(String path) {
assert isValid(path) : "Invalid path ["+path+"]";
if (path.isEmpty() || denotesRootPath(path)) {
return ROOT_NAME;
}
int end = path.length() - 1;
int pos = path.lastIndexOf('/', end);
if (pos != -1) {
return path.substring(pos + 1, end + 1);
}
return path;
}
/**
* Returns the given name without the possible SNS index suffix. If the
* name does not contain an SNS index, then it is returned as-is.
*
* @param name name with a possible SNS index suffix
* @return name without the SNS index suffix
*/
@NotNull
public static String dropIndexFromName(@NotNull String name) {
if (!name.endsWith("]")) {
return name;
}
Matcher matcher = SNS_PATTERN.matcher(name);
if (matcher.matches()) {
return matcher.group(1);
}
return name;
}
/**
* Calculate the number of elements in the path. The root path has zero
* elements.
*
* @param path the path
* @return the number of elements
*/
public static int getDepth(String path) {
assert isValid(path) : "Invalid path ["+path+"]";
if (path.isEmpty()) {
return 0;
}
int count = 1, i = 0;
if (isAbsolutePath(path)) {
if (denotesRootPath(path)) {
return 0;
}
i++;
}
while (true) {
i = path.indexOf('/', i) + 1;
if (i == 0) {
return count;
}
count++;
}
}
/**
* Returns an {@code Iterable} for the path elements. The root path ("/") and the
* empty path ("") have zero elements.
*
* @param path the path
* @return an Iterable for the path elements
*/
@NotNull
public static Iterable<String> elements(final String path) {
assert isValid(path) : "Invalid path ["+path+"]";
return new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
int pos = isAbsolute(path) ? 1 : 0;
String next;
@Override
public boolean hasNext() {
if (next == null) {
if (pos >= path.length()) {
return false;
}
int i = path.indexOf('/', pos);
if (i < 0) {
next = path.substring(pos);
pos = path.length();
} else {
next = path.substring(pos, i);
pos = i + 1;
}
}
return true;
}
@Override
public String next() {
if (hasNext()) {
String next = this.next;
this.next = null;
return next;
}
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
}
};
}
/**
* Concatenate path elements.
*
* @param parentPath the parent path
* @param relativePaths the relative path elements to add
* @return the concatenated path
*/
@NotNull
public static String concat(String parentPath, String... relativePaths) {
assert isValid(parentPath) : "Invalid parent path ["+parentPath+"]";
int parentLen = parentPath.length();
int size = relativePaths.length;
StringBuilder buff = new StringBuilder(parentLen + size * 5);
buff.append(parentPath);
boolean needSlash = parentLen > 0 && !denotesRootPath(parentPath);
for (String s : relativePaths) {
assert isValid(s);
if (isAbsolutePath(s)) {
throw new IllegalArgumentException("Cannot append absolute path " + s);
}
if (!s.isEmpty()) {
if (needSlash) {
buff.append('/');
}
buff.append(s);
needSlash = true;
}
}
return buff.toString();
}
/**
* Concatenate path elements.
*
* @param parentPath the parent path
* @param subPath the subPath path to add
* @return the concatenated path
*/
@NotNull
public static String concat(String parentPath, String subPath) {
assert isValid(parentPath) : "Invalid parent path ["+parentPath+"]";
assert isValid(subPath) : "Invalid sub path ["+subPath+"]";
// special cases
if (parentPath.isEmpty()) {
return subPath;
} else if (subPath.isEmpty()) {
return parentPath;
} else if (isAbsolutePath(subPath)) {
throw new IllegalArgumentException("Cannot append absolute path " + subPath);
}
StringBuilder buff = new StringBuilder(parentPath.length() + subPath.length() + 1);
buff.append(parentPath);
if (!denotesRootPath(parentPath)) {
buff.append('/');
}
buff.append(subPath);
return buff.toString();
}
/**
* Relative path concatenation.
*
* @param relativePaths relative paths
* @return the concatenated path or {@code null} if the resulting path is empty.
*/
@Nullable
public static String concatRelativePaths(String... relativePaths) {
StringBuilder result = new StringBuilder();
for (String path : relativePaths) {
if (path != null && !path.isEmpty()) {
int i0 = 0;
int i1 = path.length();
while (i0 < i1 && path.charAt(i0) == '/') {
i0++;
}
while (i1 > i0 && path.charAt(i1-1) == '/') {
i1--;
}
if (i1 > i0) {
if (result.length() > 0) {
result.append('/');
}
result.append(path.substring(i0, i1));
}
}
}
return result.length() == 0 ? null : result.toString();
}
/**
* Check if a path is a (direct or indirect) ancestor of another path.
*
* @param ancestor the ancestor path
* @param path the potential offspring path
* @return true if the path is an offspring of the ancestor
*/
public static boolean isAncestor(String ancestor, String path) {
assert isValid(ancestor) : "Invalid parent path ["+ancestor+"]";
assert isValid(path) : "Invalid path ["+path+"]";
if (ancestor.isEmpty() || path.isEmpty()) {
return false;
}
if (denotesRoot(ancestor)) {
if (denotesRoot(path)) {
return false;
}
} else {
ancestor += "/";
}
return path.startsWith(ancestor);
}
/**
* Relativize a path wrt. a parent path such that
* {@code relativize(parentPath, concat(parentPath, path)) == paths}
* holds.
*
* @param parentPath parent pth
* @param path path to relativize
* @return relativized path
*/
@NotNull
public static String relativize(String parentPath, String path) {
assert isValid(parentPath) : "Invalid parent path ["+parentPath+"]";
assert isValid(path) : "Invalid path ["+path+"]";
if (parentPath.equals(path)) {
return "";
}
String prefix = denotesRootPath(parentPath)
? parentPath
: parentPath + '/';
if (path.startsWith(prefix)) {
return path.substring(prefix.length());
}
throw new IllegalArgumentException("Cannot relativize " + path + " wrt. " + parentPath);
}
/**
* Get the index of the next slash.
*
* @param path the path
* @param index the starting index
* @return the index of the next slash (possibly the starting index), or -1
* if not found
*/
public static int getNextSlash(String path, int index) {
assert isValid(path) : "Invalid path ["+path+"]";
return path.indexOf('/', index);
}
/**
* Check if the path is valid, and throw an IllegalArgumentException if not.
* A valid path is absolute (starts with a '/') or relative (doesn't start
* with '/'), and contains none or more elements. A path may not end with
* '/', except for the root path. Elements itself must be at least one
* character long.
*
* @param path the path
*/
public static void validate(String path) {
if (path.isEmpty() || denotesRootPath(path)) {
return;
} else if (path.charAt(path.length() - 1) == '/') {
throw new IllegalArgumentException("Path may not end with '/': " + path);
}
char last = 0;
for (int index = 0, len = path.length(); index < len; index++) {
char c = path.charAt(index);
if (c == '/') {
if (last == '/') {
throw new IllegalArgumentException("Path may not contains '//': " + path);
}
}
last = c;
}
}
/**
* Check if the path is valid. A valid path is absolute (starts with a '/')
* or relative (doesn't start with '/'), and contains none or more elements.
* A path may not end with '/', except for the root path. Elements itself must
* be at least one character long.
*
* @param path the path
* @return {@code true} iff the path is valid.
*/
public static boolean isValid(String path) {
if (path.isEmpty() || denotesRootPath(path)) {
return true;
} else if (path.charAt(path.length() - 1) == '/') {
return false;
}
char last = 0;
for (int index = 0, len = path.length(); index < len; index++) {
char c = path.charAt(index);
if (c == '/') {
if (last == '/') {
return false;
}
}
last = c;
}
return true;
}
/**
* Unify path inclusions and exclusions.
* <ul>
* <li>A path in {@code includePaths} is only retained if {@code includePaths} contains
* none of its ancestors and {@code excludePaths} contains neither of its ancestors nor
* that path itself.</li>
* <li>A path in {@code excludePaths} is only retained if {@code includePaths} contains
* an ancestor of that path.</li>
* </ul>
*
* When a set of paths is <em>filtered wrt.</em> {@code includePaths} and {@code excludePaths}
* by first excluding all paths that have an ancestor or are contained in {@code excludePaths}
* and then including all paths that have an ancestor or are contained in {@code includePaths}
* then the result is the same regardless whether the {@code includePaths} and
* {@code excludePaths} sets have been run through this method or not.
*
* @param includePaths set of paths to be included
* @param excludedPaths set of paths to be excluded
*/
public static void unifyInExcludes(Set<String> includePaths, Set<String> excludedPaths) {
Set<String> retain = newHashSet();
Set<String> includesRemoved = newHashSet();
for (String include : includePaths) {
for (String exclude : excludedPaths) {
if (exclude.equals(include) || isAncestor(exclude, include)) {
includesRemoved.add(include);
} else if (isAncestor(include, exclude)) {
retain.add(exclude);
}
}
//Remove redundant includes /a, /a/b -> /a
for (String include2 : includePaths) {
if (isAncestor(include, include2)) {
includesRemoved.add(include2);
}
}
}
includePaths.removeAll(includesRemoved);
excludedPaths.retainAll(retain);
}
}