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 import static com.google.common.base.Preconditions.checkState; 22 import static com.google.common.jimfs.PathType.ParseResult; 23 import static java.nio.file.LinkOption.NOFOLLOW_LINKS; 24 25 import com.google.common.annotations.VisibleForTesting; 26 import com.google.common.base.Functions; 27 import com.google.common.base.Predicate; 28 import com.google.common.collect.ComparisonChain; 29 import com.google.common.collect.ImmutableList; 30 import com.google.common.collect.ImmutableSet; 31 import com.google.common.collect.Iterables; 32 import com.google.common.collect.Lists; 33 import com.google.common.collect.Ordering; 34 import java.net.URI; 35 import java.nio.file.FileSystem; 36 import java.nio.file.Files; 37 import java.nio.file.PathMatcher; 38 import java.util.ArrayList; 39 import java.util.Comparator; 40 import java.util.List; 41 import org.checkerframework.checker.nullness.compatqual.NullableDecl; 42 43 /** 44 * Service for creating {@link JimfsPath} instances and handling other path-related operations. 45 * 46 * @author Colin Decker 47 */ 48 final class PathService implements Comparator<JimfsPath> { 49 50 private static final Ordering<Name> DISPLAY_ROOT_ORDERING = Name.displayOrdering().nullsLast(); 51 private static final Ordering<Iterable<Name>> DISPLAY_NAMES_ORDERING = 52 Name.displayOrdering().lexicographical(); 53 54 private static final Ordering<Name> CANONICAL_ROOT_ORDERING = 55 Name.canonicalOrdering().nullsLast(); 56 private static final Ordering<Iterable<Name>> CANONICAL_NAMES_ORDERING = 57 Name.canonicalOrdering().lexicographical(); 58 59 private final PathType type; 60 61 private final ImmutableSet<PathNormalization> displayNormalizations; 62 private final ImmutableSet<PathNormalization> canonicalNormalizations; 63 private final boolean equalityUsesCanonicalForm; 64 65 private final Ordering<Name> rootOrdering; 66 private final Ordering<Iterable<Name>> namesOrdering; 67 68 private volatile FileSystem fileSystem; 69 private volatile JimfsPath emptyPath; 70 PathService(Configuration config)71 PathService(Configuration config) { 72 this( 73 config.pathType, 74 config.nameDisplayNormalization, 75 config.nameCanonicalNormalization, 76 config.pathEqualityUsesCanonicalForm); 77 } 78 PathService( PathType type, Iterable<PathNormalization> displayNormalizations, Iterable<PathNormalization> canonicalNormalizations, boolean equalityUsesCanonicalForm)79 PathService( 80 PathType type, 81 Iterable<PathNormalization> displayNormalizations, 82 Iterable<PathNormalization> canonicalNormalizations, 83 boolean equalityUsesCanonicalForm) { 84 this.type = checkNotNull(type); 85 this.displayNormalizations = ImmutableSet.copyOf(displayNormalizations); 86 this.canonicalNormalizations = ImmutableSet.copyOf(canonicalNormalizations); 87 this.equalityUsesCanonicalForm = equalityUsesCanonicalForm; 88 89 this.rootOrdering = equalityUsesCanonicalForm ? CANONICAL_ROOT_ORDERING : DISPLAY_ROOT_ORDERING; 90 this.namesOrdering = 91 equalityUsesCanonicalForm ? CANONICAL_NAMES_ORDERING : DISPLAY_NAMES_ORDERING; 92 } 93 94 /** Sets the file system to use for created paths. */ setFileSystem(FileSystem fileSystem)95 public void setFileSystem(FileSystem fileSystem) { 96 // allowed to not be JimfsFileSystem for testing purposes only 97 checkState(this.fileSystem == null, "may not set fileSystem twice"); 98 this.fileSystem = checkNotNull(fileSystem); 99 } 100 101 /** Returns the file system this service is for. */ getFileSystem()102 public FileSystem getFileSystem() { 103 return fileSystem; 104 } 105 106 /** Returns the default path separator. */ getSeparator()107 public String getSeparator() { 108 return type.getSeparator(); 109 } 110 111 /** Returns an empty path which has a single name, the empty string. */ emptyPath()112 public JimfsPath emptyPath() { 113 JimfsPath result = emptyPath; 114 if (result == null) { 115 // use createPathInternal to avoid recursive call from createPath() 116 result = createPathInternal(null, ImmutableList.of(Name.EMPTY)); 117 emptyPath = result; 118 return result; 119 } 120 return result; 121 } 122 123 /** Returns the {@link Name} form of the given string. */ name(String name)124 public Name name(String name) { 125 switch (name) { 126 case "": 127 return Name.EMPTY; 128 case ".": 129 return Name.SELF; 130 case "..": 131 return Name.PARENT; 132 default: 133 String display = PathNormalization.normalize(name, displayNormalizations); 134 String canonical = PathNormalization.normalize(name, canonicalNormalizations); 135 return Name.create(display, canonical); 136 } 137 } 138 139 /** Returns the {@link Name} forms of the given strings. */ 140 @VisibleForTesting names(Iterable<String> names)141 List<Name> names(Iterable<String> names) { 142 List<Name> result = new ArrayList<>(); 143 for (String name : names) { 144 result.add(name(name)); 145 } 146 return result; 147 } 148 149 /** Returns a root path with the given name. */ createRoot(Name root)150 public JimfsPath createRoot(Name root) { 151 return createPath(checkNotNull(root), ImmutableList.<Name>of()); 152 } 153 154 /** Returns a single filename path with the given name. */ createFileName(Name name)155 public JimfsPath createFileName(Name name) { 156 return createPath(null, ImmutableList.of(name)); 157 } 158 159 /** Returns a relative path with the given names. */ createRelativePath(Iterable<Name> names)160 public JimfsPath createRelativePath(Iterable<Name> names) { 161 return createPath(null, ImmutableList.copyOf(names)); 162 } 163 164 /** Returns a path with the given root (or no root, if null) and the given names. */ createPath(@ullableDecl Name root, Iterable<Name> names)165 public JimfsPath createPath(@NullableDecl Name root, Iterable<Name> names) { 166 ImmutableList<Name> nameList = ImmutableList.copyOf(Iterables.filter(names, NOT_EMPTY)); 167 if (root == null && nameList.isEmpty()) { 168 // ensure the canonical empty path (one empty string name) is used rather than a path with 169 // no root and no names 170 return emptyPath(); 171 } 172 return createPathInternal(root, nameList); 173 } 174 175 /** Returns a path with the given root (or no root, if null) and the given names. */ createPathInternal(@ullableDecl Name root, Iterable<Name> names)176 protected final JimfsPath createPathInternal(@NullableDecl Name root, Iterable<Name> names) { 177 return new JimfsPath(this, root, names); 178 } 179 180 /** Parses the given strings as a path. */ parsePath(String first, String... more)181 public JimfsPath parsePath(String first, String... more) { 182 String joined = type.joiner().join(Iterables.filter(Lists.asList(first, more), NOT_EMPTY)); 183 return toPath(type.parsePath(joined)); 184 } 185 toPath(ParseResult parsed)186 private JimfsPath toPath(ParseResult parsed) { 187 Name root = parsed.root() == null ? null : name(parsed.root()); 188 Iterable<Name> names = names(parsed.names()); 189 return createPath(root, names); 190 } 191 192 /** Returns the string form of the given path. */ toString(JimfsPath path)193 public String toString(JimfsPath path) { 194 Name root = path.root(); 195 String rootString = root == null ? null : root.toString(); 196 Iterable<String> names = Iterables.transform(path.names(), Functions.toStringFunction()); 197 return type.toString(rootString, names); 198 } 199 200 /** Creates a hash code for the given path. */ hash(JimfsPath path)201 public int hash(JimfsPath path) { 202 // Note: JimfsPath.equals() is implemented using the compare() method below; 203 // equalityUsesCanonicalForm is taken into account there via the namesOrdering, which is set 204 // at construction time. 205 int hash = 31; 206 hash = 31 * hash + getFileSystem().hashCode(); 207 208 final Name root = path.root(); 209 final ImmutableList<Name> names = path.names(); 210 211 if (equalityUsesCanonicalForm) { 212 // use hash codes of names themselves, which are based on the canonical form 213 hash = 31 * hash + (root == null ? 0 : root.hashCode()); 214 for (Name name : names) { 215 hash = 31 * hash + name.hashCode(); 216 } 217 } else { 218 // use hash codes from toString() form of names 219 hash = 31 * hash + (root == null ? 0 : root.toString().hashCode()); 220 for (Name name : names) { 221 hash = 31 * hash + name.toString().hashCode(); 222 } 223 } 224 return hash; 225 } 226 227 @Override compare(JimfsPath a, JimfsPath b)228 public int compare(JimfsPath a, JimfsPath b) { 229 return ComparisonChain.start() 230 .compare(a.root(), b.root(), rootOrdering) 231 .compare(a.names(), b.names(), namesOrdering) 232 .result(); 233 } 234 235 /** 236 * Returns the URI for the given path. The given file system URI is the base against which the 237 * path is resolved to create the returned URI. 238 */ toUri(URI fileSystemUri, JimfsPath path)239 public URI toUri(URI fileSystemUri, JimfsPath path) { 240 checkArgument(path.isAbsolute(), "path (%s) must be absolute", path); 241 String root = String.valueOf(path.root()); 242 Iterable<String> names = Iterables.transform(path.names(), Functions.toStringFunction()); 243 return type.toUri(fileSystemUri, root, names, Files.isDirectory(path, NOFOLLOW_LINKS)); 244 } 245 246 /** Converts the path of the given URI into a path for this file system. */ fromUri(URI uri)247 public JimfsPath fromUri(URI uri) { 248 return toPath(type.fromUri(uri)); 249 } 250 251 /** 252 * Returns a {@link PathMatcher} for the given syntax and pattern as specified by {@link 253 * FileSystem#getPathMatcher(String)}. 254 */ createPathMatcher(String syntaxAndPattern)255 public PathMatcher createPathMatcher(String syntaxAndPattern) { 256 return PathMatchers.getPathMatcher( 257 syntaxAndPattern, 258 type.getSeparator() + type.getOtherSeparators(), 259 equalityUsesCanonicalForm ? canonicalNormalizations : displayNormalizations); 260 } 261 262 private static final Predicate<Object> NOT_EMPTY = 263 new Predicate<Object>() { 264 @Override 265 public boolean apply(Object input) { 266 return !input.toString().isEmpty(); 267 } 268 }; 269 } 270