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