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}