• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2013 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.jimfs;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 
22 import com.google.common.collect.ComparisonChain;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.Iterables;
25 import java.io.File;
26 import java.io.IOException;
27 import java.net.URI;
28 import java.nio.file.FileSystem;
29 import java.nio.file.LinkOption;
30 import java.nio.file.Path;
31 import java.nio.file.ProviderMismatchException;
32 import java.nio.file.WatchEvent;
33 import java.nio.file.WatchKey;
34 import java.nio.file.WatchService;
35 import java.util.AbstractList;
36 import java.util.ArrayDeque;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.Deque;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Objects;
44 import org.checkerframework.checker.nullness.compatqual.NullableDecl;
45 
46 /**
47  * Jimfs implementation of {@link Path}. Creation of new {@code Path} objects is delegated to the
48  * file system's {@link PathService}.
49  *
50  * @author Colin Decker
51  */
52 final class JimfsPath implements Path {
53 
54   @NullableDecl private final Name root;
55   private final ImmutableList<Name> names;
56   private final PathService pathService;
57 
JimfsPath(PathService pathService, @NullableDecl Name root, Iterable<Name> names)58   public JimfsPath(PathService pathService, @NullableDecl Name root, Iterable<Name> names) {
59     this.pathService = checkNotNull(pathService);
60     this.root = root;
61     this.names = ImmutableList.copyOf(names);
62   }
63 
64   /** Returns the root name, or null if there is no root. */
65   @NullableDecl
root()66   public Name root() {
67     return root;
68   }
69 
70   /** Returns the list of name elements. */
names()71   public ImmutableList<Name> names() {
72     return names;
73   }
74 
75   /**
76    * Returns the file name of this path. Unlike {@link #getFileName()}, this may return the name of
77    * the root if this is a root path.
78    */
79   @NullableDecl
name()80   public Name name() {
81     if (!names.isEmpty()) {
82       return Iterables.getLast(names);
83     }
84     return root;
85   }
86 
87   /**
88    * Returns whether or not this is the empty path, with no root and a single, empty string, name.
89    */
isEmptyPath()90   public boolean isEmptyPath() {
91     return root == null && names.size() == 1 && names.get(0).toString().isEmpty();
92   }
93 
94   @Override
getFileSystem()95   public FileSystem getFileSystem() {
96     return pathService.getFileSystem();
97   }
98 
99   /**
100    * Equivalent to {@link #getFileSystem()} but with a return type of {@code JimfsFileSystem}.
101    * {@code getFileSystem()}'s return type is left as {@code FileSystem} to make testing paths
102    * easier (as long as methods that access the file system in some way are not called, the file
103    * system can be a fake file system instance).
104    */
getJimfsFileSystem()105   public JimfsFileSystem getJimfsFileSystem() {
106     return (JimfsFileSystem) pathService.getFileSystem();
107   }
108 
109   @Override
isAbsolute()110   public boolean isAbsolute() {
111     return root != null;
112   }
113 
114   @Override
getRoot()115   public JimfsPath getRoot() {
116     if (root == null) {
117       return null;
118     }
119     return pathService.createRoot(root);
120   }
121 
122   @Override
getFileName()123   public JimfsPath getFileName() {
124     return names.isEmpty() ? null : getName(names.size() - 1);
125   }
126 
127   @Override
getParent()128   public JimfsPath getParent() {
129     if (names.isEmpty() || (names.size() == 1 && root == null)) {
130       return null;
131     }
132 
133     return pathService.createPath(root, names.subList(0, names.size() - 1));
134   }
135 
136   @Override
getNameCount()137   public int getNameCount() {
138     return names.size();
139   }
140 
141   @Override
getName(int index)142   public JimfsPath getName(int index) {
143     checkArgument(
144         index >= 0 && index < names.size(),
145         "index (%s) must be >= 0 and < name count (%s)",
146         index,
147         names.size());
148     return pathService.createFileName(names.get(index));
149   }
150 
151   @Override
subpath(int beginIndex, int endIndex)152   public JimfsPath subpath(int beginIndex, int endIndex) {
153     checkArgument(
154         beginIndex >= 0 && endIndex <= names.size() && endIndex > beginIndex,
155         "beginIndex (%s) must be >= 0; endIndex (%s) must be <= name count (%s) and > beginIndex",
156         beginIndex,
157         endIndex,
158         names.size());
159     return pathService.createRelativePath(names.subList(beginIndex, endIndex));
160   }
161 
162   /** Returns true if list starts with all elements of other in the same order. */
startsWith(List<?> list, List<?> other)163   private static boolean startsWith(List<?> list, List<?> other) {
164     return list.size() >= other.size() && list.subList(0, other.size()).equals(other);
165   }
166 
167   @Override
startsWith(Path other)168   public boolean startsWith(Path other) {
169     JimfsPath otherPath = checkPath(other);
170     return otherPath != null
171         && getFileSystem().equals(otherPath.getFileSystem())
172         && Objects.equals(root, otherPath.root)
173         && startsWith(names, otherPath.names);
174   }
175 
176   @Override
startsWith(String other)177   public boolean startsWith(String other) {
178     return startsWith(pathService.parsePath(other));
179   }
180 
181   @Override
endsWith(Path other)182   public boolean endsWith(Path other) {
183     JimfsPath otherPath = checkPath(other);
184     if (otherPath == null) {
185       return false;
186     }
187 
188     if (otherPath.isAbsolute()) {
189       return compareTo(otherPath) == 0;
190     }
191     return startsWith(names.reverse(), otherPath.names.reverse());
192   }
193 
194   @Override
endsWith(String other)195   public boolean endsWith(String other) {
196     return endsWith(pathService.parsePath(other));
197   }
198 
199   @Override
normalize()200   public JimfsPath normalize() {
201     if (isNormal()) {
202       return this;
203     }
204 
205     Deque<Name> newNames = new ArrayDeque<>();
206     for (Name name : names) {
207       if (name.equals(Name.PARENT)) {
208         Name lastName = newNames.peekLast();
209         if (lastName != null && !lastName.equals(Name.PARENT)) {
210           newNames.removeLast();
211         } else if (!isAbsolute()) {
212           // if there's a root and we have an extra ".." that would go up above the root, ignore it
213           newNames.add(name);
214         }
215       } else if (!name.equals(Name.SELF)) {
216         newNames.add(name);
217       }
218     }
219 
220     return Iterables.elementsEqual(newNames, names) ? this : pathService.createPath(root, newNames);
221   }
222 
223   /**
224    * Returns whether or not this path is in a normalized form. It's normal if it both contains no
225    * "." names and contains no ".." names in a location other than the start of the path.
226    */
isNormal()227   private boolean isNormal() {
228     if (getNameCount() == 0 || (getNameCount() == 1 && !isAbsolute())) {
229       return true;
230     }
231 
232     boolean foundNonParentName = isAbsolute(); // if there's a root, the path doesn't start with ..
233     boolean normal = true;
234     for (Name name : names) {
235       if (name.equals(Name.PARENT)) {
236         if (foundNonParentName) {
237           normal = false;
238           break;
239         }
240       } else {
241         if (name.equals(Name.SELF)) {
242           normal = false;
243           break;
244         }
245 
246         foundNonParentName = true;
247       }
248     }
249     return normal;
250   }
251 
252   /** Resolves the given name against this path. The name is assumed not to be a root name. */
resolve(Name name)253   JimfsPath resolve(Name name) {
254     if (name.toString().isEmpty()) {
255       return this;
256     }
257     return pathService.createPathInternal(
258         root, ImmutableList.<Name>builder().addAll(names).add(name).build());
259   }
260 
261   @Override
resolve(Path other)262   public JimfsPath resolve(Path other) {
263     JimfsPath otherPath = checkPath(other);
264     if (otherPath == null) {
265       throw new ProviderMismatchException(other.toString());
266     }
267 
268     if (isEmptyPath() || otherPath.isAbsolute()) {
269       return otherPath;
270     }
271     if (otherPath.isEmptyPath()) {
272       return this;
273     }
274     return pathService.createPath(
275         root, ImmutableList.<Name>builder().addAll(names).addAll(otherPath.names).build());
276   }
277 
278   @Override
resolve(String other)279   public JimfsPath resolve(String other) {
280     return resolve(pathService.parsePath(other));
281   }
282 
283   @Override
resolveSibling(Path other)284   public JimfsPath resolveSibling(Path other) {
285     JimfsPath otherPath = checkPath(other);
286     if (otherPath == null) {
287       throw new ProviderMismatchException(other.toString());
288     }
289 
290     if (otherPath.isAbsolute()) {
291       return otherPath;
292     }
293     JimfsPath parent = getParent();
294     if (parent == null) {
295       return otherPath;
296     }
297     return parent.resolve(other);
298   }
299 
300   @Override
resolveSibling(String other)301   public JimfsPath resolveSibling(String other) {
302     return resolveSibling(pathService.parsePath(other));
303   }
304 
305   @Override
relativize(Path other)306   public JimfsPath relativize(Path other) {
307     JimfsPath otherPath = checkPath(other);
308     if (otherPath == null) {
309       throw new ProviderMismatchException(other.toString());
310     }
311 
312     checkArgument(
313         Objects.equals(root, otherPath.root), "Paths have different roots: %s, %s", this, other);
314 
315     if (equals(other)) {
316       return pathService.emptyPath();
317     }
318 
319     if (isEmptyPath()) {
320       return otherPath;
321     }
322 
323     ImmutableList<Name> otherNames = otherPath.names;
324     int sharedSubsequenceLength = 0;
325     for (int i = 0; i < Math.min(getNameCount(), otherNames.size()); i++) {
326       if (names.get(i).equals(otherNames.get(i))) {
327         sharedSubsequenceLength++;
328       } else {
329         break;
330       }
331     }
332 
333     int extraNamesInThis = Math.max(0, getNameCount() - sharedSubsequenceLength);
334 
335     ImmutableList<Name> extraNamesInOther =
336         (otherNames.size() <= sharedSubsequenceLength)
337             ? ImmutableList.<Name>of()
338             : otherNames.subList(sharedSubsequenceLength, otherNames.size());
339 
340     List<Name> parts = new ArrayList<>(extraNamesInThis + extraNamesInOther.size());
341 
342     // add .. for each extra name in this path
343     parts.addAll(Collections.nCopies(extraNamesInThis, Name.PARENT));
344     // add each extra name in the other path
345     parts.addAll(extraNamesInOther);
346 
347     return pathService.createRelativePath(parts);
348   }
349 
350   @Override
toAbsolutePath()351   public JimfsPath toAbsolutePath() {
352     return isAbsolute() ? this : getJimfsFileSystem().getWorkingDirectory().resolve(this);
353   }
354 
355   @Override
toRealPath(LinkOption... options)356   public JimfsPath toRealPath(LinkOption... options) throws IOException {
357     return getJimfsFileSystem()
358         .getDefaultView()
359         .toRealPath(this, pathService, Options.getLinkOptions(options));
360   }
361 
362   @Override
register( WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)363   public WatchKey register(
364       WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
365       throws IOException {
366     checkNotNull(modifiers);
367     return register(watcher, events);
368   }
369 
370   @Override
register(WatchService watcher, WatchEvent.Kind<?>... events)371   public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException {
372     checkNotNull(watcher);
373     checkNotNull(events);
374     if (!(watcher instanceof AbstractWatchService)) {
375       throw new IllegalArgumentException(
376           "watcher (" + watcher + ") is not associated with this file system");
377     }
378 
379     AbstractWatchService service = (AbstractWatchService) watcher;
380     return service.register(this, Arrays.asList(events));
381   }
382 
383   @Override
toUri()384   public URI toUri() {
385     return getJimfsFileSystem().toUri(this);
386   }
387 
388   @Override
toFile()389   public File toFile() {
390     // documented as unsupported for anything but the default file system
391     throw new UnsupportedOperationException();
392   }
393 
394   @Override
iterator()395   public Iterator<Path> iterator() {
396     return asList().iterator();
397   }
398 
asList()399   private List<Path> asList() {
400     return new AbstractList<Path>() {
401       @Override
402       public Path get(int index) {
403         return getName(index);
404       }
405 
406       @Override
407       public int size() {
408         return getNameCount();
409       }
410     };
411   }
412 
413   @Override
414   public int compareTo(Path other) {
415     // documented to throw CCE if other is associated with a different FileSystemProvider
416     JimfsPath otherPath = (JimfsPath) other;
417     return ComparisonChain.start()
418         .compare(getJimfsFileSystem().getUri(), ((JimfsPath) other).getJimfsFileSystem().getUri())
419         .compare(this, otherPath, pathService)
420         .result();
421   }
422 
423   @Override
424   public boolean equals(@NullableDecl Object obj) {
425     return obj instanceof JimfsPath && compareTo((JimfsPath) obj) == 0;
426   }
427 
428   @Override
429   public int hashCode() {
430     return pathService.hash(this);
431   }
432 
433   @Override
434   public String toString() {
435     return pathService.toString(this);
436   }
437 
438   @NullableDecl
439   private JimfsPath checkPath(Path other) {
440     if (checkNotNull(other) instanceof JimfsPath && other.getFileSystem().equals(getFileSystem())) {
441       return (JimfsPath) other;
442     }
443     return null;
444   }
445 }
446