001/* 002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * 017 * $Id$ 018 */ 019 020package org.nuxeo.shell.utils; 021 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.List; 025 026/** 027 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 028 */ 029public class Path implements Serializable { 030 031 public static final char SEP = '/'; 032 033 protected static final int HAS_LEADING = 1; 034 035 protected static final int HAS_TRAILING = 2; 036 037 protected static final int HASH_MASK = ~HAS_TRAILING; 038 039 protected static final int ALL_SEPARATORS = HAS_LEADING | HAS_TRAILING; 040 041 protected static final int USED_BITS = 2; 042 043 protected static final String[] NO_SEGMENTS = new String[0]; 044 045 private static final long serialVersionUID = 5008948361159403627L; 046 047 public static final Path EMPTY = new Path(new String[0], 0) { 048 private static final long serialVersionUID = -1731993368728652448L; 049 050 @Override 051 public String toString() { 052 return ""; 053 } 054 }; 055 056 public static final Path ROOT = new Path(new String[0], 1) { 057 private static final long serialVersionUID = -6689687769363666578L; 058 059 @Override 060 public String toString() { 061 return "/"; 062 } 063 }; 064 065 protected String[] segments; 066 067 protected int flags; 068 069 public Path(String path) { 070 init(path); 071 updateHashCode(); 072 } 073 074 public Path(String path, int flags) { 075 init(path); 076 this.flags |= flags & ALL_SEPARATORS; 077 updateHashCode(); 078 } 079 080 public Path(String[] segments, int flags) { 081 this.segments = segments; 082 this.flags = flags; 083 updateHashCode(); 084 } 085 086 public Path(Path path) { 087 segments = path.segments; 088 flags = path.flags; 089 } 090 091 private void init(String path) { 092 List<String> segments = new ArrayList<String>(); 093 int len = path.length(); 094 if (len == 0) { 095 flags = 0; 096 this.segments = NO_SEGMENTS; 097 return; 098 } 099 if (len == 1) { 100 char c = path.charAt(0); 101 if (c == '/') { 102 flags = HAS_LEADING; 103 this.segments = NO_SEGMENTS; 104 return; 105 } else if (c == '.') { 106 flags = 0; 107 this.segments = NO_SEGMENTS; 108 return; 109 } else { 110 flags = 0; 111 this.segments = new String[] { path }; 112 return; 113 } 114 } 115 char[] chars = path.toCharArray(); 116 flags = chars[0] == '/' ? HAS_LEADING : 0; 117 if (chars[len - 1] == '/') { 118 flags |= HAS_TRAILING; 119 } 120 121 int slash = 0; 122 int off = 0; 123 int cnt = 0; 124 for (int i = 0; i < len; i++) { 125 char c = chars[i]; 126 switch (c) { 127 case SEP: 128 if (slash == 0) { // first slash 129 if (cnt > 0) { // segment end 130 segments.add(new String(chars, off, cnt)); 131 cnt = 0; 132 } 133 off = i; 134 } else { // ignore double slashes 135 off++; 136 } 137 slash++; 138 break; 139 case '.': 140 if (slash > 0 || i == 0) { 141 if (i < chars.length - 2) { // look ahead 2 chars 142 char c1 = chars[i + 1]; 143 char c2 = chars[i + 2]; 144 if (c1 == '.' && c2 == SEP) { // a .. segment 145 if (segments.isEmpty()) { // add a dot segment 146 segments.add(".."); 147 } else { // remove last segment 148 int last = segments.size() - 1; 149 if (!"..".equals(segments.get(last))) { 150 segments.remove(last); 151 } else { 152 segments.add(".."); 153 } 154 } 155 i += 2; 156 slash = 1; 157 continue; 158 } else if (c1 == SEP) { // a . segment - ignore it 159 i++; 160 slash = 1; 161 continue; 162 } 163 } else if (i < chars.length - 1 && chars[i + 1] == '/') { // ignore 164 // . 165 // segment 166 slash = 0; 167 continue; // TODO - we may add here the segment to avoid 168 // rereading the char 169 } 170 } 171 // do nothing (the char will be added to the segment) 172 default: 173 if (slash > 0) { 174 slash = 0; 175 off = i; 176 } 177 cnt++; 178 break; 179 } 180 } 181 if (cnt > 0) { 182 segments.add(new String(chars, off, cnt)); 183 } 184 int size = segments.size(); 185 if (size == 0) { 186 this.segments = NO_SEGMENTS; 187 } else { 188 this.segments = segments.toArray(new String[segments.size()]); 189 } 190 } 191 192 protected final void updateHashCode() { 193 flags = (flags & ALL_SEPARATORS) | (computeHashCode() << USED_BITS); 194 } 195 196 protected int computeHashCode() { 197 int hash = 17; 198 int segmentCount = segments.length; 199 for (int i = 0; i < segmentCount; i++) { 200 // this function tends to given a fairly even distribution 201 hash = hash * 37 + segments[i].hashCode(); 202 } 203 return hash; 204 } 205 206 public boolean isAbsolute() { 207 return (flags & HAS_LEADING) != 0; 208 } 209 210 public boolean isRelative() { 211 return (flags & HAS_LEADING) == 0; 212 } 213 214 public boolean isRoot() { 215 return (segments.length == 0) && ((flags & HAS_LEADING) != 0); 216 } 217 218 public boolean isEmpty() { 219 return (segments.length == 0) && ((flags & HAS_LEADING) == 0); 220 } 221 222 public boolean hasTrailingSeparator() { 223 return (flags & HAS_TRAILING) != 0; 224 } 225 226 public int segmentCount() { 227 return segments.length; 228 } 229 230 public String[] segments() { 231 final String[] segmentCopy = new String[segments.length]; 232 System.arraycopy(segments, 0, segmentCopy, 0, segments.length); 233 return segmentCopy; 234 } 235 236 public String segment(int index) { 237 if (index >= segmentCount()) { 238 return null; 239 } 240 return segments[index]; 241 } 242 243 public String lastSegment() { 244 int len = segments.length; 245 return len == 0 ? null : segments[len - 1]; 246 } 247 248 public String getFileExtension() { 249 int len = segments.length; 250 if (len == 0) { 251 return null; 252 } 253 String name = segments[len - 1]; 254 int p = name.lastIndexOf('.'); 255 if (p > -1) { 256 return name.substring(p + 1); 257 } 258 return null; 259 } 260 261 public String getFileName() { 262 int len = segments.length; 263 if (len == 0) { 264 return null; 265 } 266 String name = segments[len - 1]; 267 int p = name.lastIndexOf('.'); 268 if (p > -1) { 269 return name.substring(0, p); 270 } 271 return name; 272 } 273 274 public String[] getFileParts() { 275 int len = segments.length; 276 if (len == 0) { 277 return null; 278 } 279 String name = segments[len - 1]; 280 int p = name.lastIndexOf('.'); 281 if (p > -1) { 282 return new String[] { name.substring(0, p), name.substring(p + 1) }; 283 } 284 return new String[] { name, null }; 285 } 286 287 public Path makeAbsolute() { 288 if (isAbsolute()) { 289 return this; 290 } 291 int k = 0; 292 for (String segment : segments) { 293 if ("..".equals(segment)) { 294 k++; 295 } else { 296 break; 297 } 298 } 299 if (k > 0) { 300 String[] newSegments = new String[segments.length - k]; 301 System.arraycopy(segments, k, newSegments, 0, newSegments.length); 302 return new Path(newSegments, flags | HAS_LEADING); 303 } 304 return new Path(segments, flags | HAS_LEADING); 305 } 306 307 public Path makeRelative() { 308 if (isRelative()) { 309 return this; 310 } 311 return new Path(segments, flags & ~HAS_LEADING); 312 } 313 314 public Path removeTrailingSeparator() { 315 if (!hasTrailingSeparator()) { 316 return this; 317 } 318 return new Path(segments, flags & ~HAS_TRAILING); 319 } 320 321 public Path addTrailingSeparator() { 322 if (hasTrailingSeparator()) { 323 return this; 324 } 325 return new Path(segments, flags | HAS_TRAILING); 326 } 327 328 public Path removeLastSegments(int count) { 329 if (count == 0) { 330 return this; 331 } 332 if (count >= segments.length) { 333 // result will have no trailing separator 334 return (flags & HAS_LEADING) != 0 ? ROOT : EMPTY; 335 } 336 assert count > 0; 337 final int newSize = segments.length - count; 338 final String[] newSegments = new String[newSize]; 339 System.arraycopy(segments, 0, newSegments, 0, newSize); 340 return new Path(newSegments, flags); 341 } 342 343 public Path removeFirstSegments(int count) { 344 if (count == 0) { 345 return this; 346 } 347 if (count >= segments.length) { 348 return EMPTY; 349 } 350 assert count > 0; 351 int newSize = segments.length - count; 352 String[] newSegments = new String[newSize]; 353 System.arraycopy(segments, count, newSegments, 0, newSize); 354 355 // result is always a relative path 356 return new Path(newSegments, flags & HAS_TRAILING); 357 } 358 359 public Path removeFileExtension() { 360 String extension = getFileExtension(); 361 if (extension == null || extension.equals("")) { //$NON-NLS-1$ 362 return this; 363 } 364 String lastSegment = lastSegment(); 365 int index = lastSegment.lastIndexOf(extension) - 1; 366 return removeLastSegments(1).append(lastSegment.substring(0, index)); 367 } 368 369 public Path up() { 370 return removeLastSegments(1); 371 } 372 373 public Path getParent() { 374 return removeLastSegments(1); 375 } 376 377 public String getName() { 378 return lastSegment(); 379 } 380 381 public Path appendSegment(String segment) { 382 int myLen = segments.length; 383 String[] newSegments = new String[myLen + 1]; 384 System.arraycopy(segments, 0, newSegments, 0, myLen); 385 newSegments[myLen] = segment; 386 return new Path(newSegments, flags); 387 } 388 389 public Path append(Path tail) { 390 // optimize some easy cases 391 if (tail == null || tail.segmentCount() == 0) { 392 return this; 393 } 394 if (isEmpty()) { 395 return tail.makeRelative(); 396 } 397 if (isRoot()) { 398 return tail.makeAbsolute(); 399 } 400 401 int flags = this.flags; 402 403 int myLen = segments.length; 404 int tailLen = tail.segmentCount(); 405 int j = 0; 406 int s = 0; 407 // remove ../ segments from the appended path 408 for (int i = 0; i < tailLen; i++) { 409 String seg = tail.segments[i]; 410 if ("..".equals(seg)) { 411 j++; 412 } else if (".".equals(seg)) { 413 if (j == 0) { 414 s++; 415 } 416 } else { 417 break; 418 } 419 } 420 if (j > 0) { 421 s = j; 422 } 423 424 int k = myLen - j; 425 if (k < 0) { 426 myLen = -k; 427 } else { 428 myLen = k; 429 } 430 431 // concatenate the two segment arrays 432 String[] newSegments = new String[myLen + tailLen - s]; 433 if (k < 0) { 434 for (int i = 0; i < myLen; i++) { 435 newSegments[i] = ".."; 436 } 437 flags &= ~HAS_LEADING; 438 } else if (k > 0) { 439 System.arraycopy(segments, 0, newSegments, 0, myLen); 440 } 441 for (int i = s; i < tailLen; i++) { 442 newSegments[myLen + i - s] = tail.segment(i); 443 } 444 // use my leading separators and the tail's trailing separator 445 446 return new Path(newSegments, (flags & HAS_LEADING) | (tail.hasTrailingSeparator() ? HAS_TRAILING : 0)); 447 } 448 449 public Path append(String tail) { 450 // optimize addition of a single segment 451 if (tail.indexOf(SEP) == -1) { 452 int tailLength = tail.length(); 453 if (tailLength < 3) { 454 // some special cases 455 if (tailLength == 0 || ".".equals(tail)) { 456 return this; 457 } 458 if ("..".equals(tail)) { 459 return removeLastSegments(1); 460 } 461 } 462 // just add the segment 463 int myLen = segments.length; 464 String[] newSegments = new String[myLen + 1]; 465 System.arraycopy(segments, 0, newSegments, 0, myLen); 466 newSegments[myLen] = tail; 467 return new Path(newSegments, flags & ~HAS_TRAILING); 468 } 469 // go with easy implementation 470 return append(new Path(tail)); 471 } 472 473 public boolean equals(Object obj) { 474 if (this == obj) { 475 return true; 476 } 477 if (!(obj instanceof Path)) { 478 return false; 479 } 480 Path target = (Path) obj; 481 // check leading separators and hash code 482 if (flags != target.flags) { 483 return false; 484 } 485 String[] targetSegments = target.segments; 486 int i = segments.length; 487 // check segment count 488 if (i != targetSegments.length) { 489 return false; 490 } 491 // check segments in reverse order - later segments more likely to 492 // differ 493 while (--i >= 0) { 494 if (!segments[i].equals(targetSegments[i])) { 495 return false; 496 } 497 } 498 return true; 499 } 500 501 @Override 502 public int hashCode() { 503 return flags & HASH_MASK; 504 } 505 506 @Override 507 public String toString() { 508 int len = segments.length; 509 if (len == 0) { 510 return (flags & HAS_LEADING) != 0 ? "/" : ""; 511 } 512 StringBuilder buf = new StringBuilder(len * 16); 513 if ((flags & HAS_LEADING) != 0) { 514 buf.append(SEP); 515 } 516 buf.append(segments[0]); 517 for (int i = 1; i < len; i++) { 518 buf.append(SEP).append(segments[i]); 519 } 520 if ((flags & HAS_TRAILING) != 0) { 521 buf.append(SEP); 522 } 523 return buf.toString(); 524 } 525 526}