• 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     return resolve(pathService.createFileName(name));
255   }
256 
257   @Override
resolve(Path other)258   public JimfsPath resolve(Path other) {
259     JimfsPath otherPath = checkPath(other);
260     if (otherPath == null) {
261       throw new ProviderMismatchException(other.toString());
262     }
263 
264     if (isEmptyPath() || otherPath.isAbsolute()) {
265       return otherPath;
266     }
267     if (otherPath.isEmptyPath()) {
268       return this;
269     }
270     return pathService.createPath(
271         root, ImmutableList.<Name>builder().addAll(names).addAll(otherPath.names).build());
272   }
273 
274   @Override
resolve(String other)275   public JimfsPath resolve(String other) {
276     return resolve(pathService.parsePath(other));
277   }
278 
279   @Override
resolveSibling(Path other)280   public JimfsPath resolveSibling(Path other) {
281     JimfsPath otherPath = checkPath(other);
282     if (otherPath == null) {
283       throw new ProviderMismatchException(other.toString());
284     }
285 
286     if (otherPath.isAbsolute()) {
287       return otherPath;
288     }
289     JimfsPath parent = getParent();
290     if (parent == null) {
291       return otherPath;
292     }
293     return parent.resolve(other);
294   }
295 
296   @Override
resolveSibling(String other)297   public JimfsPath resolveSibling(String other) {
298     return resolveSibling(pathService.parsePath(other));
299   }
300 
301   @Override
relativize(Path other)302   public JimfsPath relativize(Path other) {
303     JimfsPath otherPath = checkPath(other);
304     if (otherPath == null) {
305       throw new ProviderMismatchException(other.toString());
306     }
307 
308     checkArgument(
309         Objects.equals(root, otherPath.root), "Paths have different roots: %s, %s", this, other);
310 
311     if (equals(other)) {
312       return pathService.emptyPath();
313     }
314 
315     if (isEmptyPath()) {
316       return otherPath;
317     }
318 
319     ImmutableList<Name> otherNames = otherPath.names;
320     int sharedSubsequenceLength = 0;
321     for (int i = 0; i < Math.min(getNameCount(), otherNames.size()); i++) {
322       if (names.get(i).equals(otherNames.get(i))) {
323         sharedSubsequenceLength++;
324       } else {
325         break;
326       }
327     }
328 
329     int extraNamesInThis = Math.max(0, getNameCount() - sharedSubsequenceLength);
330 
331     ImmutableList<Name> extraNamesInOther =
332         (otherNames.size() <= sharedSubsequenceLength)
333             ? ImmutableList.<Name>of()
334             : otherNames.subList(sharedSubsequenceLength, otherNames.size());
335 
336     List<Name> parts = new ArrayList<>(extraNamesInThis + extraNamesInOther.size());
337 
338     // add .. for each extra name in this path
339     parts.addAll(Collections.nCopies(extraNamesInThis, Name.PARENT));
340     // add each extra name in the other path
341     parts.addAll(extraNamesInOther);
342 
343     return pathService.createRelativePath(parts);
344   }
345 
346   @Override
toAbsolutePath()347   public JimfsPath toAbsolutePath() {
348     return isAbsolute() ? this : getJimfsFileSystem().getWorkingDirectory().resolve(this);
349   }
350 
351   @Override
toRealPath(LinkOption... options)352   public JimfsPath toRealPath(LinkOption... options) throws IOException {
353     return getJimfsFileSystem()
354         .getDefaultView()
355         .toRealPath(this, pathService, Options.getLinkOptions(options));
356   }
357 
358   @Override
register( WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)359   public WatchKey register(
360       WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
361       throws IOException {
362     checkNotNull(modifiers);
363     return register(watcher, events);
364   }
365 
366   @Override
register(WatchService watcher, WatchEvent.Kind<?>... events)367   public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException {
368     checkNotNull(watcher);
369     checkNotNull(events);
370     if (!(watcher instanceof AbstractWatchService)) {
371       throw new IllegalArgumentException(
372           "watcher (" + watcher + ") is not associated with this file system");
373     }
374 
375     AbstractWatchService service = (AbstractWatchService) watcher;
376     return service.register(this, Arrays.asList(events));
377   }
378 
379   @Override
toUri()380   public URI toUri() {
381     return getJimfsFileSystem().toUri(this);
382   }
383 
384   @Override
toFile()385   public File toFile() {
386     // documented as unsupported for anything but the default file system
387     throw new UnsupportedOperationException();
388   }
389 
390   @Override
iterator()391   public Iterator<Path> iterator() {
392     return asList().iterator();
393   }
394 
asList()395   private List<Path> asList() {
396     return new AbstractList<Path>() {
397       @Override
398       public Path get(int index) {
399         return getName(index);
400       }
401 
402       @Override
403       public int size() {
404         return getNameCount();
405       }
406     };
407   }
408 
409   @Override
410   public int compareTo(Path other) {
411     // documented to throw CCE if other is associated with a different FileSystemProvider
412     JimfsPath otherPath = (JimfsPath) other;
413     return ComparisonChain.start()
414         .compare(getJimfsFileSystem().getUri(), ((JimfsPath) other).getJimfsFileSystem().getUri())
415         .compare(this, otherPath, pathService)
416         .result();
417   }
418 
419   @Override
420   public boolean equals(@NullableDecl Object obj) {
421     return obj instanceof JimfsPath && compareTo((JimfsPath) obj) == 0;
422   }
423 
424   @Override
425   public int hashCode() {
426     return pathService.hash(this);
427   }
428 
429   @Override
430   public String toString() {
431     return pathService.toString(this);
432   }
433 
434   @NullableDecl
435   private JimfsPath checkPath(Path other) {
436     if (checkNotNull(other) instanceof JimfsPath && other.getFileSystem().equals(getFileSystem())) {
437       return (JimfsPath) other;
438     }
439     return null;
440   }
441 }
442