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
020package org.apache.hadoop.fs;
021
022import com.google.common.annotations.VisibleForTesting;
023
024import java.io.BufferedOutputStream;
025import java.io.DataOutput;
026import java.io.EOFException;
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.FileNotFoundException;
030import java.io.FileOutputStream;
031import java.io.IOException;
032import java.io.OutputStream;
033import java.io.FileDescriptor;
034import java.net.URI;
035import java.nio.ByteBuffer;
036import java.nio.file.AccessDeniedException;
037import java.util.Arrays;
038import java.util.EnumSet;
039import java.util.StringTokenizer;
040
041import org.apache.hadoop.classification.InterfaceAudience;
042import org.apache.hadoop.classification.InterfaceStability;
043import org.apache.hadoop.conf.Configuration;
044import org.apache.hadoop.fs.permission.FsPermission;
045import org.apache.hadoop.io.IOUtils;
046import org.apache.hadoop.io.nativeio.NativeIO;
047import org.apache.hadoop.util.Progressable;
048import org.apache.hadoop.util.Shell;
049import org.apache.hadoop.util.StringUtils;
050
051/****************************************************************
052 * Implement the FileSystem API for the raw local filesystem.
053 *
054 *****************************************************************/
055@InterfaceAudience.Public
056@InterfaceStability.Stable
057public class RawLocalFileSystem extends FileSystem {
058  static final URI NAME = URI.create("file:///");
059  private Path workingDir;
060  // Temporary workaround for HADOOP-9652.
061  private static boolean useDeprecatedFileStatus = true;
062
063  @VisibleForTesting
064  public static void useStatIfAvailable() {
065    useDeprecatedFileStatus = !Stat.isAvailable();
066  }
067  
068  public RawLocalFileSystem() {
069    workingDir = getInitialWorkingDirectory();
070  }
071  
072  private Path makeAbsolute(Path f) {
073    if (f.isAbsolute()) {
074      return f;
075    } else {
076      return new Path(workingDir, f);
077    }
078  }
079  
080  /** Convert a path to a File. */
081  public File pathToFile(Path path) {
082    checkPath(path);
083    if (!path.isAbsolute()) {
084      path = new Path(getWorkingDirectory(), path);
085    }
086    return new File(path.toUri().getPath());
087  }
088
089  @Override
090  public URI getUri() { return NAME; }
091  
092  @Override
093  public void initialize(URI uri, Configuration conf) throws IOException {
094    super.initialize(uri, conf);
095    setConf(conf);
096  }
097  
098  /*******************************************************
099   * For open()'s FSInputStream.
100   *******************************************************/
101  class LocalFSFileInputStream extends FSInputStream implements HasFileDescriptor {
102    private FileInputStream fis;
103    private long position;
104
105    public LocalFSFileInputStream(Path f) throws IOException {
106      fis = new FileInputStream(pathToFile(f));
107    }
108    
109    @Override
110    public void seek(long pos) throws IOException {
111      if (pos < 0) {
112        throw new EOFException(
113          FSExceptionMessages.NEGATIVE_SEEK);
114      }
115      fis.getChannel().position(pos);
116      this.position = pos;
117    }
118    
119    @Override
120    public long getPos() throws IOException {
121      return this.position;
122    }
123    
124    @Override
125    public boolean seekToNewSource(long targetPos) throws IOException {
126      return false;
127    }
128    
129    /*
130     * Just forward to the fis
131     */
132    @Override
133    public int available() throws IOException { return fis.available(); }
134    @Override
135    public void close() throws IOException { fis.close(); }
136    @Override
137    public boolean markSupported() { return false; }
138    
139    @Override
140    public int read() throws IOException {
141      try {
142        int value = fis.read();
143        if (value >= 0) {
144          this.position++;
145          statistics.incrementBytesRead(1);
146        }
147        return value;
148      } catch (IOException e) {                 // unexpected exception
149        throw new FSError(e);                   // assume native fs error
150      }
151    }
152    
153    @Override
154    public int read(byte[] b, int off, int len) throws IOException {
155      // parameter check
156      validatePositionedReadArgs(position, b, off, len);
157      try {
158        int value = fis.read(b, off, len);
159        if (value > 0) {
160          this.position += value;
161          statistics.incrementBytesRead(value);
162        }
163        return value;
164      } catch (IOException e) {                 // unexpected exception
165        throw new FSError(e);                   // assume native fs error
166      }
167    }
168    
169    @Override
170    public int read(long position, byte[] b, int off, int len)
171      throws IOException {
172      // parameter check
173      validatePositionedReadArgs(position, b, off, len);
174      if (len == 0) {
175        return 0;
176      }
177
178      ByteBuffer bb = ByteBuffer.wrap(b, off, len);
179      try {
180        int value = fis.getChannel().read(bb, position);
181        if (value > 0) {
182          statistics.incrementBytesRead(value);
183        }
184        return value;
185      } catch (IOException e) {
186        throw new FSError(e);
187      }
188    }
189    
190    @Override
191    public long skip(long n) throws IOException {
192      long value = fis.skip(n);
193      if (value > 0) {
194        this.position += value;
195      }
196      return value;
197    }
198
199    @Override
200    public FileDescriptor getFileDescriptor() throws IOException {
201      return fis.getFD();
202    }
203  }
204  
205  @Override
206  public FSDataInputStream open(Path f, int bufferSize) throws IOException {
207    if (!exists(f)) {
208      throw new FileNotFoundException(f.toString());
209    }
210    return new FSDataInputStream(new BufferedFSInputStream(
211        new LocalFSFileInputStream(f), bufferSize));
212  }
213  
214  /*********************************************************
215   * For create()'s FSOutputStream.
216   *********************************************************/
217  class LocalFSFileOutputStream extends OutputStream {
218    private FileOutputStream fos;
219    
220    private LocalFSFileOutputStream(Path f, boolean append,
221        FsPermission permission) throws IOException {
222      File file = pathToFile(f);
223      if (permission == null) {
224        this.fos = new FileOutputStream(file, append);
225      } else {
226        if (Shell.WINDOWS && NativeIO.isAvailable()) {
227          this.fos = NativeIO.Windows.createFileOutputStreamWithMode(file,
228              append, permission.toShort());
229        } else {
230          this.fos = new FileOutputStream(file, append);
231          boolean success = false;
232          try {
233            setPermission(f, permission);
234            success = true;
235          } finally {
236            if (!success) {
237              IOUtils.cleanup(LOG, this.fos);
238            }
239          }
240        }
241      }
242    }
243    
244    /*
245     * Just forward to the fos
246     */
247    @Override
248    public void close() throws IOException { fos.close(); }
249    @Override
250    public void flush() throws IOException { fos.flush(); }
251    @Override
252    public void write(byte[] b, int off, int len) throws IOException {
253      try {
254        fos.write(b, off, len);
255      } catch (IOException e) {                // unexpected exception
256        throw new FSError(e);                  // assume native fs error
257      }
258    }
259    
260    @Override
261    public void write(int b) throws IOException {
262      try {
263        fos.write(b);
264      } catch (IOException e) {              // unexpected exception
265        throw new FSError(e);                // assume native fs error
266      }
267    }
268  }
269
270  @Override
271  public FSDataOutputStream append(Path f, int bufferSize,
272      Progressable progress) throws IOException {
273    if (!exists(f)) {
274      throw new FileNotFoundException("File " + f + " not found");
275    }
276    FileStatus status = getFileStatus(f);
277    if (status.isDirectory()) {
278      throw new IOException("Cannot append to a diretory (=" + f + " )");
279    }
280    return new FSDataOutputStream(new BufferedOutputStream(
281        createOutputStreamWithMode(f, true, null), bufferSize), statistics,
282        status.getLen());
283  }
284
285  @Override
286  public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize,
287    short replication, long blockSize, Progressable progress)
288    throws IOException {
289    return create(f, overwrite, true, bufferSize, replication, blockSize,
290        progress, null);
291  }
292
293  private FSDataOutputStream create(Path f, boolean overwrite,
294      boolean createParent, int bufferSize, short replication, long blockSize,
295      Progressable progress, FsPermission permission) throws IOException {
296    if (exists(f) && !overwrite) {
297      throw new FileAlreadyExistsException("File already exists: " + f);
298    }
299    Path parent = f.getParent();
300    if (parent != null && !mkdirs(parent)) {
301      throw new IOException("Mkdirs failed to create " + parent.toString());
302    }
303    return new FSDataOutputStream(new BufferedOutputStream(
304        createOutputStreamWithMode(f, false, permission), bufferSize),
305        statistics);
306  }
307  
308  protected OutputStream createOutputStream(Path f, boolean append) 
309      throws IOException {
310    return createOutputStreamWithMode(f, append, null);
311  }
312
313  protected OutputStream createOutputStreamWithMode(Path f, boolean append,
314      FsPermission permission) throws IOException {
315    return new LocalFSFileOutputStream(f, append, permission);
316  }
317  
318  @Override
319  @Deprecated
320  public FSDataOutputStream createNonRecursive(Path f, FsPermission permission,
321      EnumSet<CreateFlag> flags, int bufferSize, short replication, long blockSize,
322      Progressable progress) throws IOException {
323    if (exists(f) && !flags.contains(CreateFlag.OVERWRITE)) {
324      throw new FileAlreadyExistsException("File already exists: " + f);
325    }
326    return new FSDataOutputStream(new BufferedOutputStream(
327        createOutputStreamWithMode(f, false, permission), bufferSize),
328            statistics);
329  }
330
331  @Override
332  public FSDataOutputStream create(Path f, FsPermission permission,
333    boolean overwrite, int bufferSize, short replication, long blockSize,
334    Progressable progress) throws IOException {
335
336    FSDataOutputStream out = create(f, overwrite, true, bufferSize, replication,
337        blockSize, progress, permission);
338    return out;
339  }
340
341  @Override
342  public FSDataOutputStream createNonRecursive(Path f, FsPermission permission,
343      boolean overwrite,
344      int bufferSize, short replication, long blockSize,
345      Progressable progress) throws IOException {
346    FSDataOutputStream out = create(f, overwrite, false, bufferSize, replication,
347        blockSize, progress, permission);
348    return out;
349  }
350
351  @Override
352  public boolean rename(Path src, Path dst) throws IOException {
353    // Attempt rename using Java API.
354    File srcFile = pathToFile(src);
355    File dstFile = pathToFile(dst);
356    if (srcFile.renameTo(dstFile)) {
357      return true;
358    }
359
360    // Enforce POSIX rename behavior that a source directory replaces an existing
361    // destination if the destination is an empty directory.  On most platforms,
362    // this is already handled by the Java API call above.  Some platforms
363    // (notably Windows) do not provide this behavior, so the Java API call above
364    // fails.  Delete destination and attempt rename again.
365    if (this.exists(dst)) {
366      FileStatus sdst = this.getFileStatus(dst);
367      if (sdst.isDirectory() && dstFile.list().length == 0) {
368        if (LOG.isDebugEnabled()) {
369          LOG.debug("Deleting empty destination and renaming " + src + " to " +
370            dst);
371        }
372        if (this.delete(dst, false) && srcFile.renameTo(dstFile)) {
373          return true;
374        }
375      }
376    }
377
378    // The fallback behavior accomplishes the rename by a full copy.
379    if (LOG.isDebugEnabled()) {
380      LOG.debug("Falling through to a copy of " + src + " to " + dst);
381    }
382    return FileUtil.copy(this, src, this, dst, true, getConf());
383  }
384  
385  /**
386   * Delete the given path to a file or directory.
387   * @param p the path to delete
388   * @param recursive to delete sub-directories
389   * @return true if the file or directory and all its contents were deleted
390   * @throws IOException if p is non-empty and recursive is false 
391   */
392  @Override
393  public boolean delete(Path p, boolean recursive) throws IOException {
394    File f = pathToFile(p);
395    if (!f.exists()) {
396      //no path, return false "nothing to delete"
397      return false;
398    }
399    if (f.isFile()) {
400      return f.delete();
401    } else if (!recursive && f.isDirectory() && 
402        (FileUtil.listFiles(f).length != 0)) {
403      throw new IOException("Directory " + f.toString() + " is not empty");
404    }
405    return FileUtil.fullyDelete(f);
406  }
407 
408  @Override
409  public FileStatus[] listStatus(Path f) throws IOException {
410    File localf = pathToFile(f);
411    FileStatus[] results;
412
413    if (!localf.exists()) {
414      throw new FileNotFoundException("File " + f + " does not exist");
415    }
416
417    if (localf.isDirectory()) {
418      String[] names = localf.list();
419      if (names == null) {
420        if (!localf.canRead()) {
421          throw new AccessDeniedException("cannot open directory " + f +
422              ": Permission denied");
423        }
424        return null;
425      }
426      results = new FileStatus[names.length];
427      int j = 0;
428      for (int i = 0; i < names.length; i++) {
429        try {
430          // Assemble the path using the Path 3 arg constructor to make sure
431          // paths with colon are properly resolved on Linux
432          results[j] = getFileStatus(new Path(f, new Path(null, null,
433                                                          names[i])));
434          j++;
435        } catch (FileNotFoundException e) {
436          // ignore the files not found since the dir list may have have
437          // changed since the names[] list was generated.
438        }
439      }
440      if (j == names.length) {
441        return results;
442      }
443      return Arrays.copyOf(results, j);
444    }
445
446    if (!useDeprecatedFileStatus) {
447      return new FileStatus[] { getFileStatus(f) };
448    }
449    return new FileStatus[] {
450        new DeprecatedRawLocalFileStatus(localf,
451        getDefaultBlockSize(f), this) };
452  }
453  
454  protected boolean mkOneDir(File p2f) throws IOException {
455    return mkOneDirWithMode(new Path(p2f.getAbsolutePath()), p2f, null);
456  }
457
458  protected boolean mkOneDirWithMode(Path p, File p2f, FsPermission permission)
459      throws IOException {
460    if (permission == null) {
461      return p2f.mkdir();
462    } else {
463      if (Shell.WINDOWS && NativeIO.isAvailable()) {
464        try {
465          NativeIO.Windows.createDirectoryWithMode(p2f, permission.toShort());
466          return true;
467        } catch (IOException e) {
468          if (LOG.isDebugEnabled()) {
469            LOG.debug(String.format(
470                "NativeIO.createDirectoryWithMode error, path = %s, mode = %o",
471                p2f, permission.toShort()), e);
472          }
473          return false;
474        }
475      } else {
476        boolean b = p2f.mkdir();
477        if (b) {
478          setPermission(p, permission);
479        }
480        return b;
481      }
482    }
483  }
484
485  /**
486   * Creates the specified directory hierarchy. Does not
487   * treat existence as an error.
488   */
489  @Override
490  public boolean mkdirs(Path f) throws IOException {
491    return mkdirsWithOptionalPermission(f, null);
492  }
493
494  @Override
495  public boolean mkdirs(Path f, FsPermission permission) throws IOException {
496    return mkdirsWithOptionalPermission(f, permission);
497  }
498
499  private boolean mkdirsWithOptionalPermission(Path f, FsPermission permission)
500      throws IOException {
501    if(f == null) {
502      throw new IllegalArgumentException("mkdirs path arg is null");
503    }
504    Path parent = f.getParent();
505    File p2f = pathToFile(f);
506    File parent2f = null;
507    if(parent != null) {
508      parent2f = pathToFile(parent);
509      if(parent2f != null && parent2f.exists() && !parent2f.isDirectory()) {
510        throw new ParentNotDirectoryException("Parent path is not a directory: "
511            + parent);
512      }
513    }
514    if (p2f.exists() && !p2f.isDirectory()) {
515      throw new FileNotFoundException("Destination exists" +
516              " and is not a directory: " + p2f.getCanonicalPath());
517    }
518    return (parent == null || parent2f.exists() || mkdirs(parent)) &&
519      (mkOneDirWithMode(f, p2f, permission) || p2f.isDirectory());
520  }
521  
522  
523  @Override
524  public Path getHomeDirectory() {
525    return this.makeQualified(new Path(System.getProperty("user.home")));
526  }
527
528  /**
529   * Set the working directory to the given directory.
530   */
531  @Override
532  public void setWorkingDirectory(Path newDir) {
533    workingDir = makeAbsolute(newDir);
534    checkPath(workingDir);
535  }
536  
537  @Override
538  public Path getWorkingDirectory() {
539    return workingDir;
540  }
541  
542  @Override
543  protected Path getInitialWorkingDirectory() {
544    return this.makeQualified(new Path(System.getProperty("user.dir")));
545  }
546
547  @Override
548  public FsStatus getStatus(Path p) throws IOException {
549    File partition = pathToFile(p == null ? new Path("/") : p);
550    //File provides getUsableSpace() and getFreeSpace()
551    //File provides no API to obtain used space, assume used = total - free
552    return new FsStatus(partition.getTotalSpace(), 
553      partition.getTotalSpace() - partition.getFreeSpace(),
554      partition.getFreeSpace());
555  }
556  
557  // In the case of the local filesystem, we can just rename the file.
558  @Override
559  public void moveFromLocalFile(Path src, Path dst) throws IOException {
560    rename(src, dst);
561  }
562  
563  // We can write output directly to the final location
564  @Override
565  public Path startLocalOutput(Path fsOutputFile, Path tmpLocalFile)
566    throws IOException {
567    return fsOutputFile;
568  }
569  
570  // It's in the right place - nothing to do.
571  @Override
572  public void completeLocalOutput(Path fsWorkingFile, Path tmpLocalFile)
573    throws IOException {
574  }
575  
576  @Override
577  public void close() throws IOException {
578    super.close();
579  }
580  
581  @Override
582  public String toString() {
583    return "LocalFS";
584  }
585  
586  @Override
587  public FileStatus getFileStatus(Path f) throws IOException {
588    return getFileLinkStatusInternal(f, true);
589  }
590
591  @Deprecated
592  private FileStatus deprecatedGetFileStatus(Path f) throws IOException {
593    File path = pathToFile(f);
594    if (path.exists()) {
595      return new DeprecatedRawLocalFileStatus(pathToFile(f),
596          getDefaultBlockSize(f), this);
597    } else {
598      throw new FileNotFoundException("File " + f + " does not exist");
599    }
600  }
601
602  @Deprecated
603  static class DeprecatedRawLocalFileStatus extends FileStatus {
604    /* We can add extra fields here. It breaks at least CopyFiles.FilePair().
605     * We recognize if the information is already loaded by check if
606     * onwer.equals("").
607     */
608    private boolean isPermissionLoaded() {
609      return !super.getOwner().isEmpty(); 
610    }
611    
612    DeprecatedRawLocalFileStatus(File f, long defaultBlockSize, FileSystem fs) {
613      super(f.length(), f.isDirectory(), 1, defaultBlockSize,
614          f.lastModified(), new Path(f.getPath()).makeQualified(fs.getUri(),
615            fs.getWorkingDirectory()));
616    }
617    
618    @Override
619    public FsPermission getPermission() {
620      if (!isPermissionLoaded()) {
621        loadPermissionInfo();
622      }
623      return super.getPermission();
624    }
625
626    @Override
627    public String getOwner() {
628      if (!isPermissionLoaded()) {
629        loadPermissionInfo();
630      }
631      return super.getOwner();
632    }
633
634    @Override
635    public String getGroup() {
636      if (!isPermissionLoaded()) {
637        loadPermissionInfo();
638      }
639      return super.getGroup();
640    }
641
642    /// loads permissions, owner, and group from `ls -ld`
643    private void loadPermissionInfo() {
644      IOException e = null;
645      try {
646        String output = FileUtil.execCommand(new File(getPath().toUri()), 
647            Shell.getGetPermissionCommand());
648        StringTokenizer t =
649            new StringTokenizer(output, Shell.TOKEN_SEPARATOR_REGEX);
650        //expected format
651        //-rw-------    1 username groupname ...
652        String permission = t.nextToken();
653        if (permission.length() > FsPermission.MAX_PERMISSION_LENGTH) {
654          //files with ACLs might have a '+'
655          permission = permission.substring(0,
656            FsPermission.MAX_PERMISSION_LENGTH);
657        }
658        setPermission(FsPermission.valueOf(permission));
659        t.nextToken();
660
661        String owner = t.nextToken();
662        // If on windows domain, token format is DOMAIN\\user and we want to
663        // extract only the user name
664        if (Shell.WINDOWS) {
665          int i = owner.indexOf('\\');
666          if (i != -1)
667            owner = owner.substring(i + 1);
668        }
669        setOwner(owner);
670
671        setGroup(t.nextToken());
672      } catch (Shell.ExitCodeException ioe) {
673        if (ioe.getExitCode() != 1) {
674          e = ioe;
675        } else {
676          setPermission(null);
677          setOwner(null);
678          setGroup(null);
679        }
680      } catch (IOException ioe) {
681        e = ioe;
682      } finally {
683        if (e != null) {
684          throw new RuntimeException("Error while running command to get " +
685                                     "file permissions : " + 
686                                     StringUtils.stringifyException(e));
687        }
688      }
689    }
690
691    @Override
692    public void write(DataOutput out) throws IOException {
693      if (!isPermissionLoaded()) {
694        loadPermissionInfo();
695      }
696      super.write(out);
697    }
698  }
699
700  /**
701   * Use the command chown to set owner.
702   */
703  @Override
704  public void setOwner(Path p, String username, String groupname)
705    throws IOException {
706    FileUtil.setOwner(pathToFile(p), username, groupname);
707  }
708
709  /**
710   * Use the command chmod to set permission.
711   */
712  @Override
713  public void setPermission(Path p, FsPermission permission)
714    throws IOException {
715    if (NativeIO.isAvailable()) {
716      NativeIO.POSIX.chmod(pathToFile(p).getCanonicalPath(),
717                     permission.toShort());
718    } else {
719      String perm = String.format("%04o", permission.toShort());
720      Shell.execCommand(Shell.getSetPermissionCommand(perm, false,
721        FileUtil.makeShellPath(pathToFile(p), true)));
722    }
723  }
724 
725  /**
726   * Sets the {@link Path}'s last modified time <em>only</em> to the given
727   * valid time.
728   *
729   * @param mtime the modification time to set (only if greater than zero).
730   * @param atime currently ignored.
731   * @throws IOException if setting the last modified time fails.
732   */
733  @Override
734  public void setTimes(Path p, long mtime, long atime) throws IOException {
735    File f = pathToFile(p);
736    if(mtime >= 0) {
737      if(!f.setLastModified(mtime)) {
738        throw new IOException(
739          "couldn't set last-modified time to " +
740          mtime +
741          " for " +
742          f.getAbsolutePath());
743      }
744    }
745  }
746
747  @Override
748  public boolean supportsSymlinks() {
749    return true;
750  }
751
752  @SuppressWarnings("deprecation")
753  @Override
754  public void createSymlink(Path target, Path link, boolean createParent)
755      throws IOException {
756    if (!FileSystem.areSymlinksEnabled()) {
757      throw new UnsupportedOperationException("Symlinks not supported");
758    }
759    final String targetScheme = target.toUri().getScheme();
760    if (targetScheme != null && !"file".equals(targetScheme)) {
761      throw new IOException("Unable to create symlink to non-local file "+
762                            "system: "+target.toString());
763    }
764    if (createParent) {
765      mkdirs(link.getParent());
766    }
767
768    // NB: Use createSymbolicLink in java.nio.file.Path once available
769    int result = FileUtil.symLink(target.toString(),
770        makeAbsolute(link).toString());
771    if (result != 0) {
772      throw new IOException("Error " + result + " creating symlink " +
773          link + " to " + target);
774    }
775  }
776
777  /**
778   * Return a FileStatus representing the given path. If the path refers
779   * to a symlink return a FileStatus representing the link rather than
780   * the object the link refers to.
781   */
782  @Override
783  public FileStatus getFileLinkStatus(final Path f) throws IOException {
784    FileStatus fi = getFileLinkStatusInternal(f, false);
785    // getFileLinkStatus is supposed to return a symlink with a
786    // qualified path
787    if (fi.isSymlink()) {
788      Path targetQual = FSLinkResolver.qualifySymlinkTarget(this.getUri(),
789          fi.getPath(), fi.getSymlink());
790      fi.setSymlink(targetQual);
791    }
792    return fi;
793  }
794
795  /**
796   * Public {@link FileStatus} methods delegate to this function, which in turn
797   * either call the new {@link Stat} based implementation or the deprecated
798   * methods based on platform support.
799   * 
800   * @param f Path to stat
801   * @param dereference whether to dereference the final path component if a
802   *          symlink
803   * @return FileStatus of f
804   * @throws IOException
805   */
806  private FileStatus getFileLinkStatusInternal(final Path f,
807      boolean dereference) throws IOException {
808    if (!useDeprecatedFileStatus) {
809      return getNativeFileLinkStatus(f, dereference);
810    } else if (dereference) {
811      return deprecatedGetFileStatus(f);
812    } else {
813      return deprecatedGetFileLinkStatusInternal(f);
814    }
815  }
816
817  /**
818   * Deprecated. Remains for legacy support. Should be removed when {@link Stat}
819   * gains support for Windows and other operating systems.
820   */
821  @Deprecated
822  private FileStatus deprecatedGetFileLinkStatusInternal(final Path f)
823      throws IOException {
824    String target = FileUtil.readLink(new File(f.toString()));
825
826    try {
827      FileStatus fs = getFileStatus(f);
828      // If f refers to a regular file or directory
829      if (target.isEmpty()) {
830        return fs;
831      }
832      // Otherwise f refers to a symlink
833      return new FileStatus(fs.getLen(),
834          false,
835          fs.getReplication(),
836          fs.getBlockSize(),
837          fs.getModificationTime(),
838          fs.getAccessTime(),
839          fs.getPermission(),
840          fs.getOwner(),
841          fs.getGroup(),
842          new Path(target),
843          f);
844    } catch (FileNotFoundException e) {
845      /* The exists method in the File class returns false for dangling
846       * links so we can get a FileNotFoundException for links that exist.
847       * It's also possible that we raced with a delete of the link. Use
848       * the readBasicFileAttributes method in java.nio.file.attributes
849       * when available.
850       */
851      if (!target.isEmpty()) {
852        return new FileStatus(0, false, 0, 0, 0, 0, FsPermission.getDefault(),
853            "", "", new Path(target), f);
854      }
855      // f refers to a file or directory that does not exist
856      throw e;
857    }
858  }
859  /**
860   * Calls out to platform's native stat(1) implementation to get file metadata
861   * (permissions, user, group, atime, mtime, etc). This works around the lack
862   * of lstat(2) in Java 6.
863   * 
864   *  Currently, the {@link Stat} class used to do this only supports Linux
865   *  and FreeBSD, so the old {@link #deprecatedGetFileLinkStatusInternal(Path)}
866   *  implementation (deprecated) remains further OS support is added.
867   *
868   * @param f File to stat
869   * @param dereference whether to dereference symlinks
870   * @return FileStatus of f
871   * @throws IOException
872   */
873  private FileStatus getNativeFileLinkStatus(final Path f,
874      boolean dereference) throws IOException {
875    checkPath(f);
876    Stat stat = new Stat(f, getDefaultBlockSize(f), dereference, this);
877    FileStatus status = stat.getFileStatus();
878    return status;
879  }
880
881  @Override
882  public Path getLinkTarget(Path f) throws IOException {
883    FileStatus fi = getFileLinkStatusInternal(f, false);
884    // return an unqualified symlink target
885    return fi.getSymlink();
886  }
887}