de.zib.sfs.StatisticsFileSystem.java Source code

Java tutorial

Introduction

Here is the source code for de.zib.sfs.StatisticsFileSystem.java

Source

/*
 * Copyright (c) 2016 by Robert Schmidtke,
 *               Zuse Institute Berlin
 *
 * Licensed under the BSD License, see LICENSE file for details.
 *
 */
package de.zib.sfs;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.EnumSet;
import java.util.Random;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.BlockLocation;
import org.apache.hadoop.fs.ContentSummary;
import org.apache.hadoop.fs.CreateFlag;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileAlreadyExistsException;
import org.apache.hadoop.fs.FileChecksum;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FsServerDefaults;
import org.apache.hadoop.fs.FsStatus;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Options.ChecksumOpt;
import org.apache.hadoop.fs.ParentNotDirectoryException;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.PathFilter;
import org.apache.hadoop.fs.RemoteIterator;
import org.apache.hadoop.fs.UnsupportedFileSystemException;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.Progressable;

import de.zib.sfs.instrument.statistics.LiveOperationStatisticsAggregator;
import de.zib.sfs.instrument.statistics.LiveOperationStatisticsAggregator.OutputFormat;
import de.zib.sfs.instrument.statistics.OperationCategory;
import de.zib.sfs.instrument.statistics.OperationSource;

/**
 * Implements the Hadoop {@link org.apache.hadoop.fs.FileSystem} interface as it
 * is used in Hadoop and Flink.
 * 
 * @author robert
 *
 */
public class StatisticsFileSystem extends FileSystem {

    /**
     * The fully qualified class name of the file system implementation to wrap.
     * Must be a subclass of {@link org.apache.hadoop.fs.FileSystem}.
     */
    public static final String SFS_WRAPPED_FS_CLASS_NAME_KEY = "sfs.wrappedFS.className";

    /**
     * The scheme of the wrapped file system.
     */
    public static final String SFS_WRAPPED_FS_SCHEME_KEY = "sfs.wrappedFS.scheme";

    /**
     * r|w|o or any combination of them, indicates which operation categories
     * not to log.
     */
    public static final String SFS_INSTRUMENTATION_SKIP_KEY = "sfs.instrumentation.skip";

    /**
     * Path to a directory.
     */
    public static final String SFS_OUTPUT_DIRECTORY_KEY = "sfs.output.directory";

    /**
     * CSV, FB, BB.
     */
    public static final String SFS_OUTPUT_FORMAT_KEY = "sfs.output.format";

    /**
     * Set to true if instrumentation should be done on per-file basis instead
     * of globally.
     */
    public static final String SFS_TRACE_FDS_KEY = "sfs.traceFds";

    // Shadow super class' LOG
    public static final Log LOG = LogFactory.getLog(StatisticsFileSystem.class);

    /**
     * The URI of this file system, as sfs:// plus the authority of the wrapped
     * file system.
     */
    private URI fileSystemUri;

    /**
     * The wrapped file system implementation.
     */
    private FileSystem wrappedFS;

    /**
     * The scheme of the wrapped file system.
     */
    private String wrappedFSScheme;

    /**
     * Flag to track whether this file system is closed already.
     */
    private boolean closed = false;

    /**
     * Flag to track whether this file system is initialized already.
     */
    private boolean initialized = false;

    /**
     * Set of operation categories to skip.
     */
    private boolean skipRead = false, skipWrite = false, skipOther = false;

