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    
019    package org.apache.hadoop.fs;
020    
021    import java.io.IOException;
022    import java.net.URI;
023    import java.net.URISyntaxException;
024    
025    import org.apache.avro.reflect.Stringable;
026    import org.apache.commons.lang.StringUtils;
027    import org.apache.hadoop.classification.InterfaceAudience;
028    import org.apache.hadoop.classification.InterfaceStability;
029    import 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
038    public 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    }