org.apache.hadoop.fs.RawLocalFileSystem.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.fs.RawLocalFileSystem.java

Source

/**
 * 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 com.google.common.annotations.VisibleForTesting;

import java.io.BufferedOutputStream;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.FileDescriptor;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Optional;
import java.util.StringTokenizer;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.nativeio.NativeIO;
import org.apache.hadoop.util.Progressable;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.util.StringUtils;

/****************************************************************
 * Implement the FileSystem API for the raw local filesystem.
 *
 *****************************************************************/
@InterfaceAudience.Public
@InterfaceStability.Stable
public class RawLocalFileSystem extends FileSystem {
    static final URI NAME = URI.create("file:///");
    private Path workingDir;
    // Temporary workaround for HADOOP-9652.
    private static boolean useDeprecatedFileStatus = true;

    @VisibleForTesting
    public static void useStatIfAvailable() {
        useDeprecatedFileStatus = !Stat.isAvailable();
    }

    public RawLocalFileSystem() {
        workingDir = getInitialWorkingDirectory();
    }

    private Path makeAbsolute(Path f) {
        if (f.isAbsolute()) {
            return f;
        } else {
            return new Path(workingDir, f);
        }
    }

    /** Convert a path to a File. */
    public File pathToFile(Path path) {
        checkPath(path);
        if (!path.isAbsolute()) {
            path = new Path(getWorkingDirectory(), path);
        }
        return new File(path.toUri().getPath());
    }

    @Override
    public URI getUri() {
        return NAME;
    }

    @Override
    public void initialize(URI uri, Configuration conf) throws IOException {
        super.initialize(uri, conf);
        setConf(conf);
    }

    /*******************************************************
     * For open()'s FSInputStream.
     *******************************************************/
    class LocalFSFileInputStream extends FSInputStream implements HasFileDescriptor {
        private FileInputStream fis;
        private long position;

        public LocalFSFileInputStream(Path f) throws IOException {
            fis = new FileInputStream(pathToFile(f));
        }

        @Override
        public void seek(long pos) throws IOException {
            if (pos < 0) {
                throw new EOFException(FSExceptionMessages.NEGATIVE_SEEK);
            }
            fis.getChannel().position(pos);
            this.position = pos;
        }

        @Override
        public long getPos() throws IOException {
            return this.position;
        }

        @Override
        public boolean seekToNewSource(long targetPos) throws IOException {
            return false;
        }

        /*
         * Just forward to the fis
         */
        @Override
        public int available() throws IOException {
            return fis.available();
        }

        @Override
        public void close() throws IOException {
            fis.close();
        }

        @Override
        public boolean markSupported() {
            return false;
        }

        @Override
        public int read() throws IOException {
            try {
                int value = fis.read();
                if (value >= 0) {
                    this.position++;
                    statistics.incrementBytesRead(1);
                }
                return value;
            } catch (IOException e) { // unexpected exception
                throw new FSError(e); // assume native fs error
            }
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            // parameter check
            validatePositionedReadArgs(position, b, off, len);
            try {
                int value = fis.read(b, off, len);
                if (value > 0) {
                    this.position += value;
                    statistics.incrementBytesRead(value);
                }
                return value;
            } catch (IOException e) { // unexpected exception
                throw new FSError(e); // assume native fs error
            }
        }

        @Override
        public int read(long position, byte[] b, int off, int len) throws IOException {
            // parameter check
            validatePositionedReadArgs(position, b, off, len);
            if (len == 0) {
                return 0;
            }

            ByteBuffer bb = ByteBuffer.wrap(b, off, len);
            try {
                int value = fis.getChannel().read(bb, position);
                if (value > 0) {
                    statistics.incrementBytesRead(value);
                }
                return value;
            } catch (IOException e) {
                throw new FSError(e);
            }
        }

        @Override
        public long skip(long n) throws IOException {
            long value = fis.skip(n);
            if (value > 0) {
                this.position += value;
            }
            return value;
        }

