/* * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * Contributors: * Nuxeo - initial API and implementation * * $Id$ */ package org.nuxeo.common.utils; import java.io.Serializable; /** * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ public class Path implements Serializable { private static final long serialVersionUID = -5420562131803786641L; private static final int HAS_LEADING = 1; private static final int HAS_TRAILING = 2; private static final int ALL_SEPARATORS = HAS_LEADING | HAS_TRAILING; private static final int USED_BITS = 2; private static final String[] NO_SEGMENTS = new String[0]; /** * Path separator character constant "/" used in paths. */ private static final char SEPARATOR = '/'; /** Constant empty string value. */ private static final String EMPTY_STRING = ""; //$NON-NLS-1$ /** Constant value containing the empty path with no device. */ private static final Path EMPTY = new Path(EMPTY_STRING); // private static final int HASH_MASK = ~HAS_TRAILING; /** Constant root path string (<code>"/"</code>). */ private static final String ROOT_STRING = "/"; //$NON-NLS-1$ /** Constant value containing the root path with no device. */ private static final Path ROOT = new Path(ROOT_STRING); private String[] segments; private int separators; /** * Constructs a new path from the given string path. * <p> * The string path must represent a valid file system path * on the local file system. * <p> * 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) */ public Path(String fullPath) { initialize(fullPath); } /** * Optimized constructor - no validations on segments are done. * * @param segments * @param separators */ private Path(String[] segments, int separators) { // no segment validations are done for performance reasons this.segments = segments; //hash code is cached in all but the bottom three bits of the separators field this.separators = (computeHashCode() << USED_BITS) | (separators & ALL_SEPARATORS); } /** * Creates a path object from an absolute and canonical path. * <p> * This method does not check the given path - it assumes the path * has a valid format of the form "/a/b/c" without duplicate slashes or dots. * * @return the path */ public static Path createFromAbsolutePath(String path) { assert path != null; final int len = computeSegmentCount(path); if (path.length() < 2) { return ROOT; } String[] segments = new String[len]; int k = 0; int j = 1; int i = path.indexOf(SEPARATOR, j); while (i > 0) { segments[k++] = path.substring(j, i); j = i + 1; i = path.indexOf(SEPARATOR, j); } segments[k] = path.substring(j); return new Path(segments, HAS_LEADING); } public static Path createFromSegments(String[] segments) { if (segments.length == 0) { return ROOT; } return new Path(segments, HAS_LEADING); } 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(newSegments, separators); } public Path addTrailingSeparator() { if (hasTrailingSeparator() || isRoot()) { return this; } if (isEmpty()) { return new Path(segments, HAS_LEADING); } return new Path(segments, separators | HAS_TRAILING); } // XXX: confusing, one may think that this modifies the path // being appended to (like Python's list.append()). public Path append(Path tail) { //optimize some easy cases if (tail == null || tail.segmentCount() == 0) { return this; } if (isEmpty()) { return tail.makeRelative(); } if (isRoot()) { return tail.makeAbsolute(); } //concatenate the two segment arrays int myLen = segments.length; int tailLen = tail.segmentCount(); String[] newSegments = new String[myLen + tailLen]; System.arraycopy(segments, 0, newSegments, 0, myLen); for (int i = 0; i < tailLen; i++) { newSegments[myLen + i] = tail.segment(i); } //use my leading separators and the tail's trailing separator Path result = new Path(newSegments, (separators & HAS_LEADING) | (tail.hasTrailingSeparator() ? HAS_TRAILING : 0)); String tailFirstSegment = newSegments[myLen]; if (tailFirstSegment.equals("..") || tailFirstSegment.equals(".")) { //$NON-NLS-1$ //$NON-NLS-2$ result.canonicalize(); } return result; } // XXX: same remark public Path append(String tail) { //optimize addition of a single segment if (tail.indexOf(SEPARATOR) == -1) { int tailLength = tail.length(); if (tailLength < 3) { //some special cases if (tailLength == 0 || ".".equals(tail)) { //$NON-NLS-1$ return this; } if ("..".equals(tail)) { 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] = tail; return new Path(newSegments, separators & ~HAS_TRAILING); } //go with easy implementation return append(new Path(tail)); } /** * 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. * * @return true if the path was modified, and false otherwise. */ 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("."))) { //$NON-NLS-1$ //$NON-NLS-2$ //path needs to be canonicalized collapseParentReferences(); //paths of length 0 have no trailing separator if (segments.length == 0) { separators &= HAS_LEADING; } //recompute hash because canonicalize affects hash separators = (separators & ALL_SEPARATORS) | (computeHashCode() << USED_BITS); 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 (int i = 0; i < segmentCount; i++) { String segment = segments[i]; if (segment.equals("..")) { //$NON-NLS-1$ 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])) { //$NON-NLS-1$ stack[stackPointer++] = ".."; //$NON-NLS-1$ } else { stackPointer--; //stack pop } } //collapse current references } else if (!segment.equals(".") || (i == 0 && !isAbsolute())) { 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); segments = newSegments; } /** * Removes duplicate slashes from the given path. */ private static String collapseSlashes(String path) { int length = path.length(); // if the path is only 0 or 1 chars long then it could not possibly have illegal // duplicate slashes. if (length < 2) { 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 (char c : characters) { if (c == SEPARATOR) { if (!hasPrevious) { hasPrevious = true; result[count] = c; count++; } // else skip double slashes } else { hasPrevious = false; result[count] = c; count++; } } return new String(result, 0, count); } private int computeHashCode() { int hash = 17; 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; } private int computeLength() { int length = 0; if ((separators & HAS_LEADING) != 0) { length++; } //add the segment lengths int max = segments.length; if (max > 0) { for (int i = 0; i < max; i++) { length += segments[i].length(); } //add the separator lengths length += max - 1; } if ((separators & HAS_TRAILING) != 0) { length++; } return length; } private static 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 static 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; } @Override 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; } } return true; } @Override public int hashCode() { return separators & HASH_MASK; } 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); } public boolean hasTrailingSeparator() { return (separators & HAS_TRAILING) != 0; } /** * Initializes the current path with the given string. */ private Path initialize(String path) { assert path != null; path = collapseSlashes(path); int len = path.length(); //compute the separators array if (len < 2) { if (len == 1 && path.charAt(0) == SEPARATOR) { separators = HAS_LEADING; } else { separators = 0; } } else { boolean hasLeading = path.charAt(0) == SEPARATOR; boolean hasTrailing = path.charAt(len - 1) == SEPARATOR; separators = hasLeading ? HAS_LEADING : 0; 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() << USED_BITS); } return this; } public boolean isAbsolute() { //it's absolute if it has a leading separator return (separators & HAS_LEADING) != 0; } public boolean isEmpty() { //true if no segments and no leading prefix return segments.length == 0 && ((separators & ALL_SEPARATORS) != HAS_LEADING); } public boolean isPrefixOf(Path anotherPath) { 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; } 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)); } 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; } private 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; } public String lastSegment() { int len = segments.length; return len == 0 ? null : segments[len - 1]; } public Path makeAbsolute() { if (isAbsolute()) { return this; } Path result = new Path(segments, separators | HAS_LEADING); //may need canonicalizing if it has leading ".." or "." segments if (result.segmentCount() > 0) { String first = result.segment(0); if (first.equals("..") || first.equals(".")) { //$NON-NLS-1$ //$NON-NLS-2$ result.canonicalize(); } } return result; } public Path makeRelative() { if (!isAbsolute()) { return this; } return new Path(segments, separators & HAS_TRAILING); } public int matchingFirstSegments(Path anotherPath) { assert anotherPath != null; 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; } public Path removeFileExtension() { String extension = getFileExtension(); if (extension == null || extension.equals("")) { //$NON-NLS-1$ return this; } String lastSegment = lastSegment(); int index = lastSegment.lastIndexOf(extension) - 1; return removeLastSegments(1).append(lastSegment.substring(0, index)); } public Path removeFirstSegments(int count) { if (count == 0) { return this; } if (count >= segments.length) { return new Path(NO_SEGMENTS, 0); } assert count > 0; int newSize = segments.length - count; String[] newSegments = new String[newSize]; System.arraycopy(segments, count, newSegments, 0, newSize); //result is always a relative path return new Path(newSegments, separators & HAS_TRAILING); } public Path removeLastSegments(final int count) { if (count == 0) { return this; } if (count >= segments.length) { //result will have no trailing separator return new Path(NO_SEGMENTS, separators & HAS_LEADING); } assert count > 0; final int newSize = segments.length - count; final String[] newSegments = new String[newSize]; System.arraycopy(segments, 0, newSegments, 0, newSize); return new Path(newSegments, separators); } public Path removeTrailingSeparator() { if (!hasTrailingSeparator()) { return this; } return new Path(segments, separators & HAS_LEADING); } public String segment(int index) { if (index >= segments.length) { return null; } return segments[index]; } public int segmentCount() { return segments.length; } public String[] segments() { final String[] segmentCopy = new String[segments.length]; System.arraycopy(segments, 0, segmentCopy, 0, segments.length); return segmentCopy; } @Override public String toString() { final int resultSize = computeLength(); if (resultSize <= 0) { return EMPTY_STRING; } char[] result = new char[resultSize]; int offset = 0; if ((separators & HAS_LEADING) != 0) { result[offset++] = SEPARATOR; } final int len = segments.length - 1; if (len >= 0) { //append all but the last segment, with separators for (int i = 0; i < len; i++) { final int size = segments[i].length(); segments[i].getChars(0, size, result, offset); offset += size; result[offset++] = SEPARATOR; } //append the last segment final 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); } public Path uptoSegment(final int count) { if (count == 0) { return new Path(NO_SEGMENTS, separators & HAS_LEADING); } if (count >= segments.length) { return this; } assert count > 0; // Invalid parameter to Path.uptoSegment final String[] newSegments = new String[count]; System.arraycopy(segments, 0, newSegments, 0, count); return new Path(newSegments, separators); } /** * Gets the name of the icon file so that it can be displayed as alt text. */ public static String getFileNameFromPath(String iconPath) { String iconName; // String fileSeparator = System.getProperty("file.separator"); //temporary not working with the file separator, only with / int firstCharOfIconName = iconPath.lastIndexOf(SEPARATOR); int lastCharOfIconName = iconPath.lastIndexOf("."); if (firstCharOfIconName == -1) { iconName = iconPath; } else { iconName = iconPath.substring(firstCharOfIconName, lastCharOfIconName); } return iconName; } }