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.checkNotNull; 20 21 import com.google.common.base.Joiner; 22 import com.google.common.base.Splitter; 23 import com.google.common.collect.ImmutableList; 24 import com.google.common.collect.Iterables; 25 import java.net.URI; 26 import java.net.URISyntaxException; 27 import java.nio.file.InvalidPathException; 28 import java.util.Arrays; 29 import org.checkerframework.checker.nullness.compatqual.NullableDecl; 30 31 /** 32 * An object defining a specific type of path. Knows how to parse strings to a path and how to 33 * render a path as a string as well as what the path separator is and what other separators are 34 * recognized when parsing paths. 35 * 36 * @author Colin Decker 37 */ 38 public abstract class PathType { 39 40 /** 41 * Returns a Unix-style path type. "/" is both the root and the only separator. Any path starting 42 * with "/" is considered absolute. The nul character ('\0') is disallowed in paths. 43 */ unix()44 public static PathType unix() { 45 return UnixPathType.INSTANCE; 46 } 47 48 /** 49 * Returns a Windows-style path type. The canonical separator character is "\". "/" is also 50 * treated as a separator when parsing paths. 51 * 52 * <p>As much as possible, this implementation follows the information provided in <a 53 * href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">this 54 * article</a>. Paths with drive-letter roots (e.g. "C:\") and paths with UNC roots (e.g. 55 * "\\host\share\") are supported. 56 * 57 * <p>Two Windows path features are not currently supported as they are too Windows-specific: 58 * 59 * <ul> 60 * <li>Relative paths containing a drive-letter root, for example "C:" or "C:foo\bar". Such 61 * paths have a root component and optionally have names, but are <i>relative</i> paths, 62 * relative to the working directory of the drive identified by the root. 63 * <li>Absolute paths with no root, for example "\foo\bar". Such paths are absolute paths on the 64 * current drive. 65 * </ul> 66 */ windows()67 public static PathType windows() { 68 return WindowsPathType.INSTANCE; 69 } 70 71 private final boolean allowsMultipleRoots; 72 private final String separator; 73 private final String otherSeparators; 74 private final Joiner joiner; 75 private final Splitter splitter; 76 PathType(boolean allowsMultipleRoots, char separator, char... otherSeparators)77 protected PathType(boolean allowsMultipleRoots, char separator, char... otherSeparators) { 78 this.separator = String.valueOf(separator); 79 this.allowsMultipleRoots = allowsMultipleRoots; 80 this.otherSeparators = String.valueOf(otherSeparators); 81 this.joiner = Joiner.on(separator); 82 this.splitter = createSplitter(separator, otherSeparators); 83 } 84 85 private static final char[] regexReservedChars = "^$.?+*\\[]{}()".toCharArray(); 86 87 static { 88 Arrays.sort(regexReservedChars); 89 } 90 isRegexReserved(char c)91 private static boolean isRegexReserved(char c) { 92 return Arrays.binarySearch(regexReservedChars, c) >= 0; 93 } 94 createSplitter(char separator, char... otherSeparators)95 private static Splitter createSplitter(char separator, char... otherSeparators) { 96 if (otherSeparators.length == 0) { 97 return Splitter.on(separator).omitEmptyStrings(); 98 } 99 100 // TODO(cgdecker): When CharMatcher is out of @Beta, us Splitter.on(CharMatcher) 101 StringBuilder patternBuilder = new StringBuilder(); 102 patternBuilder.append("["); 103 appendToRegex(separator, patternBuilder); 104 for (char other : otherSeparators) { 105 appendToRegex(other, patternBuilder); 106 } 107 patternBuilder.append("]"); 108 return Splitter.onPattern(patternBuilder.toString()).omitEmptyStrings(); 109 } 110 appendToRegex(char separator, StringBuilder patternBuilder)111 private static void appendToRegex(char separator, StringBuilder patternBuilder) { 112 if (isRegexReserved(separator)) { 113 patternBuilder.append("\\"); 114 } 115 patternBuilder.append(separator); 116 } 117 118 /** Returns whether or not this type of path allows multiple root directories. */ allowsMultipleRoots()119 public final boolean allowsMultipleRoots() { 120 return allowsMultipleRoots; 121 } 122 123 /** 124 * Returns the canonical separator for this path type. The returned string always has a length of 125 * one. 126 */ getSeparator()127 public final String getSeparator() { 128 return separator; 129 } 130 131 /** 132 * Returns the other separators that are recognized when parsing a path. If no other separators 133 * are recognized, the empty string is returned. 134 */ getOtherSeparators()135 public final String getOtherSeparators() { 136 return otherSeparators; 137 } 138 139 /** Returns the path joiner for this path type. */ joiner()140 public final Joiner joiner() { 141 return joiner; 142 } 143 144 /** Returns the path splitter for this path type. */ splitter()145 public final Splitter splitter() { 146 return splitter; 147 } 148 149 /** Returns an empty path. */ emptyPath()150 protected final ParseResult emptyPath() { 151 return new ParseResult(null, ImmutableList.of("")); 152 } 153 154 /** 155 * Parses the given strings as a path. 156 * 157 * @throws InvalidPathException if the path isn't valid for this path type 158 */ parsePath(String path)159 public abstract ParseResult parsePath(String path); 160 161 @Override toString()162 public String toString() { 163 return getClass().getSimpleName(); 164 } 165 166 /** Returns the string form of the given path. */ toString(@ullableDecl String root, Iterable<String> names)167 public abstract String toString(@NullableDecl String root, Iterable<String> names); 168 169 /** 170 * Returns the string form of the given path for use in the path part of a URI. The root element 171 * is not nullable as the path must be absolute. The elements of the returned path <i>do not</i> 172 * need to be escaped. The {@code directory} boolean indicates whether the file the URI is for is 173 * known to be a directory. 174 */ toUriPath(String root, Iterable<String> names, boolean directory)175 protected abstract String toUriPath(String root, Iterable<String> names, boolean directory); 176 177 /** 178 * Parses a path from the given URI path. 179 * 180 * @throws InvalidPathException if the given path isn't valid for this path type 181 */ parseUriPath(String uriPath)182 protected abstract ParseResult parseUriPath(String uriPath); 183 184 /** 185 * Creates a URI for the path with the given root and names in the file system with the given URI. 186 */ toUri( URI fileSystemUri, String root, Iterable<String> names, boolean directory)187 public final URI toUri( 188 URI fileSystemUri, String root, Iterable<String> names, boolean directory) { 189 String path = toUriPath(root, names, directory); 190 try { 191 // it should not suck this much to create a new URI that's the same except with a path set =( 192 // need to do it this way for automatic path escaping 193 return new URI( 194 fileSystemUri.getScheme(), 195 fileSystemUri.getUserInfo(), 196 fileSystemUri.getHost(), 197 fileSystemUri.getPort(), 198 path, 199 null, 200 null); 201 } catch (URISyntaxException e) { 202 throw new AssertionError(e); 203 } 204 } 205 206 /** Parses a path from the given URI. */ fromUri(URI uri)207 public final ParseResult fromUri(URI uri) { 208 return parseUriPath(uri.getPath()); 209 } 210 211 /** Simple result of parsing a path. */ 212 public static final class ParseResult { 213 214 @NullableDecl private final String root; 215 private final Iterable<String> names; 216 ParseResult(@ullableDecl String root, Iterable<String> names)217 public ParseResult(@NullableDecl String root, Iterable<String> names) { 218 this.root = root; 219 this.names = checkNotNull(names); 220 } 221 222 /** Returns whether or not this result is an absolute path. */ isAbsolute()223 public boolean isAbsolute() { 224 return root != null; 225 } 226 227 /** Returns whether or not this result represents a root path. */ isRoot()228 public boolean isRoot() { 229 return root != null && Iterables.isEmpty(names); 230 } 231 232 /** Returns the parsed root element, or null if there was no root. */ 233 @NullableDecl root()234 public String root() { 235 return root; 236 } 237 238 /** Returns the parsed name elements. */ names()239 public Iterable<String> names() { 240 return names; 241 } 242 } 243 } 244