        @Override
        public FileDescriptor getFileDescriptor() throws IOException {
            return fis.getFD();
        }
    }

    @Override
    public FSDataInputStream open(Path f, int bufferSize) throws IOException {
        getFileStatus(f);
        return new FSDataInputStream(new BufferedFSInputStream(new LocalFSFileInputStream(f), bufferSize));
    }

    @Override
    public FSDataInputStream open(PathHandle fd, int bufferSize) throws IOException {
        if (!(fd instanceof LocalFileSystemPathHandle)) {
            fd = new LocalFileSystemPathHandle(fd.bytes());
        }
        LocalFileSystemPathHandle id = (LocalFileSystemPathHandle) fd;
        id.verify(getFileStatus(new Path(id.getPath())));
        return new FSDataInputStream(
                new BufferedFSInputStream(new LocalFSFileInputStream(new Path(id.getPath())), bufferSize));
    }

    /*********************************************************
     * For create()'s FSOutputStream.
     *********************************************************/
    class LocalFSFileOutputStream extends OutputStream {
        private FileOutputStream fos;

        private LocalFSFileOutputStream(Path f, boolean append, FsPermission permission) throws IOException {
            File file = pathToFile(f);
            if (!append && permission == null) {
                permission = FsPermission.getFileDefault();
            }
            if (permission == null) {
                this.fos = new FileOutputStream(file, append);
            } else {
                permission = permission.applyUMask(FsPermission.getUMask(getConf()));
                if (Shell.WINDOWS && NativeIO.isAvailable()) {
                    this.fos = NativeIO.Windows.createFileOutputStreamWithMode(file, append, permission.toShort());
                } else {
                    this.fos = new FileOutputStream(file, append);
                    boolean success = false;
                    try {
                        setPermission(f, permission);
                        success = true;
                    } finally {
                        if (!success) {
                            IOUtils.cleanup(LOG, this.fos);
                        }
                    }
                }
            }
        }

        /*
         * Just forward to the fos
         */
        @Override
        public void close() throws IOException {
            fos.close();
        }

        @Override
        public void flush() throws IOException {
            fos.flush();
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            try {
                fos.write(b, off, len);
            } catch (IOException e) { // unexpected exception
                throw new FSError(e); // assume native fs error
            }
        }

        @Override
        public void write(int b) throws IOException {
            try {
                fos.write(b);
            } catch (IOException e) { // unexpected exception
                throw new FSError(e); // assume native fs error
            }
        }
    }

    @Override
    public FSDataOutputStream append(Path f, int bufferSize, Progressable progress) throws IOException {
        FileStatus status = getFileStatus(f);
        if (status.isDirectory()) {
            throw new IOException("Cannot append to a diretory (=" + f + " )");
        }
        return new FSDataOutputStream(
                new BufferedOutputStream(createOutputStreamWithMode(f, true, null), bufferSize), statistics,
                status.getLen());
    }