    @Override
    public synchronized void initialize(URI name, Configuration conf) throws IOException {
        if (this.initialized) {
            LOG.warn("Ignoring attempt to re-initialize file system.");
            return;
        }

        super.initialize(name, conf);
        setConf(conf);

        String hostname = System.getProperty("de.zib.sfs.hostname");
        if (hostname == null) {
            LOG.warn("'de.zib.sfs.hostname' not set, did the agent start properly?");

            // Obtain hostname, preferably via executing hostname
            Process hostnameProcess = Runtime.getRuntime().exec("hostname");
            try {
                int exitCode = hostnameProcess.waitFor();
                if (exitCode != 0) {
                    LOG.warn("'hostname' returned " + exitCode + ", using $HOSTNAME instead.");
                    hostname = System.getenv("HOSTNAME");
                } else {
                    try (BufferedReader reader = new BufferedReader(
                            new InputStreamReader(hostnameProcess.getInputStream()))) {

                        StringBuilder hostnameBuilder = new StringBuilder();
                        String line = "";
                        while ((line = reader.readLine()) != null) {
                            hostnameBuilder.append(line);
                        }
                        hostname = hostnameBuilder.toString();
                    }
                }
            } catch (InterruptedException e) {
                LOG.warn("Error executing 'hostname', using $HOSTNAME instead.", e);
                hostname = System.getenv("HOSTNAME");
            }

            System.setProperty("de.zib.sfs.hostname", hostname);
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Running on " + hostname + ".");
        }

        if (System.getProperty("de.zib.sfs.pid") == null) {
            LOG.warn("'de.zib.sfs.pid' not set, did the agent start properly?");

            // use negative random number to indicate it's no real PID
            int pid = -new Random().nextInt(Integer.MAX_VALUE);
            System.setProperty("de.zib.sfs.pid", Integer.toString(pid));
        }

        if (System.getProperty("de.zib.sfs.key") == null) {
            LOG.warn("'de.zib.sfs.key' not set, did the agent start properly?");
            System.setProperty("de.zib.sfs.key", "sfs");
        }

        if (System.getProperty("de.zib.sfs.timeBin.duration") == null) {
            LOG.warn("'de.zib.sfs.timeBin.duration' not set, did the agent start properly?");
            System.setProperty("de.zib.sfs.timeBin.duration", "1000");
        }

        if (System.getProperty("de.zib.sfs.timeBin.cacheSize") == null) {
            LOG.warn("'de.zib.sfs.timeBin.cacheSize' not set, did the agent start properly?");
            System.setProperty("de.zib.sfs.timeBin.cacheSize", "30");
        }

        if (System.getProperty("de.zib.sfs.output.directory") == null) {
            LOG.warn("'de.zib.sfs.output.directory' not set, did the agent start properly?");
            System.setProperty("de.zib.sfs.output.directory", getConf().get(SFS_OUTPUT_DIRECTORY_KEY, "/tmp"));
        }

        if (System.getProperty("de.zib.sfs.output.format") == null) {
            LOG.warn("'de.zib.sfs.output.format' not set, did the agent start properly?");
            OutputFormat outputFormat = OutputFormat
                    .valueOf(getConf().get(SFS_OUTPUT_FORMAT_KEY, OutputFormat.BB.name()).toUpperCase());
            System.setProperty("de.zib.sfs.output.format", outputFormat.name());
        }

        if (System.getProperty("de.zib.sfs.traceFds") == null) {
            LOG.warn("'de.zib.sfs.traceFds' not set, did the agent start properly?");
            System.setProperty("de.zib.sfs.traceFds",
                    getConf().getBoolean(SFS_TRACE_FDS_KEY, false) ? "true" : "false");
        }

        LiveOperationStatisticsAggregator.instance.initialize();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Initialized file system statistics aggregator.");
        }

        // Obtain the file system class we want to wrap
        String wrappedFSClassName = getConf().get(SFS_WRAPPED_FS_CLASS_NAME_KEY);
        if (wrappedFSClassName == null) {
            throw new RuntimeException(SFS_WRAPPED_FS_CLASS_NAME_KEY + " not specified");
        }
        this.wrappedFSScheme = getConf().get(SFS_WRAPPED_FS_SCHEME_KEY);
        if (this.wrappedFSScheme == null) {
            throw new RuntimeException(SFS_WRAPPED_FS_SCHEME_KEY + " not specified");
        }

        Class<?> wrappedFSClass;
        try {
            wrappedFSClass = Class.forName(wrappedFSClassName);
        } catch (Exception e) {
            throw new RuntimeException("Error obtaining class '" + wrappedFSClassName + "'", e);
        }

