blob: 4e4d30e26ed8c8086e9c05e26c1709f44cf381c6 [file] [log] [blame]
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.common.jimfs;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.InvalidPathException;
import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
/**
* An object defining a specific type of path. Knows how to parse strings to a path and how to
* render a path as a string as well as what the path separator is and what other separators are
* recognized when parsing paths.
*
* @author Colin Decker
*/
public abstract class PathType {
/**
* Returns a Unix-style path type. "/" is both the root and the only separator. Any path starting
* with "/" is considered absolute. The nul character ('\0') is disallowed in paths.
*/
public static PathType unix() {
return UnixPathType.INSTANCE;
}
/**
* Returns a Windows-style path type. The canonical separator character is "\". "/" is also
* treated as a separator when parsing paths.
*
* <p>As much as possible, this implementation follows the information provided in <a
* href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">this
* article</a>. Paths with drive-letter roots (e.g. "C:\") and paths with UNC roots (e.g.
* "\\host\share\") are supported.
*
* <p>Two Windows path features are not currently supported as they are too Windows-specific:
*
* <ul>
* <li>Relative paths containing a drive-letter root, for example "C:" or "C:foo\bar". Such
* paths have a root component and optionally have names, but are <i>relative</i> paths,
* relative to the working directory of the drive identified by the root.
* <li>Absolute paths with no root, for example "\foo\bar". Such paths are absolute paths on the
* current drive.
* </ul>
*/
public static PathType windows() {
return WindowsPathType.INSTANCE;
}
private final boolean allowsMultipleRoots;
private final String separator;
private final String otherSeparators;
private final Joiner joiner;
private final Splitter splitter;
protected PathType(boolean allowsMultipleRoots, char separator, char... otherSeparators) {
this.separator = String.valueOf(separator);
this.allowsMultipleRoots = allowsMultipleRoots;
this.otherSeparators = String.valueOf(otherSeparators);
this.joiner = Joiner.on(separator);
this.splitter = createSplitter(separator, otherSeparators);
}
private static final char[] regexReservedChars = "^$.?+*\\[]{}()".toCharArray();
static {
Arrays.sort(regexReservedChars);
}
private static boolean isRegexReserved(char c) {
return Arrays.binarySearch(regexReservedChars, c) >= 0;
}
private static Splitter createSplitter(char separator, char... otherSeparators) {
if (otherSeparators.length == 0) {
return Splitter.on(separator).omitEmptyStrings();
}
// TODO(cgdecker): When CharMatcher is out of @Beta, us Splitter.on(CharMatcher)
StringBuilder patternBuilder = new StringBuilder();
patternBuilder.append("[");
appendToRegex(separator, patternBuilder);
for (char other : otherSeparators) {
appendToRegex(other, patternBuilder);
}
patternBuilder.append("]");
return Splitter.onPattern(patternBuilder.toString()).omitEmptyStrings();
}
private static void appendToRegex(char separator, StringBuilder patternBuilder) {
if (isRegexReserved(separator)) {
patternBuilder.append("\\");
}
patternBuilder.append(separator);
}
/** Returns whether or not this type of path allows multiple root directories. */
public final boolean allowsMultipleRoots() {
return allowsMultipleRoots;
}
/**
* Returns the canonical separator for this path type. The returned string always has a length of
* one.
*/
public final String getSeparator() {
return separator;
}
/**
* Returns the other separators that are recognized when parsing a path. If no other separators
* are recognized, the empty string is returned.
*/
public final String getOtherSeparators() {
return otherSeparators;
}
/** Returns the path joiner for this path type. */
public final Joiner joiner() {
return joiner;
}
/** Returns the path splitter for this path type. */
public final Splitter splitter() {
return splitter;
}
/** Returns an empty path. */
protected final ParseResult emptyPath() {
return new ParseResult(null, ImmutableList.of(""));
}
/**
* Parses the given strings as a path.
*
* @throws InvalidPathException if the path isn't valid for this path type
*/
public abstract ParseResult parsePath(String path);
@Override
public String toString() {
return getClass().getSimpleName();
}
/** Returns the string form of the given path. */
public abstract String toString(@NullableDecl String root, Iterable<String> names);
/**
* Returns the string form of the given path for use in the path part of a URI. The root element
* is not nullable as the path must be absolute. The elements of the returned path <i>do not</i>
* need to be escaped. The {@code directory} boolean indicates whether the file the URI is for is
* known to be a directory.
*/
protected abstract String toUriPath(String root, Iterable<String> names, boolean directory);
/**
* Parses a path from the given URI path.
*
* @throws InvalidPathException if the given path isn't valid for this path type
*/
protected abstract ParseResult parseUriPath(String uriPath);
/**
* Creates a URI for the path with the given root and names in the file system with the given URI.
*/
public final URI toUri(
URI fileSystemUri, String root, Iterable<String> names, boolean directory) {
String path = toUriPath(root, names, directory);
try {
// it should not suck this much to create a new URI that's the same except with a path set =(
// need to do it this way for automatic path escaping
return new URI(
fileSystemUri.getScheme(),
fileSystemUri.getUserInfo(),
fileSystemUri.getHost(),
fileSystemUri.getPort(),
path,
null,
null);
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
/** Parses a path from the given URI. */
public final ParseResult fromUri(URI uri) {
return parseUriPath(uri.getPath());
}
/** Simple result of parsing a path. */
public static final class ParseResult {
@NullableDecl private final String root;
private final Iterable<String> names;
public ParseResult(@NullableDecl String root, Iterable<String> names) {
this.root = root;
this.names = checkNotNull(names);
}
/** Returns whether or not this result is an absolute path. */
public boolean isAbsolute() {
return root != null;
}
/** Returns whether or not this result represents a root path. */
public boolean isRoot() {
return root != null && Iterables.isEmpty(names);
}
/** Returns the parsed root element, or null if there was no root. */
@NullableDecl
public String root() {
return root;
}
/** Returns the parsed name elements. */
public Iterable<String> names() {
return names;
}
}
}