001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.fs;
020
021import java.io.IOException;
022import java.net.URI;
023import java.net.URISyntaxException;
024
025import org.apache.avro.reflect.Stringable;
026import org.apache.commons.lang.StringUtils;
027import org.apache.hadoop.classification.InterfaceAudience;
028import org.apache.hadoop.classification.InterfaceStability;
029import org.apache.hadoop.conf.Configuration;
030
031/** Names a file or directory in a {@link FileSystem}.
032 * Path strings use slash as the directory separator.  A path string is
033 * absolute if it begins with a slash.
034 */
035@Stringable
036@InterfaceAudience.Public
037@InterfaceStability.Stable
038public class Path implements Comparable {
039
040  /** The directory separator, a slash. */
041  public static final String SEPARATOR = "/";
042  public static final char SEPARATOR_CHAR = '/';
043  
044  public static final String CUR_DIR = ".";
045  
046  static final boolean WINDOWS
047    = System.getProperty("os.name").startsWith("Windows");
048
049  private URI uri;                                // a hierarchical uri
050
051  /** Resolve a child path against a parent path. */
052  public Path(String parent, String child) {
053    this(new Path(parent), new Path(child));
054  }
055
056  /** Resolve a child path against a parent path. */
057  public Path(Path parent, String child) {
058    this(parent, new Path(child));
059  }
060
061  /** Resolve a child path against a parent path. */
062  public Path(String parent, Path child) {
063    this(new Path(parent), child);
064  }
065
066  /** Resolve a child path against a parent path. */
067  public Path(Path parent, Path child) {
068    // Add a slash to parent's path so resolution is compatible with URI's
069    URI parentUri = parent.uri;
070    String parentPath = parentUri.getPath();
071    if (!(parentPath.equals("/") || parentPath.equals(""))) {
072      try {
073        parentUri = new URI(parentUri.getScheme(), parentUri.getAuthority(),
074                      parentUri.getPath()+"/", null, parentUri.getFragment());
075      } catch (URISyntaxException e) {
076        throw new IllegalArgumentException(e);
077      }
078    }
079    URI resolved = parentUri.resolve(child.uri);
080    initialize(resolved.getScheme(), resolved.getAuthority(),
081               resolved.getPath(), resolved.getFragment());
082  }
083
084  private void checkPathArg( String path ) {
085    // disallow construction of a Path from an empty string
086    if ( path == null ) {
087      throw new IllegalArgumentException(
088          "Can not create a Path from a null string");
089    }
090    if( path.length() == 0 ) {
091       throw new IllegalArgumentException(
092           "Can not create a Path from an empty string");
093    }   
094  }
095  
096  /** Construct a path from a String.  Path strings are URIs, but with
097   * unescaped elements and some additional normalization. */
098  public Path(String pathString) {
099    checkPathArg( pathString );
100    
101    // We can't use 'new URI(String)' directly, since it assumes things are
102    // escaped, which we don't require of Paths. 
103    
104    // add a slash in front of paths with Windows drive letters
105    if (hasWindowsDrive(pathString, false))
106      pathString = "/"+pathString;
107
108    // parse uri components
109    String scheme = null;
110    String authority = null;
111
112    int start = 0;
113
114    // parse uri scheme, if any
115    int colon = pathString.indexOf(':');
116    int slash = pathString.indexOf('/');
117    if ((colon != -1) &&
118        ((slash == -1) || (colon < slash))) {     // has a scheme
119      scheme = pathString.substring(0, colon);
120      start = colon+1;
121    }
122
123    // parse uri authority, if any
124    if (pathString.startsWith("//", start) &&
125        (pathString.length()-start > 2)) {       // has authority
126      int nextSlash = pathString.indexOf('/', start+2);
127      int authEnd = nextSlash > 0 ? nextSlash : pathString.length();
128      authority = pathString.substring(start+2, authEnd);
129      start = authEnd;
130    }
131
132    // uri path is the rest of the string -- query & fragment not supported
133    String path = pathString.substring(start, pathString.length());
134
135    initialize(scheme, authority, path, null);
136  }
137
138  /**
139   * Construct a path from a URI
140   */
141  public Path(URI aUri) {
142    uri = aUri.normalize();
143  }
144  
145  /** Construct a Path from components. */
146  public Path(String scheme, String authority, String path) {
147    checkPathArg( path );
148    initialize(scheme, authority, path, null);
149  }
150
151  private void initialize(String scheme, String authority, String path,
152      String fragment) {
153    try {
154      this.uri = new URI(scheme, authority, normalizePath(path), null, fragment)
155        .normalize();
156    } catch (URISyntaxException e) {
157      throw new IllegalArgumentException(e);
158    }
159  }
160
161  private String normalizePath(String path) {
162    // remove double slashes & backslashes
163    path = StringUtils.replace(path, "//", "/");
164    if (Path.WINDOWS) {
165      path = StringUtils.replace(path, "\\", "/");
166    }
167    
168    // trim trailing slash from non-root path (ignoring windows drive)
169    int minLength = hasWindowsDrive(path, true) ? 4 : 1;
170    if (path.length() > minLength && path.endsWith("/")) {
171      path = path.substring(0, path.length()-1);
172    }
173    
174    return path;
175  }
176
177  private boolean hasWindowsDrive(String path, boolean slashed) {
178    if (!WINDOWS) return false;
179    int start = slashed ? 1 : 0;
180    return
181      path.length() >= start+2 &&
182      (slashed ? path.charAt(0) == '/' : true) &&
183      path.charAt(start+1) == ':' &&
184      ((path.charAt(start) >= 'A' && path.charAt(start) <= 'Z') ||
185       (path.charAt(start) >= 'a' && path.charAt(start) <= 'z'));
186  }
187
188
189  /** Convert this to a URI. */
190  public URI toUri() { return uri; }
191
192  /** Return the FileSystem that owns this Path. */
193  public FileSystem getFileSystem(Configuration conf) throws IOException {
194    return FileSystem.get(this.toUri(), conf);
195  }
196
197  /**
198   * Is an absolute path (ie a slash relative path part)
199   *  AND  a scheme is null AND  authority is null.
200   */
201  public boolean isAbsoluteAndSchemeAuthorityNull() {
202    return  (isUriPathAbsolute() && 
203        uri.getScheme() == null && uri.getAuthority() == null);
204  }
205  
206  /**
207   *  True if the path component (i.e. directory) of this URI is absolute.
208   */
209  public boolean isUriPathAbsolute() {
210    int start = hasWindowsDrive(uri.getPath(), true) ? 3 : 0;
211    return uri.getPath().startsWith(SEPARATOR, start);
212   }
213  
214  /** True if the path component of this URI is absolute. */
215  /**
216   * There is some ambiguity here. An absolute path is a slash
217   * relative name without a scheme or an authority.
218   * So either this method was incorrectly named or its
219   * implementation is incorrect. This method returns true
220   * even if there is a scheme and authority.
221   */
222  public boolean isAbsolute() {
223     return isUriPathAbsolute();
224  }
225
226  /**
227   * @return true if and only if this path represents the root of a file system
228   */
229  public boolean isRoot() {
230    return getParent() == null;
231  }
232
233  /** Returns the final component of this path.*/
234  public String getName() {
235    String path = uri.getPath();
236    int slash = path.lastIndexOf(SEPARATOR);
237    return path.substring(slash+1);
238  }
239
240  /** Returns the parent of a path or null if at root. */
241  public Path getParent() {
242    String path = uri.getPath();
243    int lastSlash = path.lastIndexOf('/');
244    int start = hasWindowsDrive(path, true) ? 3 : 0;
245    if ((path.length() == start) ||               // empty path
246        (lastSlash == start && path.length() == start+1)) { // at root
247      return null;
248    }
249    String parent;
250    if (lastSlash==-1) {
251      parent = CUR_DIR;
252    } else {
253      int end = hasWindowsDrive(path, true) ? 3 : 0;
254      parent = path.substring(0, lastSlash==end?end+1:lastSlash);
255    }
256    return new Path(uri.getScheme(), uri.getAuthority(), parent);
257  }
258
259  /** Adds a suffix to the final name in the path.*/
260  public Path suffix(String suffix) {
261    return new Path(getParent(), getName()+suffix);
262  }
263
264  @Override
265  public String toString() {
266    // we can't use uri.toString(), which escapes everything, because we want
267    // illegal characters unescaped in the string, for glob processing, etc.
268    StringBuilder buffer = new StringBuilder();
269    if (uri.getScheme() != null) {
270      buffer.append(uri.getScheme());
271      buffer.append(":");
272    }
273    if (uri.getAuthority() != null) {
274      buffer.append("//");
275      buffer.append(uri.getAuthority());
276    }
277    if (uri.getPath() != null) {
278      String path = uri.getPath();
279      if (path.indexOf('/')==0 &&
280          hasWindowsDrive(path, true) &&          // has windows drive
281          uri.getScheme() == null &&              // but no scheme
282          uri.getAuthority() == null)             // or authority
283        path = path.substring(1);                 // remove slash before drive
284      buffer.append(path);
285    }
286    if (uri.getFragment() != null) {
287      buffer.append("#");
288      buffer.append(uri.getFragment());
289    }
290    return buffer.toString();
291  }
292
293  @Override
294  public boolean equals(Object o) {
295    if (!(o instanceof Path)) {
296      return false;
297    }
298    Path that = (Path)o;
299    return this.uri.equals(that.uri);
300  }
301
302  @Override
303  public int hashCode() {
304    return uri.hashCode();
305  }
306
307  @Override
308  public int compareTo(Object o) {
309    Path that = (Path)o;
310    return this.uri.compareTo(that.uri);
311  }
312  
313  /** Return the number of elements in this path. */
314  public int depth() {
315    String path = uri.getPath();
316    int depth = 0;
317    int slash = path.length()==1 && path.charAt(0)=='/' ? -1 : 0;
318    while (slash != -1) {
319      depth++;
320      slash = path.indexOf(SEPARATOR, slash+1);
321    }
322    return depth;
323  }
324
325  /**
326   *  Returns a qualified path object.
327   *  
328   *  Deprecated - use {@link #makeQualified(URI, Path)}
329   */
330  @Deprecated
331  public Path makeQualified(FileSystem fs) {
332    return makeQualified(fs.getUri(), fs.getWorkingDirectory());
333  }
334  
335  /** Returns a qualified path object. */
336  @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
337  public Path makeQualified(URI defaultUri, Path workingDir ) {
338    Path path = this;
339    if (!isAbsolute()) {
340      path = new Path(workingDir, this);
341    }
342
343    URI pathUri = path.toUri();
344      
345    String scheme = pathUri.getScheme();
346    String authority = pathUri.getAuthority();
347    String fragment = pathUri.getFragment();
348
349    if (scheme != null &&
350        (authority != null || defaultUri.getAuthority() == null))
351      return path;
352
353    if (scheme == null) {
354      scheme = defaultUri.getScheme();
355    }
356
357    if (authority == null) {
358      authority = defaultUri.getAuthority();
359      if (authority == null) {
360        authority = "";
361      }
362    }
363    
364    URI newUri = null;
365    try {
366      newUri = new URI(scheme, authority , 
367        normalizePath(pathUri.getPath()), null, fragment);
368    } catch (URISyntaxException e) {
369      throw new IllegalArgumentException(e);
370    }
371    return new Path(newUri);
372  }
373}