        // Figure out what kind of file system we are wrapping.
        if (wrappedFSClassName.startsWith("org.apache.hadoop")
                || wrappedFSClassName.startsWith("org.xtreemfs.common.clients.hadoop")) {
            try {
                // Wrap Hadoop file system directly.
                this.wrappedFS = wrappedFSClass.asSubclass(FileSystem.class).newInstance();
            } catch (Exception e) {
                throw new RuntimeException("Error instantiating Hadoop class '" + wrappedFSClassName + "'", e);
            }
        } else {
            throw new RuntimeException("Unsupported file system class '" + wrappedFSClassName + "'");
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Wrapping file system '" + this.wrappedFS.getClass().getName() + "' with scheme '"
                    + this.wrappedFSScheme + "' as '" + getScheme() + "'.");
            LOG.debug("You can change it by setting '" + SFS_WRAPPED_FS_CLASS_NAME_KEY + "'.");
        }

        if (name.getAuthority() != null) {
            this.fileSystemUri = URI.create(getScheme() + "://" + name.getAuthority() + "/");
        } else {
            this.fileSystemUri = URI.create(getScheme() + ":///");
        }

        // Finally initialize the wrapped file system with the unwrapped name.
        URI wrappedFSUri = replaceUriScheme(name, getScheme(), this.wrappedFSScheme);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Initializing wrapped file system with URI '" + wrappedFSUri + "'.");
        }
        this.wrappedFS.initialize(wrappedFSUri, conf);

        // Add shutdown hook that closes this file system
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Running shutdown hook.");
                }

                try {
                    StatisticsFileSystem.this.close(true);
                } catch (IOException e) {
                    LOG.error("Could not close file system.", e);
                }
            }
        });

        String instrumentationSkip = getConf().get(SFS_INSTRUMENTATION_SKIP_KEY);
        if (instrumentationSkip != null) {
            this.skipRead = instrumentationSkip.contains("r");
            this.skipWrite = instrumentationSkip.contains("w");
            this.skipOther = instrumentationSkip.contains("o");
        }

        this.initialized = true;
    }

    @Override
    public Token<?>[] addDelegationTokens(String renewer, Credentials credentials) throws IOException {
        return this.wrappedFS.addDelegationTokens(renewer, credentials);
    }

    @Override
    public FSDataOutputStream append(Path f) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.append(unwrappedPath), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.append(unwrappedPath);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream append(Path f, int bufferSize) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.append(unwrappedPath, bufferSize), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.append(unwrappedPath, bufferSize);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream append(Path f, int bufferSize, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.append(unwrappedPath, bufferSize, progress), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.append(unwrappedPath, bufferSize, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public boolean cancelDeleteOnExit(Path f) {
        return this.wrappedFS.cancelDeleteOnExit(unwrapPath(f));
    }

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

    synchronized final void close(boolean fromShutdownHook) throws IOException {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Closing file system.");
        }

        if (this.closed) {
            LOG.warn("Ignoring attempt to re-close file system.");
            return;
        }

        // If called from a shutdown hook, org.apache.hadoop.fs.FileSystem will
        // be closed during its own shutdown hook, so avoid deadlock here by
        // only closing wrappedFS and super when explicitly closed.
        if (!fromShutdownHook) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Closing wrapped file system.");
            }
            this.wrappedFS.close();
            if (LOG.isDebugEnabled()) {
                LOG.debug("Closed wrapped file system.");
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug("Closing parent file system.");
            }
            super.close();
            if (LOG.isDebugEnabled()) {
                LOG.debug("Closed parent file system.");
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Closed file system.");
        }
        this.closed = true;
    }

    @Override
    public void completeLocalOutput(Path fsOutputFile, Path tmpLocalFile) throws IOException {
        this.wrappedFS.completeLocalOutput(unwrapPath(fsOutputFile), unwrapPath(tmpLocalFile));
    }

    @Override
    public void concat(Path trg, Path[] psrcs) throws IOException {
        Path[] unwrappedPsrcs = new Path[psrcs.length];
        for (int i = 0; i < psrcs.length; ++i) {
            unwrappedPsrcs[i] = unwrapPath(psrcs[i]);
        }
        this.wrappedFS.concat(unwrapPath(trg), unwrappedPsrcs);
    }

    @Override
    public void copyFromLocalFile(boolean delSrc, boolean overwrite, Path src, Path dst) throws IOException {
        this.wrappedFS.copyFromLocalFile(delSrc, overwrite, unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public void copyFromLocalFile(boolean delSrc, boolean overwrite, Path[] srcs, Path dst) throws IOException {
        Path[] unwrappedSrcs = new Path[srcs.length];
        for (int i = 0; i < srcs.length; ++i) {
            unwrappedSrcs[i] = unwrapPath(srcs[i]);
        }
        this.wrappedFS.copyFromLocalFile(delSrc, overwrite, srcs, unwrapPath(dst));
    }

    @Override
    public void copyFromLocalFile(boolean delSrc, Path src, Path dst) throws IOException {
        this.wrappedFS.copyFromLocalFile(delSrc, unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public void copyFromLocalFile(Path src, Path dst) throws IOException {
        this.wrappedFS.copyFromLocalFile(unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public void copyToLocalFile(boolean delSrc, Path src, Path dst) throws IOException {
        this.wrappedFS.copyToLocalFile(delSrc, unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public void copyToLocalFile(boolean delSrc, Path src, Path dst, boolean useRawLocalFileSystem)
            throws IOException {
        this.wrappedFS.copyToLocalFile(delSrc, unwrapPath(src), unwrapPath(dst), useRawLocalFileSystem);
    }

    @Override
    public void copyToLocalFile(Path src, Path dst) throws IOException {
        this.wrappedFS.copyToLocalFile(unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public FSDataOutputStream create(Path f) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, boolean overwrite) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, overwrite), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, overwrite);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, overwrite, bufferSize), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, overwrite, bufferSize);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize, Progressable progress)
            throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(
                    this.wrappedFS.create(unwrappedPath, overwrite, bufferSize, progress), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, overwrite, bufferSize, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize, short replication, long blockSize)
            throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(
                    this.wrappedFS.create(unwrappedPath, overwrite, bufferSize, replication, blockSize), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, overwrite, bufferSize, replication, blockSize);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize, short replication, long blockSize,
            Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(
                    this.wrappedFS.create(unwrappedPath, overwrite, bufferSize, replication, blockSize, progress),
                    f, LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, overwrite, bufferSize, replication, blockSize, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, FsPermission permission, boolean overwrite, int bufferSize,
            short replication, long blockSize, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, permission, overwrite,
                    bufferSize, replication, blockSize, progress), f, LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, permission, overwrite, bufferSize, replication, blockSize,
                    progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, FsPermission permission, EnumSet<CreateFlag> flags, int bufferSize,
            short replication, long blockSize, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, permission, flags,
                    bufferSize, replication, blockSize, progress), f, LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, permission, flags, bufferSize, replication, blockSize,
                    progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, FsPermission permission, EnumSet<CreateFlag> flags, int bufferSize,
            short replication, long blockSize, Progressable progress, ChecksumOpt checksumOpt) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, permission, flags,
                    bufferSize, replication, blockSize, progress, checksumOpt), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, permission, flags, bufferSize, replication, blockSize,
                    progress, checksumOpt);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, progress), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, short replication) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, replication), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, replication);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataOutputStream create(Path f, short replication, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.create(unwrappedPath, replication, progress), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.create(unwrappedPath, replication, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public boolean createNewFile(Path f) throws IOException {
        return this.wrappedFS.createNewFile(unwrapPath(f));
    }

    @Override
    @Deprecated
    public FSDataOutputStream createNonRecursive(Path f, boolean overwrite, int bufferSize, short replication,
            long blockSize, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.createNonRecursive(unwrappedPath, overwrite,
                    bufferSize, replication, blockSize, progress), f, LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.createNonRecursive(unwrappedPath, overwrite, bufferSize, replication, blockSize,
                    progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    @Deprecated
    public FSDataOutputStream createNonRecursive(Path f, FsPermission permission, boolean overwrite, int bufferSize,
            short replication, long blockSize, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.createNonRecursive(unwrappedPath, permission,
                    overwrite, bufferSize, replication, blockSize, progress), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.createNonRecursive(unwrappedPath, permission, overwrite, bufferSize,
                    replication, blockSize, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    @Deprecated
    public FSDataOutputStream createNonRecursive(Path f, FsPermission permission, EnumSet<CreateFlag> flags,
            int bufferSize, short replication, long blockSize, Progressable progress) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataOutputStream stream;
        if (!this.skipWrite) {
            stream = new WrappedFSDataOutputStream(this.wrappedFS.createNonRecursive(unwrappedPath, permission,
                    flags, bufferSize, replication, blockSize, progress), f,
                    LiveOperationStatisticsAggregator.instance);
        } else {
            stream = this.wrappedFS.createNonRecursive(unwrappedPath, permission, flags, bufferSize, replication,
                    blockSize, progress);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public Path createSnapshot(Path path, String snapshotName) throws IOException {
        UnwrappedPath unwrappedPath = unwrapPath(path);
        Path result = this.wrappedFS.createSnapshot(unwrappedPath, snapshotName);
        return unwrappedPath.isUnwrapped() ? wrapPath(result) : result;
    }

    @Override
    public void createSymlink(Path target, Path link, boolean createParent)
            throws AccessControlException, FileAlreadyExistsException, FileNotFoundException,
            ParentNotDirectoryException, UnsupportedFileSystemException, IOException {
        this.wrappedFS.createSymlink(unwrapPath(target), unwrapPath(link), createParent);
    }

    @Override
    @Deprecated
    public boolean delete(Path f) throws IOException {
        return this.wrappedFS.delete(unwrapPath(f));
    }

    @Override
    public boolean delete(Path f, boolean recursive) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        boolean result = this.wrappedFS.delete(unwrappedPath, recursive);
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return result;
    }

    @Override
    public boolean deleteOnExit(Path f) throws IOException {
        return this.wrappedFS.deleteOnExit(unwrapPath(f));
    }

    @Override
    public void deleteSnapshot(Path path, String snapshotName) throws IOException {
        this.wrappedFS.deleteSnapshot(unwrapPath(path), snapshotName);
    }

    @Override
    public boolean exists(Path path) throws IOException {
        return this.wrappedFS.exists(unwrapPath(path));
    }

    @Override
    @Deprecated
    public long getBlockSize(Path f) throws IOException {
        return this.wrappedFS.getBlockSize(unwrapPath(f));
    }

    @Override
    public String getCanonicalServiceName() {
        return this.wrappedFS.getCanonicalServiceName();
    }

    @Override
    public FileSystem[] getChildFileSystems() {
        return this.wrappedFS.getChildFileSystems();
    }

    @Override
    public ContentSummary getContentSummary(Path path) throws IOException {
        return this.wrappedFS.getContentSummary(unwrapPath(path));
    }

    @Override
    @Deprecated
    public long getDefaultBlockSize() {
        return this.wrappedFS.getDefaultBlockSize();
    }

    @Override
    public long getDefaultBlockSize(Path f) {
        return this.wrappedFS.getDefaultBlockSize(unwrapPath(f));
    }

    @Override
    @Deprecated
    public short getDefaultReplication() {
        return this.wrappedFS.getDefaultReplication();
    }

    @Override
    public short getDefaultReplication(Path path) {
        return this.wrappedFS.getDefaultReplication(unwrapPath(path));
    }

    @Override
    public Token<?> getDelegationToken(String renewer) throws IOException {
        return this.wrappedFS.getDelegationToken(renewer);
    }

    @Override
    public BlockLocation[] getFileBlockLocations(FileStatus file, long start, long len) throws IOException {
        long startTime = System.nanoTime();
        Path path = file.getPath();
        file.setPath(unwrapPath(path));
        BlockLocation[] blockLocations = this.wrappedFS.getFileBlockLocations(file, start, len);
        file.setPath(path);
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(file.getPath().toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return blockLocations;
    }

    @Override
    public BlockLocation[] getFileBlockLocations(Path p, long start, long len) throws IOException {
        long startTime = System.nanoTime();
        Path path = unwrapPath(p);
        BlockLocation[] blockLocations = this.wrappedFS.getFileBlockLocations(path, start, len);
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(p.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return blockLocations;
    }

    @Override
    public FileChecksum getFileChecksum(Path f) throws IOException {
        return this.wrappedFS.getFileChecksum(unwrapPath(f));
    }

    @Override
    public FileStatus getFileLinkStatus(Path f)
            throws AccessControlException, FileNotFoundException, UnsupportedFileSystemException, IOException {
        UnwrappedPath unwrappedPath = unwrapPath(f);
        FileStatus fileStatus = this.wrappedFS.getFileLinkStatus(unwrappedPath);
        if (unwrappedPath.isUnwrapped()) {
            fileStatus.setPath(setAuthority(wrapPath(fileStatus.getPath()), f.toUri().getAuthority()));
        }
        return fileStatus;
    }

    @Override
    public FileStatus getFileStatus(Path f) throws IOException {
        long startTime = System.nanoTime();
        UnwrappedPath unwrappedPath = unwrapPath(f);
        FileStatus fileStatus = this.wrappedFS.getFileStatus(unwrappedPath);
        if (unwrappedPath.isUnwrapped()) {
            fileStatus.setPath(setAuthority(wrapPath(fileStatus.getPath()), f.toUri().getAuthority()));
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return fileStatus;
    }

    @Override
    public Path getHomeDirectory() {
        return wrapPath(this.wrappedFS.getHomeDirectory());
    }

    @Override
    @Deprecated
    public long getLength(Path f) throws IOException {
        return this.wrappedFS.getLength(unwrapPath(f));
    }

    @Override
    public Path getLinkTarget(Path f) throws IOException {
        UnwrappedPath unwrappedPath = unwrapPath(f);
        return unwrappedPath.isUnwrapped() ? wrapPath(this.wrappedFS.getLinkTarget(unwrapPath(f)))
                : this.wrappedFS.getLinkTarget(unwrapPath(f));
    }

    @Override
    @Deprecated
    public String getName() {
        return this.wrappedFS.getName();
    }

    @Override
    @Deprecated
    public short getReplication(Path src) throws IOException {
        return this.wrappedFS.getReplication(unwrapPath(src));
    }

    @Override
    public String getScheme() {
        return "sfs";
    }

    @Override
    @Deprecated
    public FsServerDefaults getServerDefaults() throws IOException {
        return this.wrappedFS.getServerDefaults();
    }

    @Override
    public FsServerDefaults getServerDefaults(Path p) throws IOException {
        return this.wrappedFS.getServerDefaults(unwrapPath(p));
    }

    @Override
    public FsStatus getStatus() throws IOException {
        return this.wrappedFS.getStatus();
    }

    @Override
    public FsStatus getStatus(Path p) throws IOException {
        return this.wrappedFS.getStatus(unwrapPath(p));
    }

    @Override
    public URI getUri() {
        return this.fileSystemUri;
    }

    @Override
    public long getUsed() throws IOException {
        return this.wrappedFS.getUsed();
    }

    @Override
    public Path getWorkingDirectory() {
        Path f = this.wrappedFS.getWorkingDirectory();
        Path wrappedWorkingDirectory = setAuthority(wrapPath(f), this.fileSystemUri.getAuthority());
        return wrappedWorkingDirectory;
    }

    @Override
    public FileStatus[] globStatus(Path pathPattern) throws IOException {
        UnwrappedPath unwrappedPathPattern = unwrapPath(pathPattern);
        FileStatus[] fileStatuses = this.wrappedFS.globStatus(unwrappedPathPattern);
        if (fileStatuses == null) {
            return null;
        }
        if (unwrappedPathPattern.isUnwrapped()) {
            for (FileStatus fileStatus : fileStatuses) {
                fileStatus
                        .setPath(setAuthority(wrapPath(fileStatus.getPath()), pathPattern.toUri().getAuthority()));
            }
        }
        return fileStatuses;
    }

    @Override
    public FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOException {
        PathFilter wrappedFilter = new PathFilter() {
            @Override
            public boolean accept(Path path) {
                return filter.accept(unwrapPath(path));
            }
        };

        UnwrappedPath unwrappedPathPattern = unwrapPath(pathPattern);
        FileStatus[] fileStatuses = this.wrappedFS.globStatus(unwrappedPathPattern, wrappedFilter);
        if (fileStatuses == null) {
            return null;
        }
        if (unwrappedPathPattern.isUnwrapped()) {
            for (FileStatus fileStatus : fileStatuses) {
                fileStatus
                        .setPath(setAuthority(wrapPath(fileStatus.getPath()), pathPattern.toUri().getAuthority()));
            }
        }
        return fileStatuses;
    }

    @Override
    public boolean isDirectory(Path path) throws IOException {
        return this.wrappedFS.isDirectory(unwrapPath(path));
    }

    @Override
    public boolean isFile(Path path) throws IOException {
        return this.wrappedFS.isFile(unwrapPath(path));
    }

    @Override
    public RemoteIterator<Path> listCorruptFileBlocks(Path path) throws IOException {
        final UnwrappedPath unwrappedPath = unwrapPath(path);
        final RemoteIterator<Path> it = this.wrappedFS.listCorruptFileBlocks(unwrappedPath);
        return new RemoteIterator<Path>() {
            @Override
            public boolean hasNext() throws IOException {
                return it.hasNext();
            }

            @Override
            public Path next() throws IOException {
                return unwrappedPath.isUnwrapped() ? wrapPath(it.next()) : it.next();
            }
        };
    }

    @Override
    public RemoteIterator<LocatedFileStatus> listFiles(Path f, boolean recursive)
            throws FileNotFoundException, IOException {
        final UnwrappedPath unwrappedPath = unwrapPath(f);
        final RemoteIterator<LocatedFileStatus> it = this.wrappedFS.listFiles(unwrappedPath, recursive);
        return new RemoteIterator<LocatedFileStatus>() {
            @Override
            public boolean hasNext() throws IOException {
                return it.hasNext();
            }

            @Override
            public LocatedFileStatus next() throws IOException {
                LocatedFileStatus fileStatus = it.next();
                if (unwrappedPath.isUnwrapped()) {
                    fileStatus.setPath(setAuthority(wrapPath(fileStatus.getPath()), f.toUri().getAuthority()));
                }
                return fileStatus;
            }
        };
    }

    @Override
    public RemoteIterator<LocatedFileStatus> listLocatedStatus(Path f) throws FileNotFoundException, IOException {
        final UnwrappedPath unwrappedPath = unwrapPath(f);
        final RemoteIterator<LocatedFileStatus> it = this.wrappedFS.listLocatedStatus(unwrappedPath);
        return new RemoteIterator<LocatedFileStatus>() {
            @Override
            public boolean hasNext() throws IOException {
                return it.hasNext();
            }

            @Override
            public LocatedFileStatus next() throws IOException {
                LocatedFileStatus fileStatus = it.next();
                if (unwrappedPath.isUnwrapped()) {
                    fileStatus.setPath(setAuthority(wrapPath(fileStatus.getPath()), f.toUri().getAuthority()));
                }
                return fileStatus;
            }
        };
    }

    @Override
    public FileStatus[] listStatus(Path f) throws FileNotFoundException, IOException {
        long startTime = System.nanoTime();
        UnwrappedPath unwrappedPath = unwrapPath(f);
        FileStatus[] fileStatuses = this.wrappedFS.listStatus(unwrappedPath);
        if (unwrappedPath.isUnwrapped()) {
            for (FileStatus fileStatus : fileStatuses) {
                fileStatus.setPath(setAuthority(wrapPath(fileStatus.getPath()), f.toUri().getAuthority()));
            }
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return fileStatuses;
    }

    @Override
    public FileStatus[] listStatus(Path f, PathFilter filter) throws FileNotFoundException, IOException {
        long startTime = System.nanoTime();
        UnwrappedPath unwrappedPath = unwrapPath(f);
        PathFilter wrappedFilter = new PathFilter() {
            @Override
            public boolean accept(Path path) {
                return filter.accept(unwrapPath(path));
            }
        };

        FileStatus[] fileStatuses = this.wrappedFS.listStatus(unwrappedPath, wrappedFilter);
        if (unwrappedPath.isUnwrapped()) {
            for (FileStatus fileStatus : fileStatuses) {
                fileStatus.setPath(setAuthority(wrapPath(fileStatus.getPath()), f.toUri().getAuthority()));
            }
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return fileStatuses;
    }

    @Override
    public FileStatus[] listStatus(Path[] files) throws FileNotFoundException, IOException {
        UnwrappedPath[] unwrappedFiles = new UnwrappedPath[files.length];
        for (int i = 0; i < files.length; ++i) {
            unwrappedFiles[i] = unwrapPath(files[i]);
        }

        FileStatus[] fileStatuses = this.wrappedFS.listStatus(unwrappedFiles);
        for (int i = 0; i < fileStatuses.length; ++i) {
            if (unwrappedFiles[i].isUnwrapped()) {
                fileStatuses[i].setPath(
                        setAuthority(wrapPath(fileStatuses[i].getPath()), files[i].toUri().getAuthority()));
            }
        }
        return fileStatuses;
    }

    @Override
    public FileStatus[] listStatus(Path[] path, PathFilter filter) throws FileNotFoundException, IOException {
        UnwrappedPath[] unwrappedPaths = new UnwrappedPath[path.length];
        for (int i = 0; i < path.length; ++i) {
            unwrappedPaths[i] = unwrapPath(path[i]);
        }

        PathFilter wrappedFilter = new PathFilter() {
            @Override
            public boolean accept(Path p) {
                return filter.accept(unwrapPath(p));
            }
        };

        FileStatus[] fileStatuses = this.wrappedFS.listStatus(unwrappedPaths, wrappedFilter);
        for (int i = 0; i < fileStatuses.length; ++i) {
            if (unwrappedPaths[i].isUnwrapped()) {
                fileStatuses[i]
                        .setPath(setAuthority(wrapPath(fileStatuses[i].getPath()), path[i].toUri().getAuthority()));
            }
        }
        return fileStatuses;
    }

    @Override
    public Path makeQualified(Path path) {
        UnwrappedPath unwrappedPath = unwrapPath(path);
        return unwrappedPath.isUnwrapped() ? wrapPath(this.wrappedFS.makeQualified(unwrappedPath))
                : this.wrappedFS.makeQualified(unwrappedPath);
    }

    @Override
    public boolean mkdirs(Path f) throws IOException {
        return this.wrappedFS.mkdirs(unwrapPath(f));
    }

    @Override
    public boolean mkdirs(Path f, FsPermission permission) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        boolean result = this.wrappedFS.mkdirs(unwrappedPath, permission);
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return result;
    }

    @Override
    public void moveFromLocalFile(Path src, Path dst) throws IOException {
        this.wrappedFS.moveFromLocalFile(unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public void moveFromLocalFile(Path[] srcs, Path dst) throws IOException {
        Path[] unwrappedSrcs = new Path[srcs.length];
        for (int i = 0; i < srcs.length; ++i) {
            unwrappedSrcs[i] = unwrapPath(srcs[i]);
        }
        this.wrappedFS.moveFromLocalFile(unwrappedSrcs, unwrapPath(dst));
    }

    @Override
    public void moveToLocalFile(Path src, Path dst) throws IOException {
        this.wrappedFS.moveToLocalFile(unwrapPath(src), unwrapPath(dst));
    }

    @Override
    public FSDataInputStream open(Path f) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataInputStream stream;
        if (!this.skipRead) {
            stream = new FSDataInputStream(new WrappedFSDataInputStream(this.wrappedFS.open(unwrappedPath), f,
                    LiveOperationStatisticsAggregator.instance, this.skipOther));
        } else {
            stream = this.wrappedFS.open(unwrappedPath);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public FSDataInputStream open(Path f, int bufferSize) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedPath = unwrapPath(f);
        FSDataInputStream stream;
        if (!this.skipRead) {
            stream = new FSDataInputStream(
                    new WrappedFSDataInputStream(this.wrappedFS.open(unwrappedPath, bufferSize), f,
                            LiveOperationStatisticsAggregator.instance, this.skipOther));
        } else {
            stream = this.wrappedFS.open(unwrappedPath, bufferSize);
        }
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(f.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return stream;
    }

    @Override
    public boolean rename(Path src, Path dst) throws IOException {
        long startTime = System.nanoTime();
        Path unwrappedSrc = unwrapPath(src);
        Path unwrappedDst = unwrapPath(dst);
        boolean result = this.wrappedFS.rename(unwrappedSrc, unwrappedDst);
        if (!this.skipOther) {
            int fd = LiveOperationStatisticsAggregator.instance.registerFileDescriptor(src.toString());
            LiveOperationStatisticsAggregator.instance.aggregateOperationStatistics(OperationSource.SFS,
                    OperationCategory.OTHER, startTime, System.nanoTime(), fd);
        }
        return result;
    }

    @Override
    public void renameSnapshot(Path path, String snapshotOldName, String snapshotNewName) throws IOException {
        this.wrappedFS.renameSnapshot(unwrapPath(path), snapshotOldName, snapshotNewName);
    }

    @Override
    public Path resolvePath(Path p) throws IOException {
        UnwrappedPath unwrappedPath = unwrapPath(p);
        return unwrappedPath.isUnwrapped() ? wrapPath(this.wrappedFS.resolvePath(unwrappedPath))
                : this.wrappedFS.resolvePath(unwrappedPath);
    }

    @Override
    public void setOwner(Path p, String username, String groupname) throws IOException {
        this.wrappedFS.setOwner(unwrapPath(p), username, groupname);
    }

    @Override
    public void setPermission(Path p, FsPermission permission) throws IOException {
        this.wrappedFS.setPermission(unwrapPath(p), permission);
    }

    @Override
    public boolean setReplication(Path src, short replication) throws IOException {
        return this.wrappedFS.setReplication(unwrapPath(src), replication);
    }

    @Override
    public void setTimes(Path p, long mtime, long atime) throws IOException {
        this.wrappedFS.setTimes(unwrapPath(p), mtime, atime);
    }

    @Override
    public void setVerifyChecksum(boolean verifyChecksum) {
        this.wrappedFS.setVerifyChecksum(verifyChecksum);
    }

    @Override
    public void setWorkingDirectory(Path new_dir) {
        Path unwrappedPath = unwrapPath(new_dir);
        this.wrappedFS.setWorkingDirectory(unwrappedPath);
    }

    @Override
    public void setWriteChecksum(boolean writeChecksum) {
        this.wrappedFS.setWriteChecksum(writeChecksum);
    }

    @Override
    public Path startLocalOutput(Path fsOutputFile, Path tmpLocalFile) throws IOException {
        UnwrappedPath unwrappedFsOutputFile = unwrapPath(fsOutputFile);
        return unwrappedFsOutputFile.isUnwrapped()
                ? wrapPath(this.wrappedFS.startLocalOutput(unwrappedFsOutputFile, tmpLocalFile))
                : this.wrappedFS.startLocalOutput(unwrappedFsOutputFile, tmpLocalFile);
    }

    @Override
    public boolean supportsSymlinks() {
        return this.wrappedFS.supportsSymlinks();
    }

    // Helper methods.

    private static URI replaceUriScheme(URI uri, String from, String to) {
        // TODO add cache for replaced URIs? possibly useful for scenarios with
        // many metadata operations.

        String scheme = uri.getScheme();
        if (scheme != null) {
            if (scheme.equalsIgnoreCase(from)) {
                // uri has this scheme, replace it with new scheme

                // re-create the URI from scratch to avoid escaping of wanted
                // illegal characters
                StringBuilder buffer = new StringBuilder();

                buffer.append(to).append(":");

                String authority = uri.getAuthority();
                if (authority != null) {
                    buffer.append("//").append(authority);
                }

                String path = uri.getPath();
                if (path != null) {
                    buffer.append(path);
                }

                String fragment = uri.getFragment();
                if (fragment != null) {
                    buffer.append("#").append(fragment);
                }

                return URI.create(buffer.toString());
            } else if (scheme.equalsIgnoreCase(to)) {
                // uri already has the correct scheme
                if (LOG.isDebugEnabled()) {
                    LOG.debug("URI '" + uri + "' already has the correct scheme '" + to + "'.");
                }
                return null;
            } else {
                // uri has wrong scheme
                return null;
            }
        }

        // uri has no scheme
        return null;
    }

    static Path setAuthority(Path path, String authority) {
        if (authority != null) {
            URI pathUri = path.toUri();
            String query = pathUri.getQuery();
            String fragment = pathUri.getFragment();
            return new Path(URI.create(pathUri.getScheme() + "://" + authority + "/" + pathUri.getPath()
                    + (query != null ? ("?" + query) : "")) + (fragment != null ? ("#" + fragment) : ""));
        }

        return path;
    }

    UnwrappedPath unwrapPath(Path path) {
        URI unwrappedUri = replaceUriScheme(path.toUri(), getScheme(), this.wrappedFSScheme);

        // if the returned URI is null, then the path has not been unwrapped,
        // either because it has no scheme, it has the wrong scheme, or it
        // already has the correct scheme
        return unwrappedUri != null ? new UnwrappedPath(unwrappedUri, true)
                : new UnwrappedPath(path.toUri(), false);
    }

    Path wrapPath(Path path) {
        // only wrap the path if it has been unwrapped before
        return new Path(replaceUriScheme(path.toUri(), this.wrappedFSScheme, getScheme()));
    }

    private static class UnwrappedPath extends Path {
        private final boolean unwrapped;

        public UnwrappedPath(URI aUri) {
            super(aUri);
            this.unwrapped = false;
        }

        public UnwrappedPath(URI aUri, boolean unwrapped) {
            super(aUri);
            this.unwrapped = unwrapped;
        }

        public boolean isUnwrapped() {
            return this.unwrapped;
        }
    }
}