All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xbib.io.ftp.fs.SimpleAbstractPath Maven / Gradle / Ivy

There is a newer version: 2.6.0
Show newest version
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(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy