org.xbib.io.ftp.fs.SimpleAbstractPath Maven / Gradle / Ivy
Show all versions of ftp-fs Show documentation
package org.xbib.io.ftp.fs;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.Objects;
/**
* This class provides a base implementation of the {@link Path} interface that uses a string to store the actual path.
* This class can be used to minimize the effort required to implement the {@code Path} interface.
* Note that this class assumes that the file system uses a single forward slash ({@code /}) as its {@link FileSystem#getSeparator() separator}.
*/
public abstract class SimpleAbstractPath extends AbstractPath {
private static final String ROOT_PATH = "/";
private static final String EMPTY_PATH = "";
private static final String CURRENT_DIR = ".";
private static final String PARENT_DIR = "..";
/**
* The full path.
*/
private final String path;
/**
* The offsets in the full path of all the separate name elements.
*/
private int[] offsets;
/**
* Creates a new path.
*
* @param path The actual path.
*/
protected SimpleAbstractPath(String path) {
this(path, false);
}
/**
* Creates a new path.
*
* @param path The actual path.
* @param normalized If not {@code true}, the path will be normalized (e.g. by removing redundant forward slashes).
*/
protected SimpleAbstractPath(String path, boolean normalized) {
Objects.requireNonNull(path);
this.path = normalized ? path : normalize(path);
}
/**
* Normalizes the given path by removing redundant forward slashes and checking for invalid characters.
*/
private String normalize(String path) {
if (path.isEmpty()) {
return path;
}
StringBuilder sb = new StringBuilder(path.length());
char prev = '\0';
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c == '/' && prev == '/') {
continue;
}
if (c == '\0') {
throw Messages.path().nulCharacterNotAllowed(path);
}
sb.append(c);
prev = c;
}
if (sb.length() > 1 && sb.charAt(sb.length() - 1) == '/') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
/**
* Creates a new path. Implementations should create instances of the implementing class.
*
* @param path The actual path for the new path. This will already be normalized when called by the implementations of this class.
* @return The created path.
*/
protected abstract SimpleAbstractPath createPath(String path);
/**
* Returns the actual path.
*
* @return The actual path.
*/
public final String path() {
return path;
}
/**
* Returns the name at the given index. This method is similar to {@link #getName(int)} but returns the name as a string, not a {@link Path}.
*
* @param index The index of the name.
* @return The name at the given index.
* @throws IllegalArgumentException If the index is invalid.
*/
public final String nameAt(int index) {
initOffsets();
if (index < 0 || index >= offsets.length) {
throw Messages.invalidIndex(index);
}
final int begin = begin(index);
final int end = end(index);
return path.substring(begin, end);
}
/**
* Returns the file name. This method is similar to {@link #getFileName()} but returns the file name as a string, not a {@link Path}.
*
* @return The file name, or {@code null} if there is no file name.
*/
public final String fileName() {
initOffsets();
return offsets.length == 0 ? null : nameAt(offsets.length - 1);
}
/**
* Tells whether or not this path is absolute.
*
* This implementation returns {@code true} if the path starts with a forward slash, or {@code false} otherwise.
*/
@Override
public boolean isAbsolute() {
return path.startsWith(ROOT_PATH);
}
/**
* Returns the root path. This method is similar to {@link #getRoot()} but returns the root as a string, not a {@link Path}.
*
* @return The root path, or {@code null} if this path is relative.
*/
public final String rootPath() {
return isAbsolute() ? ROOT_PATH : null;
}
/**
* Returns the root component of this path as a {@code Path} object, or {@code null} if this path does not have a root component.
*
* This implementation returns a path {@link #createPath(String) created} with a single forward slash as its path if this path is absolute,
* or {@code null} otherwise.
*/
@Override
public Path getRoot() {
return isAbsolute() ? createPath(ROOT_PATH) : null;
}
/**
* Returns the parent path. This method is similar to {@link #getParent()} but returns the parent as a string, not a {@link Path}.
*
* @return The parent, or {@code null} if this path has no parent.
*/
public final String parentPath() {
initOffsets();
final int count = offsets.length;
if (count == 0) {
return null;
}
final int end = offsets[count - 1] - 1;
if (end <= 0) {
// The parent is the root (possibly null)
return rootPath();
}
return path.substring(0, end);
}
/**
* Returns the parent path, or {@code null} if this path does not have a parent.
*
* This implementation returns:
*
* - {@code null} if this path has no name elements.
* - {@link #getRoot()} if this path has only one name element.
* - A path {@link #createPath(String) created} with this path's path up until the last forward slash otherwise.
*
*/
@Override
public Path getParent() {
initOffsets();
String parentPath = parentPath();
return parentPath != null ? createPath(parentPath) : null;
}
/**
* Returns the number of name elements in the path.
*
* This implementation returns a value calculated from the number of forward slashes in the actual path.
*/
@Override
public int getNameCount() {
initOffsets();
return offsets.length;
}
/**
* Returns a relative {@code Path} that is a subsequence of the name elements of this path.
*
* This implementation returns a non-absolute path {@link #createPath(String) created} with a path that is the appropriate substring of this
* path's actual path.
*/
@Override
public Path subpath(int beginIndex, int endIndex) {
initOffsets();
if (beginIndex < 0 || beginIndex >= offsets.length
|| endIndex <= beginIndex || endIndex > offsets.length) {
throw Messages.invalidRange(beginIndex, endIndex);
}
final int begin = begin(beginIndex);
final int end = end(endIndex - 1);
final String subpath = path.substring(begin, end);
return createPath(subpath);
}
/**
* Tests if this path starts with the given path.
*
* This implementation will first check if the two paths have the same {@link #getFileSystem() FileSystem} and class.
* If not, {@code false} is returned.
* It will then check if the actual path of this path starts with the actual path of the given path.
*/
@Override
public boolean startsWith(Path other) {
if (getFileSystem() != other.getFileSystem() || getClass() != other.getClass()) {
return false;
}
final SimpleAbstractPath that = (SimpleAbstractPath) other;
if (that.path.isEmpty()) {
return path.isEmpty();
}
if (ROOT_PATH.equals(that.path)) {
return isAbsolute();
}
if (!path.startsWith(that.path)) {
return false;
}
return path.length() == that.path.length() || path.charAt(that.path.length()) == '/';
}
/**
* Tests if this path starts with the given path.
*
* This implementation will first check if the two paths have the same {@link #getFileSystem() FileSystem} and class.
* If not, {@code false} is returned.
* It will then check if the actual path of this path ends with the actual path of the given path.
*/
@Override
public boolean endsWith(Path other) {
if (getFileSystem() != other.getFileSystem() || getClass() != other.getClass()) {
return false;
}
final SimpleAbstractPath that = (SimpleAbstractPath) other;
if (that.path.isEmpty()) {
return path.isEmpty();
}
if (that.isAbsolute()) {
return path.equals(that.path);
}
if (!path.endsWith(that.path)) {
return false;
}
return path.length() == that.path.length() || path.charAt(path.length() - that.path.length() - 1) == '/';
}
/**
* Returns a path that is this path with redundant name elements eliminated.
*
* This implementation will go over the name elements, removing all occurrences of single dots ({@code .}).
* For any occurrence of a double dot ({@code ..}), any previous element (if any) is removed as well.
* With the remaining name elements, a new path is {@link #createPath(String) created}.
*/
@Override
public Path normalize() {
int count = getNameCount();
if (count == 0) {
return this;
}
Deque nameElements = new ArrayDeque<>(count);
int nonParentCount = 0;
for (int i = 0; i < count; i++) {
if (equalsNameAt(CURRENT_DIR, i)) {
continue;
}
boolean isParent = equalsNameAt(PARENT_DIR, i);
// If this is a parent and there is at least one non-parent, pop it.
if (isParent && nonParentCount > 0) {
nameElements.pollLast();
nonParentCount--;
continue;
}
if (!isAbsolute() || !isParent) {
// For non-absolute paths, this may add a parent if there are only parents, but that's OK.
// Example: foo/../../bar will lead to ../bar
// For absolute paths, any leading .. will not be included though.
String nameElement = nameAt(i);
nameElements.addLast(nameElement);
}
if (!isParent) {
nonParentCount++;
}
}
StringBuilder sb = new StringBuilder(path.length());
if (isAbsolute()) {
sb.append('/');
}
for (Iterator i = nameElements.iterator(); i.hasNext(); ) {
sb.append(i.next());
if (i.hasNext()) {
sb.append('/');
}
}
return createPath(sb.toString());
}
private boolean equalsNameAt(String name, int index) {
final int thisBegin = begin(index);
final int thisEnd = end(index);
final int thisLength = thisEnd - thisBegin;
if (thisLength != name.length()) {
return false;
}
return path.regionMatches(thisBegin, name, 0, thisLength);
}
/**
* Resolve the given path against this path.
*
* This implementation returns the given path if it's {@link Path#isAbsolute() absolute} or if this path has no name elements,
* this path if the given path has no name elements,
* or a path {@link #createPath(String) created} with the paths of this path and the given path joined with a forward slash otherwise.
*/
@Override
public Path resolve(Path other) {
final SimpleAbstractPath that = checkPath(other);
if (path.isEmpty() || that.isAbsolute()) {
return that;
}
if (that.path.isEmpty()) {
return this;
}
final String resolvedPath;
if (path.endsWith("/")) {
resolvedPath = path + that.path;
} else {
resolvedPath = path + "/" + that.path; //
}
return createPath(resolvedPath);
}
/**
* Constructs a relative path between this path and a given path.
*
* This implementation skips past any shared name elements, then adds as many occurrences of double dots ({@code ..}) as needed, then adds
* the remainder of the given path to the result.
*/
@Override
public Path relativize(Path other) {
final SimpleAbstractPath that = checkPath(other);
if (this.equals(that)) {
return createPath(EMPTY_PATH);
}
if (isAbsolute() != that.isAbsolute()) {
throw Messages.path().relativizeAbsoluteRelativeMismatch();
}
if (path.isEmpty()) {
return other;
}
final int thisNameCount = getNameCount();
final int thatNameCount = that.getNameCount();
final int nameCount = Math.min(thisNameCount, thatNameCount);
int index = 0;
while (index < nameCount) {
if (!equalsNameAt(that, index)) {
break;
}
index++;
}
final int parentDirs = thisNameCount - index;
int length = parentDirs * 3 - 1;
if (index < thatNameCount) {
length += that.path.length() - that.offsets[index] + 1;
}
final StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < parentDirs; i++) {
sb.append(PARENT_DIR);
if (i < length) {
sb.append('/');
}
// Else don't add a trailing slash at the end
}
if (index < thatNameCount) {
sb.append(that.path, that.offsets[index], that.path.length());
}
return createPath(sb.toString());
}
private boolean equalsNameAt(SimpleAbstractPath that, int index) {
final int thisBegin = begin(index);
final int thisEnd = end(index);
final int thisLength = thisEnd - thisBegin;
final int thatBegin = that.begin(index);
final int thatEnd = that.end(index);
final int thatLength = thatEnd - thatBegin;
if (thisLength != thatLength) {
return false;
}
return path.regionMatches(thisBegin, that.path, thatBegin, thisLength);
}
/**
* Compares two abstract paths lexicographically.
*
* This implementation checks if the given path is an instance of the same class, then compares the actual paths of the two abstract paths.
*/
@Override
public int compareTo(Path other) {
Objects.requireNonNull(other);
final SimpleAbstractPath that = getClass().cast(other);
return path.compareTo(that.path);
}
/**
* Tests this path for equality with the given object.
*
* This implementation will return {@code true} if the given object is an instance of the same class as this path, with the same file system,
* and with the same actual path.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
SimpleAbstractPath other = (SimpleAbstractPath) obj;
return getFileSystem() == other.getFileSystem()
&& path.equals(other.path);
}
@Override
public int hashCode() {
return path.hashCode();
}
/**
* Returns the string representation of this path.
*
* This implementation only returns the actual path.
*/
@Override
public String toString() {
return path;
}
private synchronized void initOffsets() {
if (offsets == null) {
if ("/".equals(path)) {
offsets = new int[0];
return;
}
boolean isAbsolute = isAbsolute();
// At least one result for non-root paths
int count = 1;
int start = isAbsolute ? 1 : 0;
while ((start = path.indexOf('/', start)) != -1) {
count++;
start++;
}
int[] result = new int[count];
start = isAbsolute ? 1 : 0;
int index = 0;
result[index++] = start;
while ((start = path.indexOf('/', start)) != -1) {
start++;
result[index++] = start;
}
offsets = result;
}
}
private int begin(int index) {
return offsets[index];
}
private int end(int index) {
return index == offsets.length - 1 ? path.length() : offsets[index + 1] - 1;
}
private SimpleAbstractPath checkPath(Path path) {
Objects.requireNonNull(path);
if (getClass().isInstance(path)) {
return getClass().cast(path);
}
throw new ProviderMismatchException();
}
}