View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.datamodel.niofs;
20  
21  import static org.mycore.datamodel.niofs.MCRAbstractFileSystem.SEPARATOR;
22  import static org.mycore.datamodel.niofs.MCRAbstractFileSystem.SEPARATOR_STRING;
23  
24  import java.io.File;
25  import java.io.IOError;
26  import java.io.IOException;
27  import java.net.URI;
28  import java.net.URISyntaxException;
29  import java.nio.file.FileStore;
30  import java.nio.file.InvalidPathException;
31  import java.nio.file.LinkOption;
32  import java.nio.file.Path;
33  import java.nio.file.ProviderMismatchException;
34  import java.nio.file.WatchEvent;
35  import java.nio.file.WatchEvent.Kind;
36  import java.nio.file.WatchEvent.Modifier;
37  import java.nio.file.WatchKey;
38  import java.nio.file.WatchService;
39  import java.text.MessageFormat;
40  import java.text.Normalizer;
41  import java.util.ArrayList;
42  import java.util.Iterator;
43  import java.util.Locale;
44  import java.util.NoSuchElementException;
45  import java.util.Objects;
46  
47  import org.mycore.common.MCRException;
48  
49  import com.google.common.primitives.Ints;
50  
51  /**
52   *  IFS implementation of the {@link Path} interface.
53   *  Absolute path have this form: <code>{owner}':/'{path}</code>
54   * @author Thomas Scheffler (yagee)
55   *
56   */
57  public abstract class MCRPath implements Path {
58  
59      String root, path, stringValue;
60  
61      private int[] offsets;
62  
63      /**
64       *
65       */
66      MCRPath(final String root, final String path) {
67          this.root = root;
68          this.path = normalizeAndCheck(Objects.requireNonNull(path, "path may not be null"));
69          if (root == null || root.isEmpty()) {
70              this.root = "";
71              stringValue = this.path;
72          } else {
73              if (!path.isEmpty() && path.charAt(0) != SEPARATOR) {
74                  final String msg = new MessageFormat("If root is given, path has to start with ''{0}'': {1}",
75                      Locale.ROOT).format(new Object[] { SEPARATOR_STRING, path });
76                  throw new IllegalArgumentException(msg);
77              }
78              stringValue = this.root + ":" + (this.path.isEmpty() ? SEPARATOR_STRING : this.path);
79          }
80          initNameComponents();
81      }
82  
83      public static MCRPath toMCRPath(final Path other) {
84          if (other == null) {
85              throw new NullPointerException();
86          }
87          if (!(other instanceof MCRPath)) {
88              throw new ProviderMismatchException("other is not an instance of MCRPath: " + other.getClass());
89          }
90          return (MCRPath) other;
91      }
92  
93      public static MCRPath getPath(String owner, String path) {
94          Path resolved = MCRPaths.getPath(owner, path);
95          return toMCRPath(resolved);
96      }
97  
98      /**
99       * Returns the root directory for a given derivate.
100      * 
101      * @param owner the file owner (usually the id of a derivate)
102      * @return the root path
103      */
104     public static MCRPath getRootPath(String owner) {
105         return getPath(owner, "/");
106     }
107 
108     /**
109      * removes redundant slashes and checks for invalid characters
110      * @param uncleanPath path to check
111      * @return normalized path
112      * @throws InvalidPathException if <code>uncleanPath</code> contains invalid characters
113      */
114     static String normalizeAndCheck(final String uncleanPath) {
115         String unicodeNormalizedUncleanPath = Normalizer.normalize(uncleanPath, Normalizer.Form.NFC);
116 
117         char prevChar = 0;
118         final boolean afterSeparator = false;
119         for (int i = 0; i < unicodeNormalizedUncleanPath.length(); i++) {
120             final char c = unicodeNormalizedUncleanPath.charAt(i);
121             checkCharacter(unicodeNormalizedUncleanPath, c, afterSeparator);
122             if (c == SEPARATOR && prevChar == SEPARATOR) {
123                 return normalize(unicodeNormalizedUncleanPath, unicodeNormalizedUncleanPath.length(), i - 1);
124             }
125             prevChar = c;
126         }
127         if (prevChar == SEPARATOR) {
128             //remove final slash
129             return normalize(unicodeNormalizedUncleanPath, unicodeNormalizedUncleanPath.length(),
130                 unicodeNormalizedUncleanPath.length() - 1);
131         }
132         return unicodeNormalizedUncleanPath;
133     }
134 
135     private static void checkCharacter(final String input, final char c, final boolean afterSeparator) {
136         if (c == '\u0000') {
137             throw new InvalidPathException(input, "Nul character is not allowed.");
138         }
139         if (afterSeparator && c == ':') {
140             throw new InvalidPathException(input, "':' is only allowed after owner id.");
141         }
142 
143     }
144 
145     private static String normalize(final String input, final int length, final int offset) {
146         if (length == 0) {
147             return input;
148         }
149         int newLength = length;
150         while (newLength > 0 && input.charAt(newLength - 1) == SEPARATOR) {
151             newLength--;
152         }
153         if (newLength == 0) {
154             return SEPARATOR_STRING;
155         }
156         final StringBuilder sb = new StringBuilder(input.length());
157         boolean afterSeparator = false;
158         if (offset > 0) {
159             final String prefix = input.substring(0, offset);
160             afterSeparator = prefix.contains(SEPARATOR_STRING);
161             sb.append(prefix);
162         }
163         char prevChar = 0;
164         for (int i = offset; i < newLength; i++) {
165             final char c = input.charAt(i);
166             checkCharacter(input, c, afterSeparator);
167             if (c == SEPARATOR && prevChar == SEPARATOR) {
168                 continue;
169             }
170             sb.append(c);
171             if (!afterSeparator && c == SEPARATOR) {
172                 afterSeparator = true;
173             }
174             prevChar = c;
175         }
176         return sb.toString();
177     }
178 
179     /* (non-Javadoc)
180      * @see java.nio.file.Path#compareTo(java.nio.file.Path)
181      */
182     @Override
183     public int compareTo(final Path other) {
184         final MCRPath that = (MCRPath) Objects.requireNonNull(other);
185         return toString().compareTo(that.toString());
186     }
187 
188     /* (non-Javadoc)
189      * @see java.nio.file.Path#endsWith(java.nio.file.Path)
190      */
191     @Override
192     public boolean endsWith(final Path other) {
193         if (!(Objects.requireNonNull(other, "other Path may not be null.") instanceof MCRPath)) {
194             return false;
195         }
196         final MCRPath that = (MCRPath) other;
197         if (this == that) {
198             return true;
199         }
200         final int thatOffsetCount = that.offsets.length;
201         final int thisOffsetCount = offsets.length;
202         final int thatPathLength = that.path.length();
203         final int thisPathLength = path.length();
204 
205         if (thatPathLength > thisPathLength) {
206             return false;
207         }
208         //checks required by Path.endsWidth()
209         final boolean thatIsAbsolute = that.isAbsolute();
210         final boolean thisIsAbsolute = isAbsolute();
211         if (thatIsAbsolute) {
212             if (!thisIsAbsolute) {
213                 return false;
214             }
215             //roots must be equal if both path contains one
216             if (!root.equals(that.root)) {
217                 return false;
218             }
219             //path must be equal too
220             return Objects.deepEquals(offsets, that.offsets) && path.equals(that.path)
221                 && that.getFileSystem().equals(getFileSystem());
222         }
223 
224         //that is not absolute
225         //check offsets
226         if (thatOffsetCount > thisOffsetCount) {
227             return false;
228         }
229         if (thisOffsetCount == thatOffsetCount && thisPathLength != thatPathLength) {
230             return false;
231         }
232         int thisPos = thisOffsetCount - thatOffsetCount;
233         for (int i = thisPos; i < thisOffsetCount; i++) {
234             if (that.offsets[i - thisPos] != offsets[i]) {
235                 return false;
236             }
237         }
238         //check characters
239         thisPos = offsets[thisOffsetCount - thatOffsetCount];
240         int thatPos = that.offsets[0];
241         if (thatPathLength - thatPos != thisPathLength - thisPos) {
242             return false;
243         }
244         while (thisPos < thisPathLength) {
245             if (path.charAt(thisPos++) != that.path.charAt(thatPos++)) {
246                 return false;
247             }
248         }
249 
250         return that.getFileSystem().equals(getFileSystem());
251     }
252 
253     /* (non-Javadoc)
254      * @see java.nio.file.Path#endsWith(java.lang.String)
255      */
256     @Override
257     public boolean endsWith(final String other) {
258         return endsWith(getFileSystem().getPath(other));
259     }
260 
261     @Override
262     public boolean equals(final Object obj) {
263         if (obj == null) {
264             return false;
265         }
266         if (!(obj instanceof MCRPath)) {
267             return false;
268         }
269         final MCRPath that = (MCRPath) obj;
270         if (!getFileSystem().equals(that.getFileSystem())) {
271             return false;
272         }
273         return stringValue.equals(that.stringValue);
274     }
275 
276     /* (non-Javadoc)
277      * @see java.nio.file.Path#getFileName()
278      */
279     @Override
280     public Path getFileName() {
281         final int nameCount = getNameCount();
282         if (nameCount == 0) {
283             return null;
284         }
285         final int lastOffset = offsets[nameCount - 1];
286         final String fileName = path.substring(lastOffset);
287         return MCRAbstractFileSystem.getPath(null, fileName, getFileSystem());
288     }
289 
290     @Override
291     public abstract MCRAbstractFileSystem getFileSystem();
292 
293     /* (non-Javadoc)
294      * @see java.nio.file.Path#getName(int)
295      */
296     @Override
297     public Path getName(final int index) {
298         final int nameCount = getNameCount();
299         if (index < 0 || index >= nameCount) {
300             throw new IllegalArgumentException();
301         }
302         final String pathElement = getPathElement(index);
303         return MCRAbstractFileSystem.getPath(null, pathElement, getFileSystem());
304     }
305 
306     /* (non-Javadoc)
307      * @see java.nio.file.Path#getNameCount()
308      */
309     @Override
310     public int getNameCount() {
311         return offsets.length;
312     }
313 
314     public String getOwner() {
315         return root;
316     }
317 
318     public String getOwnerRelativePath() {
319         return (path.equals("")) ? "/" : path;
320     }
321 
322     /**
323      * returns complete subpath.
324      * same as {@link #subpath(int, int)} with '0' and '{@link #getNameCount()}'.
325      */
326     public MCRPath subpathComplete() {
327         return isAbsolute() ? subpath(0, offsets.length) : this;
328     }
329 
330     /* (non-Javadoc)
331      * @see java.nio.file.Path#getParent()
332      */
333     @Override
334     public MCRPath getParent() {
335         final int nameCount = getNameCount();
336         if (nameCount == 0) {
337             return null;
338         }
339         final int lastOffset = offsets[nameCount - 1] - 1;
340         if (lastOffset <= 0) {
341             if (root.isEmpty()) {
342                 if (path.startsWith("/")) {
343                     //we have root as parent
344                     return MCRAbstractFileSystem.getPath(root, "/", getFileSystem());
345                 }
346                 // path is like "foo" -> no parent
347                 return null;
348             }
349             return getRoot();
350         }
351         return MCRAbstractFileSystem.getPath(root, path.substring(0, lastOffset), getFileSystem());
352     }
353 
354     /* (non-Javadoc)
355      * @see java.nio.file.Path#getRoot()
356      */
357     @Override
358     public MCRPath getRoot() {
359         if (!isAbsolute()) {
360             return null;
361         }
362         if (getNameCount() == 0) {
363             return this;
364         }
365         return getFileSystem().getRootDirectory(root);
366     }
367 
368     @Override
369     public int hashCode() {
370         return stringValue.hashCode();
371     }
372 
373     /* (non-Javadoc)
374      * @see java.nio.file.Path#isAbsolute()
375      */
376     @Override
377     public boolean isAbsolute() {
378         return root == null || !root.isEmpty();
379     }
380 
381     /* (non-Javadoc)
382      * @see java.nio.file.Path#iterator()
383      */
384     @Override
385     public Iterator<Path> iterator() {
386         return new Iterator<>() {
387             int i = 0;
388 
389             @Override
390             public boolean hasNext() {
391                 return i < getNameCount();
392             }
393 
394             @Override
395             public Path next() {
396                 if (hasNext()) {
397                     final Path result = getName(i);
398                     i++;
399                     return result;
400                 }
401                 throw new NoSuchElementException();
402             }
403 
404             @Override
405             public void remove() {
406                 throw new UnsupportedOperationException();
407             }
408         };
409     }
410 
411     /* (non-Javadoc)
412      * @see java.nio.file.Path#normalize()
413      */
414     @Override
415     public Path normalize() {
416         final int count = getNameCount();
417         int remaining = count;
418         final boolean[] ignoreSubPath = new boolean[count];
419 
420         for (int i = 0; i < count; i++) {
421             if (ignoreSubPath[i]) {
422                 continue;
423             }
424             final int subPathIndex = offsets[i];
425             int subPathLength;
426             if (i == offsets.length - 1) {
427                 subPathLength = path.length() - subPathIndex;
428             } else {
429                 subPathLength = offsets[i + 1] - subPathIndex - 1;
430             }
431             if (path.charAt(subPathIndex) == '.') {
432                 if (subPathLength == 1) {
433                     ignoreSubPath[i] = true;
434                     remaining--;
435                 } else if (subPathLength == 2 && path.charAt(subPathIndex + 1) == '.') {
436                     ignoreSubPath[i] = true;
437                     remaining--;
438                     //go backward to the last unignored and mark it ignored
439                     //can't normalize if all preceding elements already ignored
440                     for (int r = i - 1; r > 0; r--) {
441                         if (!ignoreSubPath[r]) {
442                             ignoreSubPath[r] = true;
443                             remaining--;
444                             break;
445                         }
446                     }
447                 }
448             }
449 
450         }
451 
452         if (count == remaining) {
453             return this;
454         }
455         if (remaining == 0) {
456             return isAbsolute() ? getRoot() : getFileSystem().emptyPath();
457         }
458         final StringBuilder sb = new StringBuilder(path.length());
459         if (isAbsolute()) {
460             sb.append(SEPARATOR);
461         }
462         for (int i = 0; i < count; i++) {
463             if (ignoreSubPath[i]) {
464                 continue;
465             }
466             sb.append(getPathElement(i));
467             if (--remaining > 0) {
468                 sb.append('/');
469             }
470         }
471         return MCRAbstractFileSystem.getPath(root, sb.toString(), getFileSystem());
472     }
473 
474     /* (non-Javadoc)
475      * @see java.nio.file.Path#register(java.nio.file.WatchService, java.nio.file.WatchEvent.Kind[])
476      */
477     @Override
478     public WatchKey register(final WatchService watcher, final Kind<?>... events) throws IOException {
479         return register(watcher, events, new WatchEvent.Modifier[0]);
480     }
481 
482     /* (non-Javadoc)
483      * @see java.nio.file.Path#register(java.nio.file.WatchService, java.nio.file.WatchEvent.Kind[], java.nio.file.WatchEvent.Modifier[])
484      */
485     @Override
486     public WatchKey register(final WatchService watcher, final Kind<?>[] events, final Modifier... modifiers)
487         throws IOException {
488         throw new UnsupportedOperationException();
489     }
490 
491     /* (non-Javadoc)
492      * @see java.nio.file.Path#relativize(java.nio.file.Path)
493      */
494     @Override
495     public MCRPath relativize(final Path other) {
496         if (equals(Objects.requireNonNull(other, "Cannot relativize against 'null'."))) {
497             return getFileSystem().emptyPath();
498         }
499         if (isAbsolute() != other.isAbsolute()) {
500             throw new IllegalArgumentException("'other' must be absolute if and only if this is absolute, too.");
501         }
502         final MCRPath that = toMCRPath(other);
503         if (!isAbsolute() && isEmpty()) {
504             return that;
505         }
506         URI thisURI;
507         URI thatURI;
508         try {
509             thisURI = new URI(null, null, path, null);
510             thatURI = new URI(null, null, that.path, null);
511         } catch (URISyntaxException e) {
512             throw new MCRException(e);
513         }
514         final URI relativizedURI = thisURI.relativize(thatURI);
515         if (thatURI.equals(relativizedURI)) {
516             return that;
517         }
518         return MCRAbstractFileSystem.getPath(null, relativizedURI.getPath(), getFileSystem());
519     }
520 
521     private static boolean isEmpty(Path test) {
522         return test instanceof MCRPath && ((MCRPath) test).isEmpty()
523             || (test.getNameCount() == 1 && test.getName(0).toString().isEmpty());
524     }
525 
526     /* (non-Javadoc)
527      * @see java.nio.file.Path#resolve(java.nio.file.Path)
528      */
529     @Override
530     public Path resolve(final Path other) {
531         if (other.isAbsolute()) {
532             return other;
533         }
534         if (isEmpty(other)) {
535             return this;
536         }
537         String otherStr = toMCRPathString(other);
538         final int baseLength = path.length();
539         final int childLength = other.toString().length();
540         if (isEmpty() || otherStr.charAt(0) == SEPARATOR) {
541             return root == null ? other : MCRAbstractFileSystem.getPath(root, otherStr, getFileSystem());
542         }
543         final StringBuilder result = new StringBuilder(baseLength + 1 + childLength);
544         if (baseLength == 1 && path.charAt(0) == SEPARATOR) {
545             result.append(SEPARATOR);
546             result.append(otherStr);
547         } else {
548             result.append(path);
549             result.append(SEPARATOR);
550             result.append(otherStr);
551         }
552         return MCRAbstractFileSystem.getPath(root, result.toString(), getFileSystem());
553     }
554 
555     private String toMCRPathString(final Path other) {
556         String otherStr = other.toString();
557         String otherSeperator = other.getFileSystem().getSeparator();
558         return otherSeperator.equals(SEPARATOR_STRING) ? otherStr : otherStr.replace(otherSeperator, SEPARATOR_STRING);
559     }
560 
561     /* (non-Javadoc)
562      * @see java.nio.file.Path#resolve(java.lang.String)
563      */
564     @Override
565     public Path resolve(final String other) {
566         return resolve(getFileSystem().getPath(other));
567     }
568 
569     /* (non-Javadoc)
570      * @see java.nio.file.Path#resolveSibling(java.nio.file.Path)
571      */
572     @Override
573     public Path resolveSibling(final Path other) {
574         Objects.requireNonNull(other);
575         final Path parent = getParent();
576         return parent == null ? other : parent.resolve(other);
577     }
578 
579     /* (non-Javadoc)
580      * @see java.nio.file.Path#resolveSibling(java.lang.String)
581      */
582     @Override
583     public Path resolveSibling(final String other) {
584         return resolveSibling(getFileSystem().getPath(other));
585     }
586 
587     /* (non-Javadoc)
588      * @see java.nio.file.Path#startsWith(java.nio.file.Path)
589      */
590     @Override
591     public boolean startsWith(final Path other) {
592         if (!(Objects.requireNonNull(other, "other Path may not be null.") instanceof MCRPath)) {
593             return false;
594         }
595         final MCRPath that = (MCRPath) other;
596         if (this == that) {
597             return true;
598         }
599         final int thatOffsetCount = that.offsets.length;
600         final int thisOffsetCount = offsets.length;
601 
602         //checks required by Path.startsWidth()
603         if (thatOffsetCount > thisOffsetCount || that.path.length() > path.length()) {
604             return false;
605         }
606         if (thatOffsetCount == thisOffsetCount && that.path.length() != path.length()) {
607             return false;
608         }
609         if (!Objects.deepEquals(root, that.root)) {
610             return false;
611         }
612         if (!Objects.deepEquals(getFileSystem(), that.getFileSystem())) {
613             return false;
614         }
615         for (int i = 0; i < thatOffsetCount; i++) {
616             if (that.offsets[i] != offsets[i]) {
617                 return false;
618             }
619         }
620         if (!path.startsWith(that.path)) {
621             return false;
622         }
623         final int thatPathLength = that.path.length();
624         // return false if this.path==/foo/bar and that.path==/path
625         return thatPathLength <= path.length() || path.charAt(thatPathLength) == SEPARATOR;
626     }
627 
628     /* (non-Javadoc)
629      * @see java.nio.file.Path#startsWith(java.lang.String)
630      */
631     @Override
632     public boolean startsWith(final String other) {
633         return startsWith(getFileSystem().getPath(other));
634     }
635 
636     /* (non-Javadoc)
637      * @see java.nio.file.Path#subpath(int, int)
638      */
639     @Override
640     public MCRPath subpath(final int beginIndex, final int endIndex) {
641         if (beginIndex < 0) {
642             throw new IllegalArgumentException("beginIndex may not be negative: " + beginIndex);
643         }
644         if (beginIndex >= offsets.length) {
645             throw new IllegalArgumentException("beginIndex may not be greater or qual to the number of path elements("
646                 + offsets.length + "): " + beginIndex);
647         }
648         if (endIndex > offsets.length) {
649             throw new IllegalArgumentException("endIndex may not be greater that the number of path elements("
650                 + offsets.length + "): " + endIndex);
651         }
652         if (beginIndex >= endIndex) {
653             throw new IllegalArgumentException("endIndex must be greater than beginIndex(" + beginIndex + "): "
654                 + endIndex);
655         }
656         final int begin = offsets[beginIndex];
657         final int end = endIndex == offsets.length ? path.length() : offsets[endIndex] - 1;
658         return MCRAbstractFileSystem.getPath(null, path.substring(begin, end), getFileSystem());
659     }
660 
661     /* (non-Javadoc)
662      * @see java.nio.file.Path#toAbsolutePath()
663      */
664     @Override
665     public Path toAbsolutePath() {
666         if (isAbsolute()) {
667             return this;
668         }
669         throw new IOError(new IOException("There is no default directory to resolve " + this + " against to."));
670     }
671 
672     /* (non-Javadoc)
673      * @see java.nio.file.Path#toFile()
674      */
675     @Override
676     public File toFile() {
677         throw new UnsupportedOperationException();
678     }
679 
680     /* (non-Javadoc)
681      * @see java.nio.file.Path#toRealPath(java.nio.file.LinkOption[])
682      */
683     @Override
684     public Path toRealPath(final LinkOption... options) throws IOException {
685         if (isAbsolute()) {
686             final MCRPath normalized = (MCRPath) normalize();
687             getFileSystem().provider().checkAccess(normalized); //eventually throws IOException
688             return normalized;
689         }
690         throw new IOException("Cannot get real path from relative path.");
691     }
692 
693     @SuppressWarnings("resource")
694     public Path toPhysicalPath() throws IOException {
695         if (isAbsolute()) {
696             for (FileStore fs : getFileSystem().getFileStores()) {
697                 if (fs instanceof MCRAbstractFileStore) {
698                     Path physicalPath = ((MCRAbstractFileStore) fs).getPhysicalPath(this);
699                     if (physicalPath != null) {
700                         return physicalPath;
701                     }
702                 }
703             }
704             return null;
705         }
706         throw new IOException("Cannot get real path from relative path.");
707     }
708 
709     @Override
710     public String toString() {
711         return stringValue;
712     }
713 
714     /* (non-Javadoc)
715      * @see java.nio.file.Path#toUri()
716      */
717     @Override
718     public URI toUri() {
719         try {
720             if (isAbsolute()) {
721                 return MCRPaths.getURI(getFileSystem().provider().getScheme(), root, path);
722             }
723             return new URI(null, null, path, null);
724         } catch (URISyntaxException e) {
725             throw new RuntimeException(e);
726         }
727     }
728 
729     private String getPathElement(final int index) {
730         final int begin = offsets[index];
731         final int end = index == offsets.length - 1 ? path.length() : offsets[index + 1] - 1;
732         return path.substring(begin, end);
733     }
734 
735     private void initNameComponents() {
736         final ArrayList<Integer> list = new ArrayList<>();
737         if (isEmpty()) {
738             if (!isAbsolute()) {
739                 // is empty path but not root component
740                 // empty path considered to have one name element
741                 list.add(0);
742             }
743         } else {
744             int start = 0;
745             while (start < path.length()) {
746                 if (path.charAt(start) != SEPARATOR) {
747                     break;
748                 }
749                 start++;
750             }
751             int off = start;
752             while (off < path.length()) {
753                 if (path.charAt(off) != SEPARATOR) {
754                     off++;
755                 } else {
756                     list.add(start);
757                     start = ++off;
758                 }
759             }
760             if (start != off) {
761                 list.add(start);
762             }
763         }
764         offsets = Ints.toArray(list);
765     }
766 
767     private boolean isEmpty() {
768         return path.isEmpty();
769     }
770 
771 }