| /** |
| * 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); |
| } |
| } |