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}