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}