blob: f18a675d1e7917829f9c8b4a8ee0c448095cebe8 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.hadoop.fs;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Pattern;
import org.apache.avro.reflect.Stringable;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.HadoopIllegalArgumentException;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
/**
* Names a file or directory in a {@link FileSystem}.
* Path strings use slash as the directory separator.
*/
@Stringable
@InterfaceAudience.Public
@InterfaceStability.Stable
public class Path implements Comparable {
/**
* The directory separator, a slash.
*/
public static final String SEPARATOR = "/";
/**
* The directory separator, a slash, as a character.
*/
public static final char SEPARATOR_CHAR = '/';
/**
* The current directory, ".".
*/
public static final String CUR_DIR = ".";
/**
* Whether the current host is a Windows machine.
*/
public static final boolean WINDOWS =
System.getProperty("os.name").startsWith("Windows");
/**
* Pre-compiled regular expressions to detect path formats.
*/
private static final Pattern HAS_DRIVE_LETTER_SPECIFIER =
Pattern.compile("^/?[a-zA-Z]:");
private URI uri; // a hierarchical uri
/**
* Test whether this Path uses a scheme and is relative.
* Pathnames with scheme and relative path are illegal.
*/
void checkNotSchemeWithRelative() {
if (toUri().isAbsolute() && !isUriPathAbsolute()) {
throw new HadoopIllegalArgumentException(
"Unsupported name: has scheme but relative path-part");
}
}
void checkNotRelative() {
if (!isAbsolute() && toUri().getScheme() == null) {
throw new HadoopIllegalArgumentException("Path is relative");
}
}
/**
* Return a version of the given Path without the scheme information.
*
* @param path the source Path
* @return a copy of this Path without the scheme information
*/
public static Path getPathWithoutSchemeAndAuthority(Path path) {
// This code depends on Path.toString() to remove the leading slash before
// the drive specification on Windows.
Path newPath = path.isUriPathAbsolute() ?
new Path(null, null, path.toUri().getPath()) :
path;
return newPath;
}
/**
* Create a new Path based on the child path resolved against the parent path.
*
* @param parent the parent path
* @param child the child path
*/
public Path(String parent, String child) {
this(new Path(parent), new Path(child));
}
/**
* Create a new Path based on the child path resolved against the parent path.
*
* @param parent the parent path
* @param child the child path
*/
public Path(Path parent, String child) {
this(parent, new Path(child));
}
/**
* Create a new Path based on the child path resolved against the parent path.
*
* @param parent the parent path
* @param child the child path
*/
public Path(String parent, Path child) {
this(new Path(parent), child);
}
/**
* Create a new Path based on the child path resolved against the parent path.
*
* @param parent the parent path
* @param child the child path
*/
public Path(Path parent, Path child) {
// Add a slash to parent's path so resolution is compatible with URI's
URI parentUri = parent.uri;
String parentPath = parentUri.getPath();
if (!(parentPath.equals("/") || parentPath.isEmpty())) {
try {
parentUri = new URI(parentUri.getScheme(), parentUri.getAuthority(),
parentUri.getPath()+"/", null, parentUri.getFragment());
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
URI resolved = parentUri.resolve(child.uri);
initialize(resolved.getScheme(), resolved.getAuthority(),
resolved.getPath(), resolved.getFragment());
}
private void checkPathArg( String path ) throws IllegalArgumentException {
// disallow construction of a Path from an empty string
if ( path == null ) {
throw new IllegalArgumentException(
"Can not create a Path from a null string");
}
if( path.length() == 0 ) {
throw new IllegalArgumentException(
"Can not create a Path from an empty string");
}
}
/**
* Construct a path from a String. Path strings are URIs, but with
* unescaped elements and some additional normalization.
*
* @param pathString the path string
*/
public Path(String pathString) throws IllegalArgumentException {
checkPathArg( pathString );
// We can't use 'new URI(String)' directly, since it assumes things are
// escaped, which we don't require of Paths.
// add a slash in front of paths with Windows drive letters
if (hasWindowsDrive(pathString) && pathString.charAt(0) != '/') {
pathString = "/" + pathString;
}
// parse uri components
String scheme = null;
String authority = null;
int start = 0;
// parse uri scheme, if any
int colon = pathString.indexOf(':');
int slash = pathString.indexOf('/');
if ((colon != -1) &&
((slash == -1) || (colon < slash))) { // has a scheme
scheme = pathString.substring(0, colon);
start = colon+1;
}
// parse uri authority, if any
if (pathString.startsWith("//", start) &&
(pathString.length()-start > 2)) { // has authority
int nextSlash = pathString.indexOf('/', start+2);
int authEnd = nextSlash > 0 ? nextSlash : pathString.length();
authority = pathString.substring(start+2, authEnd);
start = authEnd;
}
// uri path is the rest of the string -- query & fragment not supported
String path = pathString.substring(start, pathString.length());
initialize(scheme, authority, path, null);
}
/**
* Construct a path from a URI
*
* @param aUri the source URI
*/
public Path(URI aUri) {
uri = aUri.normalize();
}
/**
* Construct a Path from components.
*
* @param scheme the scheme
* @param authority the authority
* @param path the path
*/
public Path(String scheme, String authority, String path) {
checkPathArg( path );
// add a slash in front of paths with Windows drive letters
if (hasWindowsDrive(path) && path.charAt(0) != '/') {
path = "/" + path;
}
// add "./" in front of Linux relative paths so that a path containing
// a colon e.q. "a:b" will not be interpreted as scheme "a".
if (!WINDOWS && path.charAt(0) != '/') {
path = "./" + path;
}
initialize(scheme, authority, path, null);
}
private void initialize(String scheme, String authority, String path,
String fragment) {
try {
this.uri = new URI(scheme, authority, normalizePath(scheme, path), null, fragment)
.normalize();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Merge 2 paths such that the second path is appended relative to the first.
* The returned path has the scheme and authority of the first path. On
* Windows, the drive specification in the second path is discarded.
*
* @param path1 the first path
* @param path2 the second path, to be appended relative to path1
* @return the merged path
*/
public static Path mergePaths(Path path1, Path path2) {
String path2Str = path2.toUri().getPath();
path2Str = path2Str.substring(startPositionWithoutWindowsDrive(path2Str));
// Add path components explicitly, because simply concatenating two path
// string is not safe, for example:
// "/" + "/foo" yields "//foo", which will be parsed as authority in Path
return new Path(path1.toUri().getScheme(),
path1.toUri().getAuthority(),
path1.toUri().getPath() + path2Str);
}
/**
* Normalize a path string to use non-duplicated forward slashes as
* the path separator and remove any trailing path separators.
*
* @param scheme the URI scheme. Used to deduce whether we
* should replace backslashes or not
* @param path the scheme-specific part
* @return the normalized path string
*/
private static String normalizePath(String scheme, String path) {
// Remove double forward slashes.
path = StringUtils.replace(path, "//", "/");
// Remove backslashes if this looks like a Windows path. Avoid
// the substitution if it looks like a non-local URI.
if (WINDOWS &&
(hasWindowsDrive(path) ||
(scheme == null) ||
(scheme.isEmpty()) ||
(scheme.equals("file")))) {
path = StringUtils.replace(path, "\\", "/");
}
// trim trailing slash from non-root path (ignoring windows drive)
int minLength = startPositionWithoutWindowsDrive(path) + 1;
if (path.length() > minLength && path.endsWith(SEPARATOR)) {
path = path.substring(0, path.length()-1);
}
return path;
}
private static boolean hasWindowsDrive(String path) {
return (WINDOWS && HAS_DRIVE_LETTER_SPECIFIER.matcher(path).find());
}
private static int startPositionWithoutWindowsDrive(String path) {
if (hasWindowsDrive(path)) {
return path.charAt(0) == SEPARATOR_CHAR ? 3 : 2;
} else {
return 0;
}
}
/**
* Determine whether a given path string represents an absolute path on
* Windows. e.g. "C:/a/b" is an absolute path. "C:a/b" is not.
*
* @param pathString the path string to evaluate
* @param slashed true if the given path is prefixed with "/"
* @return true if the supplied path looks like an absolute path with a Windows
* drive-specifier
*/
public static boolean isWindowsAbsolutePath(final String pathString,
final boolean slashed) {
int start = startPositionWithoutWindowsDrive(pathString);
return start > 0
&& pathString.length() > start
&& ((pathString.charAt(start) == SEPARATOR_CHAR) ||
(pathString.charAt(start) == '\\'));
}
/**
* Convert this Path to a URI.
*
* @return this Path as a URI
*/
public URI toUri() { return uri; }
/**
* Return the FileSystem that owns this Path.
*
* @param conf the configuration to use when resolving the FileSystem
* @return the FileSystem that owns this Path
* @throws java.io.IOException thrown if there's an issue resolving the
* FileSystem
*/
public FileSystem getFileSystem(Configuration conf) throws IOException {
return FileSystem.get(this.toUri(), conf);
}
/**
* Returns true if the path component (i.e. directory) of this URI is
* absolute <strong>and</strong> the scheme is null, <b>and</b> the authority
* is null.
*
* @return whether the path is absolute and the URI has no scheme nor
* authority parts
*/
public boolean isAbsoluteAndSchemeAuthorityNull() {
return (isUriPathAbsolute() &&
uri.getScheme() == null && uri.getAuthority() == null);
}
/**
* Returns true if the path component (i.e. directory) of this URI is
* absolute.
*
* @return whether this URI's path is absolute
*/
public boolean isUriPathAbsolute() {
int start = startPositionWithoutWindowsDrive(uri.getPath());
return uri.getPath().startsWith(SEPARATOR, start);
}
/**
* Returns true if the path component (i.e. directory) of this URI is
* absolute. This method is a wrapper for {@link #isUriPathAbsolute()}.
*
* @return whether this URI's path is absolute
*/
public boolean isAbsolute() {
return isUriPathAbsolute();
}
/**
* Returns true if and only if this path represents the root of a file system.
*
* @return true if and only if this path represents the root of a file system
*/
public boolean isRoot() {
return getParent() == null;
}
/**
* Returns the final component of this path.
*
* @return the final component of this path
*/
public String getName() {
String path = uri.getPath();
int slash = path.lastIndexOf(SEPARATOR);
return path.substring(slash+1);
}
/**
* Returns the parent of a path or null if at root.
* @return the parent of a path or null if at root
*/
public Path getParent() {
String path = uri.getPath();
int lastSlash = path.lastIndexOf('/');
int start = startPositionWithoutWindowsDrive(path);
if ((path.length() == start) || // empty path
(lastSlash == start && path.length() == start+1)) { // at root
return null;
}
String parent;
if (lastSlash==-1) {
parent = CUR_DIR;
} else {
parent = path.substring(0, lastSlash==start?start+1:lastSlash);
}
return new Path(uri.getScheme(), uri.getAuthority(), parent);
}
/**
* Adds a suffix to the final name in the path.
*
* @param suffix the suffix to add
* @return a new path with the suffix added
*/
public Path suffix(String suffix) {
return new Path(getParent(), getName()+suffix);
}
@Override
public String toString() {
// we can't use uri.toString(), which escapes everything, because we want
// illegal characters unescaped in the string, for glob processing, etc.
StringBuilder buffer = new StringBuilder();
if (uri.getScheme() != null) {
buffer.append(uri.getScheme());
buffer.append(":");
}
if (uri.getAuthority() != null) {
buffer.append("//");
buffer.append(uri.getAuthority());
}
if (uri.getPath() != null) {
String path = uri.getPath();
if (path.indexOf('/')==0 &&
hasWindowsDrive(path) && // has windows drive
uri.getScheme() == null && // but no scheme
uri.getAuthority() == null) // or authority
path = path.substring(1); // remove slash before drive
buffer.append(path);
}
if (uri.getFragment() != null) {
buffer.append("#");
buffer.append(uri.getFragment());
}
return buffer.toString();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Path)) {
return false;
}
Path that = (Path)o;
return this.uri.equals(that.uri);
}
@Override
public int hashCode() {
return uri.hashCode();
}
@Override
public int compareTo(Object o) {
Path that = (Path)o;
return this.uri.compareTo(that.uri);
}
/**
* Returns the number of elements in this path.
* @return the number of elements in this path
*/
public int depth() {
String path = uri.getPath();
int depth = 0;
int slash = path.length()==1 && path.charAt(0)=='/' ? -1 : 0;
while (slash != -1) {
depth++;
slash = path.indexOf(SEPARATOR, slash+1);
}
return depth;
}
/**
* Returns a qualified path object for the {@link FileSystem}'s working
* directory.
*
* @param fs the target FileSystem
* @return a qualified path object for the FileSystem's working directory
* @deprecated use {@link #makeQualified(URI, Path)}
*/
@Deprecated
public Path makeQualified(FileSystem fs) {
return makeQualified(fs.getUri(), fs.getWorkingDirectory());
}
/**
* Returns a qualified path object.
*
* @param defaultUri if this path is missing the scheme or authority
* components, borrow them from this URI
* @param workingDir if this path isn't absolute, treat it as relative to this
* working directory
* @return this path if it contains a scheme and authority and is absolute, or
* a new path that includes a path and authority and is fully qualified
*/
@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
public Path makeQualified(URI defaultUri, Path workingDir ) {
Path path = this;
if (!isAbsolute()) {
path = new Path(workingDir, this);
}
URI pathUri = path.toUri();
String scheme = pathUri.getScheme();
String authority = pathUri.getAuthority();
String fragment = pathUri.getFragment();
if (scheme != null &&
(authority != null || defaultUri.getAuthority() == null))
return path;
if (scheme == null) {
scheme = defaultUri.getScheme();
}
if (authority == null) {
authority = defaultUri.getAuthority();
if (authority == null) {
authority = "";
}
}
URI newUri = null;
try {
newUri = new URI(scheme, authority ,
normalizePath(scheme, pathUri.getPath()), null, fragment);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
return new Path(newUri);
}
}