/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.resource;
import com.google.common.base.Objects;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.nullToEmpty;
/**
* Client side implementation for the resource path.
* <p/>
* A path is an ordered collection of string segments, separated by a
* standard separator character, "/". A path may also have a leading
* and/or a trailing separator.
* <p/>
* Note that paths are value objects; all operations on paths return
* a new path; the path that is operated on is unscathed.
* <p/>
* This class is not intended to be extended by clients.
*
* @author Vlad Zhukovskyi
* @since 4.0.0-RC7
*/
public final class Path {
/**
* Path separator character constant "/" used in paths.
*/
public static char SEPARATOR = '/';
/**
* Device separator character constant ":" used in paths.
*/
public static char DEVICE_SEPARATOR = ':';
/** masks for separator values */
private static final int HAS_LEADING = 1;
private static final int IS_UNC = 2;
private static final int HAS_TRAILING = 4;
private static final int ALL_SEPARATORS = HAS_LEADING | IS_UNC | HAS_TRAILING;
/** Constant empty string value. */
private static final String EMPTY_STRING = "";
/** Constant value indicating no segments */
private static final String[] NO_SEGMENTS = new String[0];
/** Constant value containing the empty path with no device. */
public static final Path EMPTY = new Path(EMPTY_STRING);
/** Mask for all bits that are involved in the hash code */
private static final int HASH_MASK = ~HAS_TRAILING;
/** Constant root path string (<code>"/"</code>). */
private static final String ROOT_STRING = "/";
/** Constant value containing the root path with no device. */
public static final Path ROOT = new Path(ROOT_STRING);
/** The device id string. May be null if there is no device. */
private String device = null;
/** The path segments */
private String[] segments;
/** flags indicating separators (has leading, is UNC, has trailing) */
private int separators;
/**
* Constructs a new path from the given string path.
* The string path must represent a valid file system path
* on the local file system.
* The path is canonicalized and double slashes are removed
* except at the beginning. (to handle UNC paths). All forward
* slashes ('/') are treated as segment delimiters, and any
* segment and device delimiters for the local file system are
* also respected.
*
* @param pathString
* the portable string path
* @since 4.0.0-RC5
*/
public static Path valueOf(String pathString) {
return new Path(pathString);
}
/* (Intentionally not included in javadoc)
* Private constructor.
*/
private Path() {
// not allowed
}
/**
* Constructs a new path from the given string path.
* The string path must represent a valid file system path
* on the local file system.
* The path is canonicalized and double slashes are removed
* except at the beginning. (to handle UNC paths). All forward
* slashes ('/') are treated as segment delimiters, and any
* segment and device delimiters for the local file system are
* also respected (such as colon (':') and backslash ('\') on some file systems).
*
* @param fullPath
* the string path
* @see #isValidPath(String)
* @since 4.0.0-RC5
*/
public Path(String fullPath) {
initialize(null, fullPath);
}
/**
* Constructs a new path from the given device id and string path.
* The given string path must be valid.
* The path is canonicalized and double slashes are removed except
* at the beginning (to handle UNC paths). All forward
* slashes ('/') are treated as segment delimiters, and any
* segment delimiters for the local file system are
* also respected (such as backslash ('\') on some file systems).
*
* @param device
* the device id
* @param path
* the string path
* @see #isValidPath(String)
* @see #setDevice(String)
* @since 4.0.0-RC5
*/
public Path(String device, String path) {
initialize(device, path);
}
/* (Intentionally not included in javadoc)
* Private constructor.
*/
private Path(String device, String[] segments, int _separators) {
// no segment validations are done for performance reasons
this.segments = segments;
this.device = device;
//hash code is cached in all but the bottom three bits of the separators field
this.separators = (computeHashCode() << 3) | (_separators & ALL_SEPARATORS);
}
/**
* Returns a new path which is the same as this path but with
* the given file extension added. If this path is empty, root or has a
* trailing separator, this path is returned. If this path already
* has an extension, the existing extension is left and the given
* extension simply appended. Clients wishing to replace
* the current extension should first remove the extension and
* then add the desired one.
* <p>
* The file extension portion is defined as the string
* following the last period (".") character in the last segment.
* The given extension should not include a leading ".".
* </p>
*
* @param extension
* the file extension to append
* @return the new path
* @see #getFileExtension()
* @since 4.0.0-RC5
*/
public Path addFileExtension(String extension) {
if (isRoot() || isEmpty() || hasTrailingSeparator())
return this;
int len = segments.length;
String[] newSegments = new String[len];
System.arraycopy(segments, 0, newSegments, 0, len - 1);
newSegments[len - 1] = segments[len - 1] + '.' + extension;
return new Path(device, newSegments, separators);
}
/**
* Returns a path with the same segments as this path
* but with a trailing separator added.
* This path must have at least one segment.
* <p>
* If this path already has a trailing separator,
* this path is returned.
* </p>
*
* @return the new path
* @see #hasTrailingSeparator()
* @see #removeTrailingSeparator()
* @since 4.0.0-RC5
*/
public Path addTrailingSeparator() {
if (hasTrailingSeparator() || isRoot()) {
return this;
}
if (isEmpty()) {
return new Path(device, segments, HAS_LEADING);
}
return new Path(device, segments, separators | HAS_TRAILING);
}
/**
* Returns the canonicalized path obtained from the
* concatenation of the given path's segments to the
* end of this path. If the given path has a trailing
* separator, the result will have a trailing separator.
* The device id of this path is preserved (the one
* of the given path is ignored). Duplicate slashes
* are removed from the path except at the beginning
* where the path is considered to be UNC.
*
* @param path
* the path to concatenate
* @return the new path
* @since 4.0.0-RC5
*/
public Path append(Path path) {
//optimize some easy cases
if (path == null || path.segmentCount() == 0)
return this;
//these call chains look expensive, but in most cases they are no-ops
if (this.isEmpty())
return path.setDevice(device).makeRelative().makeUNC(isUNC());
if (this.isRoot())
return path.setDevice(device).makeAbsolute().makeUNC(isUNC());
//concatenate the two segment arrays
int myLen = segments.length;
int tailLen = path.segmentCount();
String[] newSegments = new String[myLen + tailLen];
System.arraycopy(segments, 0, newSegments, 0, myLen);
for (int i = 0; i < tailLen; i++) {
newSegments[myLen + i] = path.segment(i);
}
//use my leading separators and the tail's trailing separator
Path result = new Path(device, newSegments,
(separators & (HAS_LEADING | IS_UNC)) | (path.hasTrailingSeparator() ? HAS_TRAILING : 0));
String tailFirstSegment = newSegments[myLen];
if (tailFirstSegment.equals("..") || tailFirstSegment.equals(".")) {
result.canonicalize();
}
return result;
}
/**
* Returns the canonicalized path obtained from the
* concatenation of the given string path to the
* end of this path. The given string path must be a valid
* path. If it has a trailing separator,
* the result will have a trailing separator.
* The device id of this path is preserved (the one
* of the given string is ignored). Duplicate slashes
* are removed from the path except at the beginning
* where the path is considered to be UNC.
*
* @param path
* the string path to concatenate
* @return the new path
* @see #isValidPath(String)
* @since 4.0.0-RC5
*/
public Path append(String path) {
//optimize addition of a single segment
if (path.indexOf(SEPARATOR) == -1 && path.indexOf("\\") == -1 && path.indexOf(DEVICE_SEPARATOR) == -1) {
int tailLength = path.length();
if (tailLength < 3) {
//some special cases
if (tailLength == 0 || ".".equals(path)) {
return this;
}
if ("..".equals(path))
return removeLastSegments(1);
}
//just add the segment
int myLen = segments.length;
String[] newSegments = new String[myLen + 1];
System.arraycopy(segments, 0, newSegments, 0, myLen);
newSegments[myLen] = path;
return new Path(device, newSegments, separators & ~HAS_TRAILING);
}
//go with easy implementation
return append(new Path(path));
}
/**
* Destructively converts this path to its canonical form.
* <p>
* In its canonical form, a path does not have any
* "." segments, and parent references ("..") are collapsed
* where possible.
* </p>
*
* @return true if the path was modified, and false otherwise
* @since 4.0.0-RC5
*/
private boolean canonicalize() {
//look for segments that need canonicalizing
for (int i = 0, max = segments.length; i < max; i++) {
String segment = segments[i];
if (segment.charAt(0) == '.' && (segment.equals("..") || segment.equals("."))) {
//path needs to be canonicalized
collapseParentReferences();
//paths of length 0 have no trailing separator
if (segments.length == 0)
separators &= (HAS_LEADING | IS_UNC);
//recompute hash because canonicalize affects hash
separators = (separators & ALL_SEPARATORS) | (computeHashCode() << 3);
return true;
}
}
return false;
}
/**
* Destructively removes all occurrences of ".." segments from this path.
*/
private void collapseParentReferences() {
int segmentCount = segments.length;
String[] stack = new String[segmentCount];
int stackPointer = 0;
for (String segment : segments) {
if (segment.equals("..")) {
if (stackPointer == 0) {
// if the stack is empty we are going out of our scope
// so we need to accumulate segments. But only if the original
// path is relative. If it is absolute then we can't go any higher than
// root so simply toss the .. references.
if (!isAbsolute())
stack[stackPointer++] = segment; //stack push
} else {
// if the top is '..' then we are accumulating segments so don't pop
if ("..".equals(stack[stackPointer - 1]))
stack[stackPointer++] = "..";
else
stackPointer--;
//stack pop
}
//collapse current references
} else if (!segment.equals(".") || segmentCount == 1)
stack[stackPointer++] = segment; //stack push
}
//if the number of segments hasn't changed, then no modification needed
if (stackPointer == segmentCount)
return;
//build the new segment array backwards by popping the stack
String[] newSegments = new String[stackPointer];
System.arraycopy(stack, 0, newSegments, 0, stackPointer);
this.segments = newSegments;
}
/**
* Removes duplicate slashes from the given path, with the exception
* of leading double slash which represents a UNC path.
*/
private String collapseSlashes(String path) {
int length = path.length();
// if the path is only 0, 1 or 2 chars long then it could not possibly have illegal
// duplicate slashes.
if (length < 3)
return path;
// check for an occurrence of // in the path. Start at index 1 to ensure we skip leading UNC //
// If there are no // then there is nothing to collapse so just return.
if (path.indexOf("//", 1) == -1)
return path;
// We found an occurrence of // in the path so do the slow collapse.
char[] result = new char[path.length()];
int count = 0;
boolean hasPrevious = false;
char[] characters = path.toCharArray();
for (int index = 0; index < characters.length; index++) {
char c = characters[index];
if (c == SEPARATOR) {
if (hasPrevious) {
// skip double slashes, except for beginning of UNC.
// note that a UNC path can't have a device.
if (device == null && index == 1) {
result[count] = c;
count++;
}
} else {
hasPrevious = true;
result[count] = c;
count++;
}
} else {
hasPrevious = false;
result[count] = c;
count++;
}
}
return new String(result, 0, count);
}
/* (Intentionally not included in javadoc)
* Computes the hash code for this object.
*/
private int computeHashCode() {
int hash = device == null ? 17 : device.hashCode();
int segmentCount = segments.length;
for (int i = 0; i < segmentCount; i++) {
//this function tends to given a fairly even distribution
hash = hash * 37 + segments[i].hashCode();
}
return hash;
}
/* (Intentionally not included in javadoc)
* Returns the size of the string that will be created by toString or toOSString.
*/
private int computeLength() {
int length = 0;
if (device != null)
length += device.length();
if ((separators & HAS_LEADING) != 0)
length++;
if ((separators & IS_UNC) != 0)
length++;
//add the segment lengths
int max = segments.length;
if (max > 0) {
for (String segment : segments) {
length += segment.length();
}
//add the separator lengths
length += max - 1;
}
if ((separators & HAS_TRAILING) != 0)
length++;
return length;
}
/* (Intentionally not included in javadoc)
* Returns the number of segments in the given path
*/
private int computeSegmentCount(String path) {
int len = path.length();
if (len == 0 || (len == 1 && path.charAt(0) == SEPARATOR)) {
return 0;
}
int count = 1;
int prev = -1;
int i;
while ((i = path.indexOf(SEPARATOR, prev + 1)) != -1) {
if (i != prev + 1 && i != len) {
++count;
}
prev = i;
}
if (path.charAt(len - 1) == SEPARATOR) {
--count;
}
return count;
}
/**
* Computes the segment array for the given canonicalized path.
*/
private String[] computeSegments(String path) {
// performance sensitive --- avoid creating garbage
int segmentCount = computeSegmentCount(path);
if (segmentCount == 0)
return NO_SEGMENTS;
String[] newSegments = new String[segmentCount];
int len = path.length();
// check for initial slash
int firstPosition = (path.charAt(0) == SEPARATOR) ? 1 : 0;
// check for UNC
if (firstPosition == 1 && len > 1 && (path.charAt(1) == SEPARATOR))
firstPosition = 2;
int lastPosition = (path.charAt(len - 1) != SEPARATOR) ? len - 1 : len - 2;
// for non-empty paths, the number of segments is
// the number of slashes plus 1, ignoring any leading
// and trailing slashes
int next = firstPosition;
for (int i = 0; i < segmentCount; i++) {
int start = next;
int end = path.indexOf(SEPARATOR, next);
if (end == -1) {
newSegments[i] = path.substring(start, lastPosition + 1);
} else {
newSegments[i] = path.substring(start, end);
}
next = end + 1;
}
return newSegments;
}
/**
* Returns whether this path equals the given object.
* <p>
* Equality for paths is defined to be: same sequence of segments,
* same absolute/relative status, and same device.
* Trailing separators are disregarded.
* Paths are not generally considered equal to objects other than paths.
* </p>
*
* @param obj
* the other object
* @return <code>true</code> if the paths are equivalent,
* and <code>false</code> if they are not
* @since 4.0.0-RC5
*/
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof Path))
return false;
Path target = (Path)obj;
//check leading separators and hash code
if ((separators & HASH_MASK) != (target.separators & HASH_MASK))
return false;
String[] targetSegments = target.segments;
int i = segments.length;
//check segment count
if (i != targetSegments.length)
return false;
//check segments in reverse order - later segments more likely to differ
while (--i >= 0)
if (!segments[i].equals(targetSegments[i]))
return false;
//check device last (least likely to differ)
return device == target.device || (device != null && device.equals(target.device));
}
/**
* Returns the device id for this path, or <code>null</code> if this
* path has no device id. Note that the result will end in ':'.
*
* @return the device id, or <code>null</code>
* @see #setDevice(String)
* @since 4.0.0-RC5
*/
public String getDevice() {
return device;
}
/**
* Returns the file extension portion of this path,
* or <code>null</code> if there is none.
* <p>
* The file extension portion is defined as the string
* following the last period (".") character in the last segment.
* If there is no period in the last segment, the path has no
* file extension portion. If the last segment ends in a period,
* the file extension portion is the empty string.
* </p>
*
* @return the file extension or <code>null</code>
* @see #addFileExtension(String)
* @since 4.0.0-RC5
*/
public String getFileExtension() {
if (hasTrailingSeparator()) {
return null;
}
String lastSegment = lastSegment();
if (lastSegment == null) {
return null;
}
int index = lastSegment.lastIndexOf('.');
if (index == -1) {
return null;
}
return lastSegment.substring(index + 1);
}
/* (Intentionally not included in javadoc)
* Computes the hash code for this object.
*/
public int hashCode() {
return Objects.hashCode(segments);
}
/**
* Returns whether this path has a trailing separator.
* <p>
* Note: In the root path ("/"), the separator is considered to
* be leading rather than trailing.
* </p>
*
* @return <code>true</code> if this path has a trailing
* separator, and <code>false</code> otherwise
* @see #addTrailingSeparator()
* @see #removeTrailingSeparator()
* @since 4.0.0-RC5
*/
public boolean hasTrailingSeparator() {
return (separators & HAS_TRAILING) != 0;
}
/*
* Initialize the current path with the given string.
*/
private Path initialize(String deviceString, String path) {
checkNotNull(path);
this.device = deviceString;
path = collapseSlashes(path);
int len = path.length();
//compute the separators array
if (len < 2) {
if (len == 1 && path.charAt(0) == SEPARATOR) {
this.separators = HAS_LEADING;
} else {
this.separators = 0;
}
} else {
boolean hasLeading = path.charAt(0) == SEPARATOR;
boolean isUNC = hasLeading && path.charAt(1) == SEPARATOR;
//UNC path of length two has no trailing separator
boolean hasTrailing = !(isUNC && len == 2) && path.charAt(len - 1) == SEPARATOR;
separators = hasLeading ? HAS_LEADING : 0;
if (isUNC)
separators |= IS_UNC;
if (hasTrailing)
separators |= HAS_TRAILING;
}
//compute segments and ensure canonical form
segments = computeSegments(path);
if (!canonicalize()) {
//compute hash now because canonicalize didn't need to do it
separators = (separators & ALL_SEPARATORS) | (computeHashCode() << 3);
}
return this;
}
/**
* Returns whether this path is an absolute path (ignoring
* any device id).
* <p>
* Absolute paths start with a path separator.
* A root path, like <code>/</code> or <code>C:/</code>,
* is considered absolute. UNC paths are always absolute.
* </p>
*
* @return <code>true</code> if this path is an absolute path,
* and <code>false</code> otherwise
* @since 4.0.0-RC5
*/
public boolean isAbsolute() {
//it's absolute if it has a leading separator
return (separators & HAS_LEADING) != 0;
}
/**
* Returns whether this path has no segments and is not
* a root path.
*
* @return <code>true</code> if this path is empty,
* and <code>false</code> otherwise
* @since 4.0.0-RC5
*/
public boolean isEmpty() {
//true if no segments and no leading prefix
return segments.length == 0 && ((separators & ALL_SEPARATORS) != HAS_LEADING);
}
/**
* Returns whether this path is a prefix of the given path.
* To be a prefix, this path's segments must
* appear in the argument path in the same order,
* and their device ids must match.
* <p>
* An empty path is a prefix of all paths with the same device; a root path is a prefix of
* all absolute paths with the same device.
* </p>
*
* @param anotherPath
* the other path
* @return <code>true</code> if this path is a prefix of the given path,
* and <code>false</code> otherwise
* @since 4.0.0-RC5
*/
public boolean isPrefixOf(Path anotherPath) {
if (device == null) {
if (anotherPath.getDevice() != null) {
return false;
}
} else {
if (!device.equalsIgnoreCase(anotherPath.getDevice())) {
return false;
}
}
if (isEmpty() || (isRoot() && anotherPath.isAbsolute())) {
return true;
}
int len = segments.length;
if (len > anotherPath.segmentCount()) {
return false;
}
for (int i = 0; i < len; i++) {
if (!segments[i].equals(anotherPath.segment(i)))
return false;
}
return true;
}
/**
* Returns whether this path is a root path.
* <p>
* The root path is the absolute non-UNC path with zero segments;
* e.g., <code>/</code> or <code>C:/</code>.
* The separator is considered a leading separator, not a trailing one.
* </p>
*
* @return <code>true</code> if this path is a root path,
* and <code>false</code> otherwise
* @since 4.0.0-RC5
*/
public boolean isRoot() {
//must have no segments, a leading separator, and not be a UNC path.
return this == ROOT || (segments.length == 0 && ((separators & ALL_SEPARATORS) == HAS_LEADING));
}
/**
* Returns a boolean value indicating whether or not this path
* is considered to be in UNC form. Return false if this path
* has a device set or if the first 2 characters of the path string
* are not <code>Path.SEPARATOR</code>.
*
* @return boolean indicating if this path is UNC
* @since 4.0.0-RC5
*/
public boolean isUNC() {
return device == null && (separators & IS_UNC) != 0;
}
/**
* Returns whether the given string is syntactically correct as
* a path. The device id is the prefix up to and including the device
* separator for the local file system; the path proper is everything to
* the right of it, or the entire string if there is no device separator.
* When the platform location is a file system with no meaningful device
* separator, the entire string is treated as the path proper.
* The device id is not checked for validity; the path proper is correct
* if each of the segments in its canonicalized form is valid.
*
* @param path
* the path to check
* @return <code>true</code> if the given string is a valid path,
* and <code>false</code> otherwise
* @see #isValidSegment(String)
* @since 4.0.0-RC5
*/
public static boolean isValidPath(String path) {
Path test = new Path(path);
for (int i = 0, max = test.segmentCount(); i < max; i++)
if (!isValidSegment(test.segment(i)))
return false;
return true;
}
/**
* Returns whether the given string is valid as a segment in
* a path. The rules for valid segments are as follows:
* <ul>
* <li> the empty string is not valid
* <li> any string containing the slash character ('/') is not valid
* <li>any string containing segment or device separator characters
* on the local file system, such as the backslash ('\') and colon (':')
* on some file systems.
* </ul>
*
* @param segment
* the path segment to check
* @return <code>true</code> if the given path segment is valid,
* and <code>false</code> otherwise
* @since 4.0.0-RC5
*/
protected static boolean isValidSegment(String segment) {
int size = segment.length();
if (size == 0)
return false;
for (int i = 0; i < size; i++) {
char c = segment.charAt(i);
if (c == '/')
return false;
}
return true;
}
/**
* Returns the last segment of this path, or
* <code>null</code> if it does not have any segments.
*
* @return the last segment of this path, or <code>null</code>
* @since 4.0.0-RC5
*/
public String lastSegment() {
int len = segments.length;
return len == 0 ? null : segments[len - 1];
}
/**
* Returns an absolute path with the segments and device id of this path.
* Absolute paths start with a path separator. If this path is absolute,
* it is simply returned.
*
* @return the new path
* @since 4.0.0-RC5
*/
public Path makeAbsolute() {
if (isAbsolute()) {
return this;
}
Path result = new Path(device, segments, separators | HAS_LEADING);
//may need canonicalizing if it has leading ".." or "." segments
if (result.segmentCount() > 0) {
String first = result.segment(0);
assert first != null;
if (first.equals("..") || first.equals(".")) {
result.canonicalize();
}
}
return result;
}
/**
* Returns a relative path with the segments and device id of this path.
* Absolute paths start with a path separator and relative paths do not.
* If this path is relative, it is simply returned.
*
* @return the new path
* @since 4.0.0-RC5
*/
public Path makeRelative() {
if (!isAbsolute()) {
return this;
}
return new Path(device, segments, separators & HAS_TRAILING);
}
/**
* Returns a path equivalent to this path, but relative to the given base path if possible.
* <p>
* The path is only made relative if the base path if both paths have the same device
* and have a non-zero length common prefix. If the paths have different devices,
* or no common prefix, then this path is simply returned. If the path is successfully
* made relative, then appending the returned path to the base will always produce
* a path equal to this path.
* </p>
*
* @param base
* The base path to make this path relative to
* @return A path relative to the base path, or this path if it could
* not be made relative to the given base
* @since 4.0.0-RC5
*/
public Path makeRelativeTo(Path base) {
//can't make relative if devices are not equal
if (device != base.getDevice() && (device == null || !device.equalsIgnoreCase(base.getDevice())))
return this;
int commonLength = matchingFirstSegments(base);
final int differenceLength = base.segmentCount() - commonLength;
final int newSegmentLength = differenceLength + segmentCount() - commonLength;
if (newSegmentLength == 0)
return Path.EMPTY;
String[] newSegments = new String[newSegmentLength];
//add parent references for each segment different from the base
Arrays.fill(newSegments, 0, differenceLength, ".."); //$NON-NLS-1$
//append the segments of this path not in common with the base
System.arraycopy(segments, commonLength, newSegments, differenceLength, newSegmentLength - differenceLength);
return new Path(null, newSegments, separators & HAS_TRAILING);
}
/**
* Return a new path which is the equivalent of this path converted to UNC
* form (if the given boolean is true) or this path not as a UNC path (if the given
* boolean is false). If UNC, the returned path will not have a device and the
* first 2 characters of the path string will be <code>Path.SEPARATOR</code>. If not UNC, the
* first 2 characters of the returned path string will not be <code>Path.SEPARATOR</code>.
*
* @param toUNC
* true if converting to UNC, false otherwise
* @return the new path, either in UNC form or not depending on the boolean parameter
* @since 4.0.0-RC5
*/
public Path makeUNC(boolean toUNC) {
// if we are already in the right form then just return
if (!(toUNC ^ isUNC()))
return this;
int newSeparators = this.separators;
if (toUNC) {
newSeparators |= HAS_LEADING | IS_UNC;
} else {
//mask out the UNC bit
newSeparators &= HAS_LEADING | HAS_TRAILING;
}
return new Path(toUNC ? null : device, segments, newSeparators);
}
/**
* Returns a count of the number of segments which match in
* this path and the given path (device ids are ignored),
* comparing in increasing segment number order.
*
* @param anotherPath
* the other path
* @return the number of matching segments
* @since 4.0.0-RC5
*/
public int matchingFirstSegments(Path anotherPath) {
checkNotNull(anotherPath);
int anotherPathLen = anotherPath.segmentCount();
int max = Math.min(segments.length, anotherPathLen);
int count = 0;
for (int i = 0; i < max; i++) {
if (!segments[i].equals(anotherPath.segment(i))) {
return count;
}
count++;
}
return count;
}
/**
* Returns a new path which is the same as this path but with
* the file extension removed. If this path does not have an
* extension, this path is returned.
* <p>
* The file extension portion is defined as the string
* following the last period (".") character in the last segment.
* If there is no period in the last segment, the path has no
* file extension portion. If the last segment ends in a period,
* the file extension portion is the empty string.
* </p>
*
* @return the new path
* @see #addFileExtension(String)
* @since 4.0.0-RC5
*/
public Path removeFileExtension() {
String extension = getFileExtension();
if (extension == null || extension.equals("")) {
return this;
}
String lastSegment = lastSegment();
int index = lastSegment.lastIndexOf(extension) - 1;
return removeLastSegments(1).append(lastSegment.substring(0, index));
}
/**
* Returns a copy of this path with the given number of segments
* removed from the beginning. The device id is preserved.
* The number must be greater or equal zero.
* If the count is zero, this path is returned.
* The resulting path will always be a relative path with respect
* to this path. If the number equals or exceeds the number
* of segments in this path, an empty relative path is returned.
*
* @param count
* the number of segments to remove
* @return the new path
* @since 4.0.0-RC5
*/
public Path removeFirstSegments(int count) {
if (count == 0)
return this;
if (count >= segments.length) {
return new Path(device, NO_SEGMENTS, 0);
}
checkArgument(count > 0);
int newSize = segments.length - count;
String[] newSegments = new String[newSize];
System.arraycopy(this.segments, count, newSegments, 0, newSize);
//result is always a relative path
return new Path(device, newSegments, separators & HAS_TRAILING);
}
/**
* Returns a copy of this path with the given number of segments
* removed from the end. The device id is preserved.
* The number must be greater or equal zero.
* If the count is zero, this path is returned.
* <p>
* If this path has a trailing separator, it will still
* have a trailing separator after the last segments are removed
* (assuming there are some segments left). If there is no
* trailing separator, the result will not have a trailing
* separator.
* If the number equals or exceeds the number
* of segments in this path, a path with no segments is returned.
* </p>
*
* @param count
* the number of segments to remove
* @return the new path
* @since 4.0.0-RC5
*/
public Path removeLastSegments(int count) {
if (count == 0)
return this;
if (count >= segments.length) {
//result will have no trailing separator
return new Path(device, NO_SEGMENTS, separators & (HAS_LEADING | IS_UNC));
}
checkArgument(count > 0);
int newSize = segments.length - count;
String[] newSegments = new String[newSize];
System.arraycopy(this.segments, 0, newSegments, 0, newSize);
return new Path(device, newSegments, separators & (HAS_LEADING | IS_UNC));
}
/**
* Returns a path with the same segments as this path
* but with a trailing separator removed.
* Does nothing if this path does not have at least one segment.
* The device id is preserved.
* <p>
* If this path does not have a trailing separator,
* this path is returned.
* </p>
*
* @return the new path
* @see #addTrailingSeparator()
* @see #hasTrailingSeparator()
* @since 4.0.0-RC5
*/
public Path removeTrailingSeparator() {
if (!hasTrailingSeparator()) {
return this;
}
return new Path(device, segments, separators & (HAS_LEADING | IS_UNC));
}
/**
* Returns the specified segment of this path, or
* <code>null</code> if the path does not have such a segment.
*
* @param index
* the 0-based segment index
* @return the specified segment, or <code>null</code>
* @since 4.0.0-RC5
*/
public String segment(int index) {
if (index >= segments.length)
return null;
return segments[index];
}
/**
* Returns the number of segments in this path.
* <p>
* Note that both root and empty paths have 0 segments.
* </p>
*
* @return the number of segments
* @since 4.0.0-RC5
*/
public int segmentCount() {
return segments.length;
}
/**
* Returns the segments in this path in order.
*
* @return an array of string segments
* @since 4.0.0-RC5
*/
public String[] segments() {
String[] segmentCopy = new String[segments.length];
System.arraycopy(segments, 0, segmentCopy, 0, segments.length);
return segmentCopy;
}
/**
* Returns a new path which is the same as this path but with
* the given device id. The device id must end with a ":".
* A device independent path is obtained by passing <code>null</code>.
* <p>
* For example, "C:" and "Server/Volume:" are typical device ids.
* </p>
*
* @param device
* the device id or <code>null</code>
* @return a new path
* @see #getDevice()
* @since 4.0.0-RC5
*/
public Path setDevice(String device) {
if (device != null) {
checkArgument(device.indexOf(Path.DEVICE_SEPARATOR) == (device.length() - 1), "Last character should be the device separator");
}
//return the receiver if the device is the same
if (device == this.device || (device != null && device.equals(this.device)))
return this;
return new Path(device, segments, separators);
}
/**
* Returns a string representation of this path, including its
* device id. The same separator, "/", is used on all platforms.
* <p>
* Example result strings (without and with device id):
* <pre>
* "/foo/bar.txt"
* "bar.txt"
* "/foo/"
* "foo/"
* ""
* "/"
* "C:/foo/bar.txt"
* "C:bar.txt"
* "C:/foo/"
* "C:foo/"
* "C:"
* "C:/"
* </pre>
* This string is suitable for passing to <code>Path(String)</code>.
* </p>
*
* @return a string representation of this path
* @since 4.0.0-RC5
*/
public String toString() {
int resultSize = computeLength();
if (resultSize <= 0)
return EMPTY_STRING;
char[] result = new char[resultSize];
int offset = 0;
if (device != null) {
int size = device.length();
device.getChars(0, size, result, offset);
offset += size;
}
if ((separators & HAS_LEADING) != 0)
result[offset++] = SEPARATOR;
if ((separators & IS_UNC) != 0)
result[offset++] = SEPARATOR;
int len = segments.length - 1;
if (len >= 0) {
//append all but the last segment, with separators
for (int i = 0; i < len; i++) {
int size = segments[i].length();
segments[i].getChars(0, size, result, offset);
offset += size;
result[offset++] = SEPARATOR;
}
//append the last segment
int size = segments[len].length();
segments[len].getChars(0, size, result, offset);
offset += size;
}
if ((separators & HAS_TRAILING) != 0)
result[offset++] = SEPARATOR;
return new String(result);
}
/**
* Returns a copy of this path truncated after the
* given number of segments. The number must not be negative.
* The device id is preserved.
* <p>
* If this path has a trailing separator, the result will too
* (assuming there are some segments left). If there is no
* trailing separator, the result will not have a trailing
* separator.
* Copying up to segment zero simply means making an copy with
* no path segments.
* </p>
*
* @param count
* the segment number at which to truncate the path
* @return the new path
* @since 4.0.0-RC5
*/
public Path uptoSegment(int count) {
if (count == 0)
return new Path(device, NO_SEGMENTS, separators & (HAS_LEADING | IS_UNC));
if (count >= segments.length)
return this;
checkArgument(count > 0, "Invalid parameter to Path.uptoSegment");
String[] newSegments = new String[count];
System.arraycopy(segments, 0, newSegments, 0, count);
return new Path(device, newSegments, separators);
}
/**
* Returns a copy of this path with removed last segment.
*
* @return the new path
* @since 4.4.0
*/
public Path parent() {
return segmentCount() == 1 ? Path.ROOT : this.removeLastSegments(1);
}
/**
* Converts given input array of paths into the list.
*
* @param paths
* the input array of paths
* @return the converted list
* @since 4.4.0
*/
public static List<String> toList(Path[] paths) {
if (paths == null || paths.length == 0) {
return Collections.emptyList();
}
List<String> list = new ArrayList<>(paths.length);
for (Path path : paths) {
list.add(path.toString());
}
return list;
}
/**
* Calculated common path from the several paths given as array.
* <p>
* For example we have three paths:
* <ul>
* <li>{@code /a/b/c}</li>
* <li>{@code /a/b/d}</li>
* <li>{@code /a/b/d/e}</li>
* </ul>
* Common path will be {@code /a/b}
*
* @param paths
* paths array
* @return common path of empty string if given array is empty
* @throws NullPointerException
* in case if given {@code paths} array is null
* @since 5.0.0
*/
public static Path commonPath(Path... paths) {
checkNotNull(paths);
Path commonPath = Path.ROOT;
if (paths.length == 0) {
return EMPTY;
}
if (paths.length == 1) {
return paths[0];
}
for (int i = 0; i < paths[0].segmentCount(); i++) {
final String currentSegment = paths[0].segment(i);
boolean segmentsMatched = true;
for (int j = 1; j < paths.length && segmentsMatched; j++) {
final Path comparedPath = paths[j];
if (comparedPath.segmentCount() < i) {
segmentsMatched = false;
break;
} else {
segmentsMatched = nullToEmpty(comparedPath.segment(i)).equals(currentSegment);
}
}
if (segmentsMatched) {
commonPath = commonPath.append(currentSegment);
} else {
break;
}
}
return commonPath;
}
}