001/* 002 * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * IBM Corporation - initial API and implementation 018 */ 019package org.nuxeo.common.utils; 020 021import java.io.Serializable; 022 023/** 024 * Contains code from Eclipse org.eclipse.core.runtime.Path class 025 * 026 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 027 */ 028public class Path implements Serializable { 029 030 private static final long serialVersionUID = -5420562131803786641L; 031 032 private static final int HAS_LEADING = 1; 033 034 private static final int HAS_TRAILING = 2; 035 036 private static final int ALL_SEPARATORS = HAS_LEADING | HAS_TRAILING; 037 038 private static final int USED_BITS = 2; 039 040 private static final String[] NO_SEGMENTS = new String[0]; 041 042 /** 043 * Path separator character constant "/" used in paths. 044 */ 045 private static final char SEPARATOR = '/'; 046 047 /** Constant empty string value. */ 048 private static final String EMPTY_STRING = ""; //$NON-NLS-1$ 049 050 /** Constant value containing the empty path with no device. */ 051 private static final Path EMPTY = new Path(EMPTY_STRING); 052 053 // 054 private static final int HASH_MASK = ~HAS_TRAILING; 055 056 /** Constant root path string (<code>"/"</code>). */ 057 private static final String ROOT_STRING = "/"; //$NON-NLS-1$ 058 059 /** Constant value containing the root path with no device. */ 060 private static final Path ROOT = new Path(ROOT_STRING); 061 062 private String[] segments; 063 064 private int separators; 065 066 /** 067 * Constructs a new path from the given string path. 068 * <p> 069 * The string path must represent a valid file system path on the local file system. 070 * <p> 071 * The path is canonicalized and double slashes are removed except at the beginning. (to handle UNC paths). All 072 * forward slashes ('/') are treated as segment delimiters, and any segment and device delimiters for the local file 073 * system are also respected (such as colon (':') and backslash ('\') on some file systems). 074 * 075 * @param fullPath the string path 076 * @see #isValidPath(String) 077 */ 078 public Path(String fullPath) { 079 initialize(fullPath); 080 } 081 082 /** 083 * Optimized constructor - no validations on segments are done. 084 * 085 * @param segments 086 * @param separators 087 */ 088 private Path(String[] segments, int separators) { 089 // no segment validations are done for performance reasons 090 this.segments = segments; 091 // hash code is cached in all but the bottom three bits of the separators field 092 this.separators = (computeHashCode() << USED_BITS) | (separators & ALL_SEPARATORS); 093 } 094 095 /** 096 * Creates a path object from an absolute and canonical path. 097 * <p> 098 * This method does not check the given path - it assumes the path has a valid format of the form "/a/b/c" without 099 * duplicate slashes or dots. 100 * 101 * @return the path 102 */ 103 public static Path createFromAbsolutePath(String path) { 104 assert path != null; 105 final int len = computeSegmentCount(path); 106 if (path.length() < 2) { 107 return ROOT; 108 } 109 String[] segments = new String[len]; 110 int k = 0; 111 int j = 1; 112 int i = path.indexOf(SEPARATOR, j); 113 while (i > 0) { 114 segments[k++] = path.substring(j, i); 115 j = i + 1; 116 i = path.indexOf(SEPARATOR, j); 117 } 118 segments[k] = path.substring(j); 119 return new Path(segments, HAS_LEADING); 120 } 121 122 public static Path createFromSegments(String[] segments) { 123 if (segments.length == 0) { 124 return ROOT; 125 } 126 return new Path(segments, HAS_LEADING); 127 } 128 129 public Path addFileExtension(String extension) { 130 if (isRoot() || isEmpty() || hasTrailingSeparator()) { 131 return this; 132 } 133 int len = segments.length; 134 String[] newSegments = new String[len]; 135 System.arraycopy(segments, 0, newSegments, 0, len - 1); 136 newSegments[len - 1] = segments[len - 1] + '.' + extension; 137 return new Path(newSegments, separators); 138 } 139 140 public Path addTrailingSeparator() { 141 if (hasTrailingSeparator() || isRoot()) { 142 return this; 143 } 144 if (isEmpty()) { 145 return new Path(segments, HAS_LEADING); 146 } 147 return new Path(segments, separators | HAS_TRAILING); 148 } 149 150 // XXX: confusing, one may think that this modifies the path 151 // being appended to (like Python's list.append()). 152 public Path append(Path tail) { 153 // optimize some easy cases 154 if (tail == null || tail.segmentCount() == 0) { 155 return this; 156 } 157 if (isEmpty()) { 158 return tail.makeRelative(); 159 } 160 if (isRoot()) { 161 return tail.makeAbsolute(); 162 } 163 164 // concatenate the two segment arrays 165 int myLen = segments.length; 166 int tailLen = tail.segmentCount(); 167 String[] newSegments = new String[myLen + tailLen]; 168 System.arraycopy(segments, 0, newSegments, 0, myLen); 169 for (int i = 0; i < tailLen; i++) { 170 newSegments[myLen + i] = tail.segment(i); 171 } 172 // use my leading separators and the tail's trailing separator 173 Path result = new Path(newSegments, (separators & HAS_LEADING) 174 | (tail.hasTrailingSeparator() ? HAS_TRAILING : 0)); 175 String tailFirstSegment = newSegments[myLen]; 176 if (tailFirstSegment.equals("..") || tailFirstSegment.equals(".")) { //$NON-NLS-1$ //$NON-NLS-2$ 177 result.canonicalize(); 178 } 179 return result; 180 } 181 182 // XXX: same remark 183 public Path append(String tail) { 184 // optimize addition of a single segment 185 if (tail.indexOf(SEPARATOR) == -1) { 186 int tailLength = tail.length(); 187 if (tailLength < 3) { 188 // some special cases 189 if (tailLength == 0 || ".".equals(tail)) { //$NON-NLS-1$ 190 return this; 191 } 192 if ("..".equals(tail)) { 193 return removeLastSegments(1); 194 } 195 } 196 // just add the segment 197 int myLen = segments.length; 198 String[] newSegments = new String[myLen + 1]; 199 System.arraycopy(segments, 0, newSegments, 0, myLen); 200 newSegments[myLen] = tail; 201 return new Path(newSegments, separators & ~HAS_TRAILING); 202 } 203 // go with easy implementation 204 return append(new Path(tail)); 205 } 206 207 /** 208 * Destructively converts this path to its canonical form. 209 * <p> 210 * In its canonical form, a path does not have any "." segments, and parent references ("..") are collapsed where 211 * possible. 212 * 213 * @return true if the path was modified, and false otherwise. 214 */ 215 private boolean canonicalize() { 216 // look for segments that need canonicalizing 217 for (int i = 0, max = segments.length; i < max; i++) { 218 String segment = segments[i]; 219 if (segment.charAt(0) == '.' && (segment.equals("..") || segment.equals("."))) { //$NON-NLS-1$ //$NON-NLS-2$ 220 // path needs to be canonicalized 221 collapseParentReferences(); 222 // paths of length 0 have no trailing separator 223 if (segments.length == 0) { 224 separators &= HAS_LEADING; 225 } 226 // recompute hash because canonicalize affects hash 227 separators = (separators & ALL_SEPARATORS) | (computeHashCode() << USED_BITS); 228 return true; 229 } 230 } 231 return false; 232 } 233 234 /** 235 * Destructively removes all occurrences of ".." segments from this path. 236 */ 237 private void collapseParentReferences() { 238 int segmentCount = segments.length; 239 String[] stack = new String[segmentCount]; 240 int stackPointer = 0; 241 for (int i = 0; i < segmentCount; i++) { 242 String segment = segments[i]; 243 if (segment.equals("..")) { //$NON-NLS-1$ 244 if (stackPointer == 0) { 245 // if the stack is empty we are going out of our scope 246 // so we need to accumulate segments. But only if the original 247 // path is relative. If it is absolute then we can't go any higher than 248 // root so simply toss the .. references. 249 if (!isAbsolute()) { 250 stack[stackPointer++] = segment; // stack push 251 } 252 } else { 253 // if the top is '..' then we are accumulating segments so don't pop 254 if ("..".equals(stack[stackPointer - 1])) { //$NON-NLS-1$ 255 stack[stackPointer++] = ".."; //$NON-NLS-1$ 256 } else { 257 stackPointer--; 258 // stack pop 259 } 260 } 261 // collapse current references 262 } else if (!segment.equals(".") || (i == 0 && !isAbsolute())) { 263 stack[stackPointer++] = segment; // stack push 264 } 265 } 266 // if the number of segments hasn't changed, then no modification needed 267 if (stackPointer == segmentCount) { 268 return; 269 } 270 // build the new segment array backwards by popping the stack 271 String[] newSegments = new String[stackPointer]; 272 System.arraycopy(stack, 0, newSegments, 0, stackPointer); 273 segments = newSegments; 274 } 275 276 /** 277 * Removes duplicate slashes from the given path. 278 */ 279 private static String collapseSlashes(String path) { 280 int length = path.length(); 281 // if the path is only 0 or 1 chars long then it could not possibly have illegal 282 // duplicate slashes. 283 if (length < 2) { 284 return path; 285 } 286 // check for an occurrence of // in the path. Start at index 1 to ensure we skip leading UNC // 287 // If there are no // then there is nothing to collapse so just return. 288 if (path.indexOf("//", 1) == -1) { 289 return path; 290 } 291 // We found an occurrence of // in the path so do the slow collapse. 292 char[] result = new char[path.length()]; 293 int count = 0; 294 boolean hasPrevious = false; 295 char[] characters = path.toCharArray(); 296 for (char c : characters) { 297 if (c == SEPARATOR) { 298 if (!hasPrevious) { 299 hasPrevious = true; 300 result[count] = c; 301 count++; 302 } // else skip double slashes 303 } else { 304 hasPrevious = false; 305 result[count] = c; 306 count++; 307 } 308 } 309 return new String(result, 0, count); 310 } 311 312 private int computeHashCode() { 313 int hash = 17; 314 int segmentCount = segments.length; 315 for (int i = 0; i < segmentCount; i++) { 316 // this function tends to given a fairly even distribution 317 hash = hash * 37 + segments[i].hashCode(); 318 } 319 return hash; 320 } 321 322 private int computeLength() { 323 int length = 0; 324 if ((separators & HAS_LEADING) != 0) { 325 length++; 326 } 327 // add the segment lengths 328 int max = segments.length; 329 if (max > 0) { 330 for (int i = 0; i < max; i++) { 331 length += segments[i].length(); 332 } 333 // add the separator lengths 334 length += max - 1; 335 } 336 if ((separators & HAS_TRAILING) != 0) { 337 length++; 338 } 339 return length; 340 } 341 342 private static int computeSegmentCount(String path) { 343 int len = path.length(); 344 if (len == 0 || (len == 1 && path.charAt(0) == SEPARATOR)) { 345 return 0; 346 } 347 int count = 1; 348 int prev = -1; 349 int i; 350 while ((i = path.indexOf(SEPARATOR, prev + 1)) != -1) { 351 if (i != prev + 1 && i != len) { 352 ++count; 353 } 354 prev = i; 355 } 356 if (path.charAt(len - 1) == SEPARATOR) { 357 --count; 358 } 359 return count; 360 } 361 362 /** 363 * Computes the segment array for the given canonicalized path. 364 */ 365 private static String[] computeSegments(String path) { 366 // performance sensitive --- avoid creating garbage 367 int segmentCount = computeSegmentCount(path); 368 if (segmentCount == 0) { 369 return NO_SEGMENTS; 370 } 371 String[] newSegments = new String[segmentCount]; 372 int len = path.length(); 373 // check for initial slash 374 int firstPosition = (path.charAt(0) == SEPARATOR) ? 1 : 0; 375 // check for UNC 376 if (firstPosition == 1 && len > 1 && (path.charAt(1) == SEPARATOR)) { 377 firstPosition = 2; 378 } 379 int lastPosition = (path.charAt(len - 1) != SEPARATOR) ? len - 1 : len - 2; 380 // for non-empty paths, the number of segments is 381 // the number of slashes plus 1, ignoring any leading 382 // and trailing slashes 383 int next = firstPosition; 384 for (int i = 0; i < segmentCount; i++) { 385 int start = next; 386 int end = path.indexOf(SEPARATOR, next); 387 if (end == -1) { 388 newSegments[i] = path.substring(start, lastPosition + 1); 389 } else { 390 newSegments[i] = path.substring(start, end); 391 } 392 next = end + 1; 393 } 394 return newSegments; 395 } 396 397 @Override 398 public boolean equals(Object obj) { 399 if (this == obj) { 400 return true; 401 } 402 if (!(obj instanceof Path)) { 403 return false; 404 } 405 Path target = (Path) obj; 406 // check leading separators and hash code 407 if ((separators & HASH_MASK) != (target.separators & HASH_MASK)) { 408 return false; 409 } 410 String[] targetSegments = target.segments; 411 int i = segments.length; 412 // check segment count 413 if (i != targetSegments.length) { 414 return false; 415 } 416 // check segments in reverse order - later segments more likely to differ 417 while (--i >= 0) { 418 if (!segments[i].equals(targetSegments[i])) { 419 return false; 420 } 421 } 422 return true; 423 } 424 425 @Override 426 public int hashCode() { 427 return separators & HASH_MASK; 428 } 429 430 public String getFileExtension() { 431 if (hasTrailingSeparator()) { 432 return null; 433 } 434 String lastSegment = lastSegment(); 435 if (lastSegment == null) { 436 return null; 437 } 438 int index = lastSegment.lastIndexOf('.'); 439 if (index == -1) { 440 return null; 441 } 442 return lastSegment.substring(index + 1); 443 } 444 445 public boolean hasTrailingSeparator() { 446 return (separators & HAS_TRAILING) != 0; 447 } 448 449 /** 450 * Initializes the current path with the given string. 451 */ 452 private Path initialize(String path) { 453 assert path != null; 454 455 path = collapseSlashes(path); 456 int len = path.length(); 457 458 // compute the separators array 459 if (len < 2) { 460 if (len == 1 && path.charAt(0) == SEPARATOR) { 461 separators = HAS_LEADING; 462 } else { 463 separators = 0; 464 } 465 } else { 466 boolean hasLeading = path.charAt(0) == SEPARATOR; 467 boolean hasTrailing = path.charAt(len - 1) == SEPARATOR; 468 separators = hasLeading ? HAS_LEADING : 0; 469 if (hasTrailing) { 470 separators |= HAS_TRAILING; 471 } 472 } 473 // compute segments and ensure canonical form 474 segments = computeSegments(path); 475 if (!canonicalize()) { 476 // compute hash now because canonicalize didn't need to do it 477 separators = (separators & ALL_SEPARATORS) | (computeHashCode() << USED_BITS); 478 } 479 return this; 480 } 481 482 public boolean isAbsolute() { 483 // it's absolute if it has a leading separator 484 return (separators & HAS_LEADING) != 0; 485 } 486 487 public boolean isEmpty() { 488 // true if no segments and no leading prefix 489 return segments.length == 0 && ((separators & ALL_SEPARATORS) != HAS_LEADING); 490 } 491 492 public boolean isPrefixOf(Path anotherPath) { 493 if (isEmpty() || (isRoot() && anotherPath.isAbsolute())) { 494 return true; 495 } 496 int len = segments.length; 497 if (len > anotherPath.segmentCount()) { 498 return false; 499 } 500 for (int i = 0; i < len; i++) { 501 if (!segments[i].equals(anotherPath.segment(i))) { 502 return false; 503 } 504 } 505 return true; 506 } 507 508 public boolean isRoot() { 509 // must have no segments, a leading separator, and not be a UNC path. 510 return this == ROOT || (segments.length == 0 && ((separators & ALL_SEPARATORS) == HAS_LEADING)); 511 } 512 513 public static boolean isValidPath(String path) { 514 Path test = new Path(path); 515 for (int i = 0, max = test.segmentCount(); i < max; i++) { 516 if (!isValidSegment(test.segment(i))) { 517 return false; 518 } 519 } 520 return true; 521 } 522 523 private static boolean isValidSegment(String segment) { 524 int size = segment.length(); 525 if (size == 0) { 526 return false; 527 } 528 for (int i = 0; i < size; i++) { 529 char c = segment.charAt(i); 530 if (c == '/') { 531 return false; 532 } 533 } 534 return true; 535 } 536 537 public String lastSegment() { 538 int len = segments.length; 539 return len == 0 ? null : segments[len - 1]; 540 } 541 542 public Path makeAbsolute() { 543 if (isAbsolute()) { 544 return this; 545 } 546 Path result = new Path(segments, separators | HAS_LEADING); 547 // may need canonicalizing if it has leading ".." or "." segments 548 if (result.segmentCount() > 0) { 549 String first = result.segment(0); 550 if (first.equals("..") || first.equals(".")) { //$NON-NLS-1$ //$NON-NLS-2$ 551 result.canonicalize(); 552 } 553 } 554 return result; 555 } 556 557 public Path makeRelative() { 558 if (!isAbsolute()) { 559 return this; 560 } 561 return new Path(segments, separators & HAS_TRAILING); 562 } 563 564 public int matchingFirstSegments(Path anotherPath) { 565 assert anotherPath != null; 566 int anotherPathLen = anotherPath.segmentCount(); 567 int max = Math.min(segments.length, anotherPathLen); 568 int count = 0; 569 for (int i = 0; i < max; i++) { 570 if (!segments[i].equals(anotherPath.segment(i))) { 571 return count; 572 } 573 count++; 574 } 575 return count; 576 } 577 578 public Path removeFileExtension() { 579 String extension = getFileExtension(); 580 if (extension == null || extension.equals("")) { //$NON-NLS-1$ 581 return this; 582 } 583 String lastSegment = lastSegment(); 584 int index = lastSegment.lastIndexOf(extension) - 1; 585 return removeLastSegments(1).append(lastSegment.substring(0, index)); 586 } 587 588 public Path removeFirstSegments(int count) { 589 if (count == 0) { 590 return this; 591 } 592 if (count >= segments.length) { 593 return new Path(NO_SEGMENTS, 0); 594 } 595 assert count > 0; 596 int newSize = segments.length - count; 597 String[] newSegments = new String[newSize]; 598 System.arraycopy(segments, count, newSegments, 0, newSize); 599 600 // result is always a relative path 601 return new Path(newSegments, separators & HAS_TRAILING); 602 } 603 604 public Path removeLastSegments(final int count) { 605 if (count == 0) { 606 return this; 607 } 608 if (count >= segments.length) { 609 // result will have no trailing separator 610 return new Path(NO_SEGMENTS, separators & HAS_LEADING); 611 } 612 assert count > 0; 613 final int newSize = segments.length - count; 614 final String[] newSegments = new String[newSize]; 615 System.arraycopy(segments, 0, newSegments, 0, newSize); 616 return new Path(newSegments, separators); 617 } 618 619 public Path removeTrailingSeparator() { 620 if (!hasTrailingSeparator()) { 621 return this; 622 } 623 return new Path(segments, separators & HAS_LEADING); 624 } 625 626 public String segment(int index) { 627 if (index >= segments.length) { 628 return null; 629 } 630 return segments[index]; 631 } 632 633 public int segmentCount() { 634 return segments.length; 635 } 636 637 public String[] segments() { 638 final String[] segmentCopy = new String[segments.length]; 639 System.arraycopy(segments, 0, segmentCopy, 0, segments.length); 640 return segmentCopy; 641 } 642 643 @Override 644 public String toString() { 645 final int resultSize = computeLength(); 646 if (resultSize <= 0) { 647 return EMPTY_STRING; 648 } 649 char[] result = new char[resultSize]; 650 int offset = 0; 651 if ((separators & HAS_LEADING) != 0) { 652 result[offset++] = SEPARATOR; 653 } 654 final int len = segments.length - 1; 655 if (len >= 0) { 656 // append all but the last segment, with separators 657 for (int i = 0; i < len; i++) { 658 final int size = segments[i].length(); 659 segments[i].getChars(0, size, result, offset); 660 offset += size; 661 result[offset++] = SEPARATOR; 662 } 663 // append the last segment 664 final int size = segments[len].length(); 665 segments[len].getChars(0, size, result, offset); 666 offset += size; 667 } 668 if ((separators & HAS_TRAILING) != 0) { 669 result[offset] = SEPARATOR; 670 } 671 return new String(result); 672 } 673 674 public Path uptoSegment(final int count) { 675 if (count == 0) { 676 return new Path(NO_SEGMENTS, separators & HAS_LEADING); 677 } 678 if (count >= segments.length) { 679 return this; 680 } 681 assert count > 0; // Invalid parameter to Path.uptoSegment 682 final String[] newSegments = new String[count]; 683 System.arraycopy(segments, 0, newSegments, 0, count); 684 return new Path(newSegments, separators); 685 } 686 687 /** 688 * Gets the name of the icon file so that it can be displayed as alt text. 689 */ 690 public static String getFileNameFromPath(String iconPath) { 691 String iconName; 692 // String fileSeparator = System.getProperty("file.separator"); 693 694 // temporary not working with the file separator, only with / 695 int firstCharOfIconName = iconPath.lastIndexOf(SEPARATOR); 696 int lastCharOfIconName = iconPath.lastIndexOf("."); 697 if (firstCharOfIconName == -1) { 698 iconName = iconPath; 699 } else { 700 iconName = iconPath.substring(firstCharOfIconName, lastCharOfIconName); 701 } 702 return iconName; 703 } 704 705}