    @Override
    public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize, short replication, long blockSize,
            Progressable progress) throws IOException {
        return create(f, overwrite, true, bufferSize, replication, blockSize, progress, null);
    }

    private FSDataOutputStream create(Path f, boolean overwrite, boolean createParent, int bufferSize,
            short replication, long blockSize, Progressable progress, FsPermission permission) throws IOException {
        if (exists(f) && !overwrite) {
            throw new FileAlreadyExistsException("File already exists: " + f);
        }
        Path parent = f.getParent();
        if (parent != null && !mkdirs(parent)) {
            throw new IOException("Mkdirs failed to create " + parent.toString());
        }
        return new FSDataOutputStream(
                new BufferedOutputStream(createOutputStreamWithMode(f, false, permission), bufferSize), statistics);
    }

    protected OutputStream createOutputStream(Path f, boolean append) throws IOException {
        return createOutputStreamWithMode(f, append, null);
    }

    protected OutputStream createOutputStreamWithMode(Path f, boolean append, FsPermission permission)
            throws IOException {
        return new LocalFSFileOutputStream(f, append, permission);
    }

    @Override
    public FSDataOutputStream createNonRecursive(Path f, FsPermission permission, EnumSet<CreateFlag> flags,
            int bufferSize, short replication, long blockSize, Progressable progress) throws IOException {
        if (exists(f) && !flags.contains(CreateFlag.OVERWRITE)) {
            throw new FileAlreadyExistsException("File already exists: " + f);
        }
        return new FSDataOutputStream(
                new BufferedOutputStream(createOutputStreamWithMode(f, false, permission), bufferSize), statistics);
    }

    @Override
    public FSDataOutputStream create(Path f, FsPermission permission, boolean overwrite, int bufferSize,
            short replication, long blockSize, Progressable progress) throws IOException {

        FSDataOutputStream out = create(f, overwrite, true, bufferSize, replication, blockSize, progress,
                permission);
        return out;
    }

    @Override
    public FSDataOutputStream createNonRecursive(Path f, FsPermission permission, boolean overwrite, int bufferSize,
            short replication, long blockSize, Progressable progress) throws IOException {
        FSDataOutputStream out = create(f, overwrite, false, bufferSize, replication, blockSize, progress,
                permission);
        return out;
    }

    @Override
    public void concat(final Path trg, final Path[] psrcs) throws IOException {
        final int bufferSize = 4096;
        try (FSDataOutputStream out = create(trg)) {
            for (Path src : psrcs) {
                try (FSDataInputStream in = open(src)) {
                    IOUtils.copyBytes(in, out, bufferSize, false);
                }
            }
        }
    }

    @Override
    public boolean rename(Path src, Path dst) throws IOException {
        // Attempt rename using Java API.
        File srcFile = pathToFile(src);
        File dstFile = pathToFile(dst);
        if (srcFile.renameTo(dstFile)) {
            return true;
        }

        // Else try POSIX style rename on Windows only
        if (Shell.WINDOWS && handleEmptyDstDirectoryOnWindows(src, srcFile, dst, dstFile)) {
            return true;
        }

        // The fallback behavior accomplishes the rename by a full copy.
        if (LOG.isDebugEnabled()) {
            LOG.debug("Falling through to a copy of " + src + " to " + dst);
        }
        return FileUtil.copy(this, src, this, dst, true, getConf());
    }

    @VisibleForTesting
    public final boolean handleEmptyDstDirectoryOnWindows(Path src, File srcFile, Path dst, File dstFile)
            throws IOException {

        // Enforce POSIX rename behavior that a source directory replaces an
        // existing destination if the destination is an empty directory. On most
        // platforms, this is already handled by the Java API call above. Some
        // platforms (notably Windows) do not provide this behavior, so the Java API
        // call renameTo(dstFile) fails. Delete destination and attempt rename
        // again.
        try {
            FileStatus sdst = this.getFileStatus(dst);
            String[] dstFileList = dstFile.list();
            if (dstFileList != null) {
                if (sdst.isDirectory() && dstFileList.length == 0) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Deleting empty destination and renaming " + src + " to " + dst);
                    }
                    if (this.delete(dst, false) && srcFile.renameTo(dstFile)) {
                        return true;
                    }
                }
            }
        } catch (FileNotFoundException ignored) {
        }
        return false;
    }

    @Override
    public boolean truncate(Path f, final long newLength) throws IOException {
        FileStatus status = getFileStatus(f);
        if (status == null) {
            throw new FileNotFoundException("File " + f + " not found");
        }
        if (status.isDirectory()) {
            throw new IOException("Cannot truncate a directory (=" + f + ")");
        }
        long oldLength = status.getLen();
        if (newLength > oldLength) {
            throw new IllegalArgumentException("Cannot truncate to a larger file size. Current size: " + oldLength
                    + ", truncate size: " + newLength + ".");
        }
        try (FileOutputStream out = new FileOutputStream(pathToFile(f), true)) {
            try {
                out.getChannel().truncate(newLength);
            } catch (IOException e) {
                throw new FSError(e);
            }
        }
        return true;
    }

    /**
     * Delete the given path to a file or directory.
     * @param p the path to delete
     * @param recursive to delete sub-directories
     * @return true if the file or directory and all its contents were deleted
     * @throws IOException if p is non-empty and recursive is false 
     */
    @Override
    public boolean delete(Path p, boolean recursive) throws IOException {
        File f = pathToFile(p);
        if (!f.exists()) {
            //no path, return false "nothing to delete"
            return false;
        }
        if (f.isFile()) {
            return f.delete();
        } else if (!recursive && f.isDirectory() && (FileUtil.listFiles(f).length != 0)) {
            throw new IOException("Directory " + f.toString() + " is not empty");
        }
        return FileUtil.fullyDelete(f);
    }

    /**
     * {@inheritDoc}
     *
     * (<b>Note</b>: Returned list is not sorted in any given order,
     * due to reliance on Java's {@link File#list()} API.)
     */
    @Override
    public FileStatus[] listStatus(Path f) throws IOException {
        File localf = pathToFile(f);
        FileStatus[] results;

        if (!localf.exists()) {
            throw new FileNotFoundException("File " + f + " does not exist");
        }

        if (localf.isDirectory()) {
            String[] names = FileUtil.list(localf);
            results = new FileStatus[names.length];
            int j = 0;
            for (int i = 0; i < names.length; i++) {
                try {
                    // Assemble the path using the Path 3 arg constructor to make sure
                    // paths with colon are properly resolved on Linux
                    results[j] = getFileStatus(new Path(f, new Path(null, null, names[i])));
                    j++;
                } catch (FileNotFoundException e) {
                    // ignore the files not found since the dir list may have have
                    // changed since the names[] list was generated.
                }
            }
            if (j == names.length) {
                return results;
            }
            return Arrays.copyOf(results, j);
        }

        if (!useDeprecatedFileStatus) {
            return new FileStatus[] { getFileStatus(f) };
        }
        return new FileStatus[] { new DeprecatedRawLocalFileStatus(localf, getDefaultBlockSize(f), this) };
    }

    protected boolean mkOneDir(File p2f) throws IOException {
        return mkOneDirWithMode(new Path(p2f.getAbsolutePath()), p2f, null);
    }

    protected boolean mkOneDirWithMode(Path p, File p2f, FsPermission permission) throws IOException {
        if (permission == null) {
            permission = FsPermission.getDirDefault();
        }
        permission = permission.applyUMask(FsPermission.getUMask(getConf()));
        if (Shell.WINDOWS && NativeIO.isAvailable()) {
            try {
                NativeIO.Windows.createDirectoryWithMode(p2f, permission.toShort());
                return true;
            } catch (IOException e) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(String.format("NativeIO.createDirectoryWithMode error, path = %s, mode = %o", p2f,
                            permission.toShort()), e);
                }
                return false;
            }
        } else {
            boolean b = p2f.mkdir();
            if (b) {
                setPermission(p, permission);
            }
            return b;
        }
    }

    /**
     * Creates the specified directory hierarchy. Does not
     * treat existence as an error.
     */
    @Override
    public boolean mkdirs(Path f) throws IOException {
        return mkdirsWithOptionalPermission(f, null);
    }

    @Override
    public boolean mkdirs(Path f, FsPermission permission) throws IOException {
        return mkdirsWithOptionalPermission(f, permission);
    }

    private boolean mkdirsWithOptionalPermission(Path f, FsPermission permission) throws IOException {
        if (f == null) {
            throw new IllegalArgumentException("mkdirs path arg is null");
        }
        Path parent = f.getParent();
        File p2f = pathToFile(f);
        File parent2f = null;
        if (parent != null) {
            parent2f = pathToFile(parent);
            if (parent2f != null && parent2f.exists() && !parent2f.isDirectory()) {
                throw new ParentNotDirectoryException("Parent path is not a directory: " + parent);
            }
        }
        if (p2f.exists() && !p2f.isDirectory()) {
            throw new FileAlreadyExistsException(
                    "Destination exists" + " and is not a directory: " + p2f.getCanonicalPath());
        }
        return (parent == null || parent2f.exists() || mkdirs(parent))
                && (mkOneDirWithMode(f, p2f, permission) || p2f.isDirectory());
    }

    @Override
    public Path getHomeDirectory() {
        return this.makeQualified(new Path(System.getProperty("user.home")));
    }

    /**
     * Set the working directory to the given directory.
     */
    @Override
    public void setWorkingDirectory(Path newDir) {
        workingDir = makeAbsolute(newDir);
        checkPath(workingDir);
    }

    @Override
    public Path getWorkingDirectory() {
        return workingDir;
    }

    @Override
    protected Path getInitialWorkingDirectory() {
        return this.makeQualified(new Path(System.getProperty("user.dir")));
    }

    @Override
    public FsStatus getStatus(Path p) throws IOException {
        File partition = pathToFile(p == null ? new Path("/") : p);
        //File provides getUsableSpace() and getFreeSpace()
        //File provides no API to obtain used space, assume used = total - free
        return new FsStatus(partition.getTotalSpace(), partition.getTotalSpace() - partition.getFreeSpace(),
                partition.getFreeSpace());
    }

    // In the case of the local filesystem, we can just rename the file.
    @Override
    public void moveFromLocalFile(Path src, Path dst) throws IOException {
        rename(src, dst);
    }

    // We can write output directly to the final location
    @Override
    public Path startLocalOutput(Path fsOutputFile, Path tmpLocalFile) throws IOException {
        return fsOutputFile;
    }

    // It's in the right place - nothing to do.
    @Override
    public void completeLocalOutput(Path fsWorkingFile, Path tmpLocalFile) throws IOException {
    }

    @Override
    public void close() throws IOException {
        super.close();
    }

    @Override
    public String toString() {
        return "LocalFS";
    }

    @Override
    public FileStatus getFileStatus(Path f) throws IOException {
        return getFileLinkStatusInternal(f, true);
    }

    @Deprecated
    private FileStatus deprecatedGetFileStatus(Path f) throws IOException {
        File path = pathToFile(f);
        if (path.exists()) {
            return new DeprecatedRawLocalFileStatus(pathToFile(f), getDefaultBlockSize(f), this);
        } else {
            throw new FileNotFoundException("File " + f + " does not exist");
        }
    }

    @Deprecated
    static class DeprecatedRawLocalFileStatus extends FileStatus {
        /* We can add extra fields here. It breaks at least CopyFiles.FilePair().
         * We recognize if the information is already loaded by check if
         * onwer.equals("").
         */
        private boolean isPermissionLoaded() {
            return !super.getOwner().isEmpty();
        }

        private static long getLastAccessTime(File f) throws IOException {
            long accessTime;
            try {
                accessTime = Files.readAttributes(f.toPath(), BasicFileAttributes.class).lastAccessTime()
                        .toMillis();
            } catch (NoSuchFileException e) {
                throw new FileNotFoundException("File " + f + " does not exist");
            }
            return accessTime;
        }

        DeprecatedRawLocalFileStatus(File f, long defaultBlockSize, FileSystem fs) throws IOException {
            super(f.length(), f.isDirectory(), 1, defaultBlockSize, f.lastModified(), getLastAccessTime(f), null,
                    null, null, new Path(f.getPath()).makeQualified(fs.getUri(), fs.getWorkingDirectory()));
        }

        @Override
        public FsPermission getPermission() {
            if (!isPermissionLoaded()) {
                loadPermissionInfo();
            }
            return super.getPermission();
        }

        @Override
        public String getOwner() {
            if (!isPermissionLoaded()) {
                loadPermissionInfo();
            }
            return super.getOwner();
        }

        @Override
        public String getGroup() {
            if (!isPermissionLoaded()) {
                loadPermissionInfo();
            }
            return super.getGroup();
        }

        /**
         * Load file permission information (UNIX symbol rwxrwxrwx, sticky bit info).
         *
         * To improve peformance, give priority to native stat() call. First try get
         * permission information by using native JNI call then fall back to use non
         * native (ProcessBuilder) call in case native lib is not loaded or native
         * call is not successful
         */
        private synchronized void loadPermissionInfo() {
            if (!isPermissionLoaded() && NativeIO.isAvailable()) {
                try {
                    loadPermissionInfoByNativeIO();
                } catch (IOException ex) {
                    LOG.debug("Native call failed", ex);
                }
            }

            if (!isPermissionLoaded()) {
                loadPermissionInfoByNonNativeIO();
            }
        }

        /// loads permissions, owner, and group from `ls -ld`
        @VisibleForTesting
        void loadPermissionInfoByNonNativeIO() {
            IOException e = null;
            try {
                String output = FileUtil.execCommand(new File(getPath().toUri()), Shell.getGetPermissionCommand());
                StringTokenizer t = new StringTokenizer(output, Shell.TOKEN_SEPARATOR_REGEX);
                //expected format
                //-rw-------    1 username groupname ...
                String permission = t.nextToken();
                if (permission.length() > FsPermission.MAX_PERMISSION_LENGTH) {
                    //files with ACLs might have a '+'
                    permission = permission.substring(0, FsPermission.MAX_PERMISSION_LENGTH);
                }
                setPermission(FsPermission.valueOf(permission));
                t.nextToken();

                String owner = t.nextToken();
                String group = t.nextToken();
                // If on windows domain, token format is DOMAIN\\user and we want to
                // extract only the user name
                // same as to the group name
                if (Shell.WINDOWS) {
                    owner = removeDomain(owner);
                    group = removeDomain(group);
                }
                setOwner(owner);
                setGroup(group);
            } catch (Shell.ExitCodeException ioe) {
                if (ioe.getExitCode() != 1) {
                    e = ioe;
                } else {
                    setPermission(null);
                    setOwner(null);
                    setGroup(null);
                }
            } catch (IOException ioe) {
                e = ioe;
            } finally {
                if (e != null) {
                    throw new RuntimeException("Error while running command to get " + "file permissions : "
                            + StringUtils.stringifyException(e));
                }
            }
        }

        // In Windows, domain name is added.
        // For example, given machine name (domain name) dname, user name i, then
        // the result for user is dname\\i and for group is dname\\None. So we need
        // remove domain name as follows:
        // DOMAIN\\user => user, DOMAIN\\group => group
        private String removeDomain(String str) {
            int index = str.indexOf("\\");
            if (index != -1) {
                str = str.substring(index + 1);
            }
            return str;
        }

        // loads permissions, owner, and group from `ls -ld`
        // but use JNI to more efficiently get file mode (permission, owner, group)
        // by calling file stat() in *nix or some similar calls in Windows
        @VisibleForTesting
        void loadPermissionInfoByNativeIO() throws IOException {
            Path path = getPath();
            String pathName = path.toUri().getPath();
            // remove leading slash for Windows path
            if (Shell.WINDOWS && pathName.startsWith("/")) {
                pathName = pathName.substring(1);
            }
            try {
                NativeIO.POSIX.Stat stat = NativeIO.POSIX.getStat(pathName);
                String owner = stat.getOwner();
                String group = stat.getGroup();
                int mode = stat.getMode();
                setOwner(owner);
                setGroup(group);
                setPermission(new FsPermission(mode));
            } catch (IOException e) {
                setOwner(null);
                setGroup(null);
                setPermission(null);
                throw e;
            }
        }

        @Override
        public void write(DataOutput out) throws IOException {
            if (!isPermissionLoaded()) {
                loadPermissionInfo();
            }
            super.write(out);
        }
    }

    /**
     * Use the command chown to set owner.
     */
    @Override
    public void setOwner(Path p, String username, String groupname) throws IOException {
        FileUtil.setOwner(pathToFile(p), username, groupname);
    }

    /**
     * Use the command chmod to set permission.
     */
    @Override
    public void setPermission(Path p, FsPermission permission) throws IOException {
        if (NativeIO.isAvailable()) {
            NativeIO.POSIX.chmod(pathToFile(p).getCanonicalPath(), permission.toShort());
        } else {
            String perm = String.format("%04o", permission.toShort());
            Shell.execCommand(
                    Shell.getSetPermissionCommand(perm, false, FileUtil.makeShellPath(pathToFile(p), true)));
        }
    }

    /**
     * Sets the {@link Path}'s last modified time and last access time to
     * the given valid times.
     *
     * @param mtime the modification time to set (only if no less than zero).
     * @param atime the access time to set (only if no less than zero).
     * @throws IOException if setting the times fails.
     */
    @Override
    public void setTimes(Path p, long mtime, long atime) throws IOException {
        try {
            BasicFileAttributeView view = Files.getFileAttributeView(pathToFile(p).toPath(),
                    BasicFileAttributeView.class);
            FileTime fmtime = (mtime >= 0) ? FileTime.fromMillis(mtime) : null;
            FileTime fatime = (atime >= 0) ? FileTime.fromMillis(atime) : null;
            view.setTimes(fmtime, fatime, null);
        } catch (NoSuchFileException e) {
            throw new FileNotFoundException("File " + p + " does not exist");
        }
    }

    /**
     * Hook to implement support for {@link PathHandle} operations.
     * @param stat Referent in the target FileSystem
     * @param opts Constraints that determine the validity of the
     *            {@link PathHandle} reference.
     */
    protected PathHandle createPathHandle(FileStatus stat, Options.HandleOpt... opts) {
        if (stat.isDirectory() || stat.isSymlink()) {
            throw new IllegalArgumentException("PathHandle only available for files");
        }
        String authority = stat.getPath().toUri().getAuthority();
        if (authority != null && !authority.equals("file://")) {
            throw new IllegalArgumentException("Wrong FileSystem: " + stat.getPath());
        }
        Options.HandleOpt.Data data = Options.HandleOpt.getOpt(Options.HandleOpt.Data.class, opts)
                .orElse(Options.HandleOpt.changed(false));
        Options.HandleOpt.Location loc = Options.HandleOpt.getOpt(Options.HandleOpt.Location.class, opts)
                .orElse(Options.HandleOpt.moved(false));
        if (loc.allowChange()) {
            throw new UnsupportedOperationException(
                    "Tracking file movement in " + "basic FileSystem is not supported");
        }
        final Path p = stat.getPath();
        final Optional<Long> mtime = !data.allowChange() ? Optional.of(stat.getModificationTime())
                : Optional.empty();
        return new LocalFileSystemPathHandle(p.toString(), mtime);
    }

    @Override
    public boolean supportsSymlinks() {
        return true;
    }

    @SuppressWarnings("deprecation")
    @Override
    public void createSymlink(Path target, Path link, boolean createParent) throws IOException {
        if (!FileSystem.areSymlinksEnabled()) {
            throw new UnsupportedOperationException("Symlinks not supported");
        }
        final String targetScheme = target.toUri().getScheme();
        if (targetScheme != null && !"file".equals(targetScheme)) {
            throw new IOException("Unable to create symlink to non-local file " + "system: " + target.toString());
        }
        if (createParent) {
            mkdirs(link.getParent());
        }

        // NB: Use createSymbolicLink in java.nio.file.Path once available
        int result = FileUtil.symLink(target.toString(), makeAbsolute(link).toString());
        if (result != 0) {
            throw new IOException("Error " + result + " creating symlink " + link + " to " + target);
        }
    }

    /**
     * Return a FileStatus representing the given path. If the path refers
     * to a symlink return a FileStatus representing the link rather than
     * the object the link refers to.
     */
    @Override
    public FileStatus getFileLinkStatus(final Path f) throws IOException {
        FileStatus fi = getFileLinkStatusInternal(f, false);
        // getFileLinkStatus is supposed to return a symlink with a
        // qualified path
        if (fi.isSymlink()) {
            Path targetQual = FSLinkResolver.qualifySymlinkTarget(this.getUri(), fi.getPath(), fi.getSymlink());
            fi.setSymlink(targetQual);
        }
        return fi;
    }

    /**
     * Public {@link FileStatus} methods delegate to this function, which in turn
     * either call the new {@link Stat} based implementation or the deprecated
     * methods based on platform support.
     * 
     * @param f Path to stat
     * @param dereference whether to dereference the final path component if a
     *          symlink
     * @return FileStatus of f
     * @throws IOException
     */
    private FileStatus getFileLinkStatusInternal(final Path f, boolean dereference) throws IOException {
        if (!useDeprecatedFileStatus) {
            return getNativeFileLinkStatus(f, dereference);
        } else if (dereference) {
            return deprecatedGetFileStatus(f);
        } else {
            return deprecatedGetFileLinkStatusInternal(f);
        }
    }

    /**
     * Deprecated. Remains for legacy support. Should be removed when {@link Stat}
     * gains support for Windows and other operating systems.
     */
    @Deprecated
    private FileStatus deprecatedGetFileLinkStatusInternal(final Path f) throws IOException {
        String target = FileUtil.readLink(new File(f.toString()));

        try {
            FileStatus fs = getFileStatus(f);
            // If f refers to a regular file or directory
            if (target.isEmpty()) {
                return fs;
            }
            // Otherwise f refers to a symlink
            return new FileStatus(fs.getLen(), false, fs.getReplication(), fs.getBlockSize(),
                    fs.getModificationTime(), fs.getAccessTime(), fs.getPermission(), fs.getOwner(), fs.getGroup(),
                    new Path(target), f);
        } catch (FileNotFoundException e) {
            /* The exists method in the File class returns false for dangling
             * links so we can get a FileNotFoundException for links that exist.
             * It's also possible that we raced with a delete of the link. Use
             * the readBasicFileAttributes method in java.nio.file.attributes
             * when available.
             */
            if (!target.isEmpty()) {
                return new FileStatus(0, false, 0, 0, 0, 0, FsPermission.getDefault(), "", "", new Path(target), f);
            }
            // f refers to a file or directory that does not exist
            throw e;
        }
    }

    /**
     * Calls out to platform's native stat(1) implementation to get file metadata
     * (permissions, user, group, atime, mtime, etc). This works around the lack
     * of lstat(2) in Java 6.
     * 
     *  Currently, the {@link Stat} class used to do this only supports Linux
     *  and FreeBSD, so the old {@link #deprecatedGetFileLinkStatusInternal(Path)}
     *  implementation (deprecated) remains further OS support is added.
     *
     * @param f File to stat
     * @param dereference whether to dereference symlinks
     * @return FileStatus of f
     * @throws IOException
     */
    private FileStatus getNativeFileLinkStatus(final Path f, boolean dereference) throws IOException {
        checkPath(f);
        Stat stat = new Stat(f, getDefaultBlockSize(f), dereference, this);
        FileStatus status = stat.getFileStatus();
        return status;
    }

    @Override
    public Path getLinkTarget(Path f) throws IOException {
        FileStatus fi = getFileLinkStatusInternal(f, false);
        // return an unqualified symlink target
        return fi.getSymlink();
    }
}