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