/*
 * 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.commons.vfs2.provider;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.vfs2.Capability;
import org.apache.commons.vfs2.FileContent;
import org.apache.commons.vfs2.FileContentInfoFactory;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileNotFolderException;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSelector;
import org.apache.commons.vfs2.FileSystem;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.FileUtil;
import org.apache.commons.vfs2.NameScope;
import org.apache.commons.vfs2.RandomAccessContent;
import org.apache.commons.vfs2.Selectors;
import org.apache.commons.vfs2.operations.DefaultFileOperations;
import org.apache.commons.vfs2.operations.FileOperations;
import org.apache.commons.vfs2.util.FileObjectUtils;
import org.apache.commons.vfs2.util.RandomAccessMode;

/**
 * A partial file object implementation.
 *
 * TODO - Chop this class up - move all the protected methods to several interfaces, so that structure and content can
 * be separately overridden.
 *
 * <p>
 * TODO - Check caps in methods like getChildren(), etc, and give better error messages (eg 'this file type does not
 * support listing children', vs 'this is not a folder')
 * </p>
 *
 * @param <AFS> An AbstractFileSystem subclass
 */
public abstract class AbstractFileObject<AFS extends AbstractFileSystem> implements FileObject {

    private static final String DO_GET_INPUT_STREAM_INT = "doGetInputStream(int)";

    /**
     * Same as {@link BufferedInputStream}.
     */
    public static final int DEFAULT_BUFFER_SIZE = 8192;

    private static final FileName[] EMPTY_FILE_ARRAY = {};

    private static final int INITIAL_LIST_SIZE = 5;

    /**
     * Traverses a file.
     */
    private static void traverse(final DefaultFileSelectorInfo fileInfo, final FileSelector selector,
            final boolean depthwise, final List<FileObject> selected) throws Exception {
        // Check the file itself
        final FileObject file = fileInfo.getFile();
        final int index = selected.size();

        // If the file is a folder, traverse it
        if (file.getType().hasChildren() && selector.traverseDescendents(fileInfo)) {
            final int curDepth = fileInfo.getDepth();
            fileInfo.setDepth(curDepth + 1);

            // Traverse the children
            final FileObject[] children = file.getChildren();
            for (final FileObject child : children) {
                fileInfo.setFile(child);
                traverse(fileInfo, selector, depthwise, selected);
            }

            fileInfo.setFile(file);
            fileInfo.setDepth(curDepth);
        }

        // Add the file if doing depthwise traversal
        if (selector.includeFile(fileInfo)) {
            if (depthwise) {
                // Add this file after its descendants
                selected.add(file);
            } else {
                // Add this file before its descendants
                selected.add(index, file);
            }
        }
    }
    private final AbstractFileName fileName;

    private final AFS fileSystem;
    private FileContent content;
    // Cached info
    private boolean attached;

    private FileType type;
    private FileObject parent;

    // Changed to hold only the name of the children and let the object
    // go into the global files cache
    // private FileObject[] children;
    private FileName[] children;

    private List<Object> objects;

    /**
     * FileServices instance.
     */
    private FileOperations operations;

    /**
     *
     * @param name the file name - muse be an instance of {@link AbstractFileName}
     * @param fileSystem the file system
     * @throws ClassCastException if {@code name} is not an instance of {@link AbstractFileName}
     */
    protected AbstractFileObject(final AbstractFileName name, final AFS fileSystem) {
        this.fileName = name;
        this.fileSystem = fileSystem;
        fileSystem.fileObjectHanded(this);
    }

    /**
     * Attaches to the file.
     *
     * @throws FileSystemException if an error occurs.
     */
    private void attach() throws FileSystemException {
        synchronized (fileSystem) {
            if (attached) {
                return;
            }

            try {
                // Attach and determine the file type
                doAttach();
                attached = true;
                // now the type could already be injected by doAttach (e.g from parent to child)

                /*
                 * VFS-210: determine the type when really asked fore if (type == null) { setFileType(doGetType()); } if
                 * (type == null) { setFileType(FileType.IMAGINARY); }
                 */
            } catch (final Exception exc) {
                throw new FileSystemException("vfs.provider/get-type.error", exc, fileName);
            }

            // fs.fileAttached(this);
        }
    }

    /**
     * Queries the object if a simple rename to the file name of {@code newfile} is possible.
     *
     * @param newfile the new file name
     * @return true if rename is possible
     */
    @Override
    public boolean canRenameTo(final FileObject newfile) {
        return fileSystem == newfile.getFileSystem();
    }

    /**
     * Notifies the file that its children have changed.
     *
     * @param childName The name of the child.
     * @param newType The type of the child.
     * @throws Exception if an error occurs.
     */
    protected void childrenChanged(final FileName childName, final FileType newType) throws Exception {
        // TODO - this may be called when not attached

        if (children != null && childName != null && newType != null) {
            // TODO - figure out if children[] can be replaced by list
            final ArrayList<FileName> list = new ArrayList<>(Arrays.asList(children));
            if (newType.equals(FileType.IMAGINARY)) {
                list.remove(childName);
            } else {
                list.add(childName);
            }
            children = new FileName[list.size()];
            list.toArray(children);
        }

        // removeChildrenCache();
        onChildrenChanged(childName, newType);
    }

    /**
     * Closes this file, and its content.
     *
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void close() throws FileSystemException {
        FileSystemException exc = null;

        synchronized (fileSystem) {
            // Close the content
            if (content != null) {
                try {
                    content.close();
                    content = null;
                } catch (final FileSystemException e) {
                    exc = e;
                }
            }

            // Detach from the file
            try {
                detach();
            } catch (final Exception e) {
                exc = new FileSystemException("vfs.provider/close.error", fileName, e);
            }

            if (exc != null) {
                throw exc;
            }
        }
    }

    /**
     * Compares two FileObjects (ignores case).
     *
     * @param file the object to compare.
     * @return a negative integer, zero, or a positive integer when this object is less than, equal to, or greater than
     *         the given object.
     */
    @Override
    public int compareTo(final FileObject file) {
        if (file == null) {
            return 1;
        }
        return this.toString().compareToIgnoreCase(file.toString());
    }

    /**
     * Copies another file to this file.
     *
     * @param file The FileObject to copy.
     * @param selector The FileSelector.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void copyFrom(final FileObject file, final FileSelector selector) throws FileSystemException {
        if (!FileObjectUtils.exists(file)) {
            throw new FileSystemException("vfs.provider/copy-missing-file.error", file);
        }

        // Locate the files to copy across
        final ArrayList<FileObject> files = new ArrayList<>();
        file.findFiles(selector, false, files);

        // Copy everything across
        for (final FileObject srcFile : files) {
            // Determine the destination file
            final String relPath = file.getName().getRelativeName(srcFile.getName());
            final FileObject destFile = resolveFile(relPath, NameScope.DESCENDENT_OR_SELF);

            // Clean up the destination file, if necessary
            if (FileObjectUtils.exists(destFile) && destFile.getType() != srcFile.getType()) {
                // The destination file exists, and is not of the same type,
                // so delete it
                // TODO - add a pluggable policy for deleting and overwriting existing files
                destFile.deleteAll();
            }

            // Copy across
            try {
                if (srcFile.getType().hasContent()) {
                    FileUtil.copyContent(srcFile, destFile);
                } else if (srcFile.getType().hasChildren()) {
                    destFile.createFolder();
                }
            } catch (final IOException e) {
                throw new FileSystemException("vfs.provider/copy-file.error", e, srcFile, destFile);
            }
        }
    }

    /**
     * Creates this file, if it does not exist.
     *
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void createFile() throws FileSystemException {
        synchronized (fileSystem) {
            try {
                // VFS-210: We do not want to trunc any existing file, checking for its existence is
                // still required
                if (exists() && !isFile()) {
                    throw new FileSystemException("vfs.provider/create-file.error", fileName);
                }

                if (!exists()) {
                    getOutputStream().close();
                    endOutput();
                }
            } catch (final RuntimeException re) {
                throw re;
            } catch (final Exception e) {
                throw new FileSystemException("vfs.provider/create-file.error", fileName, e);
            }
        }
    }

    /**
     * Creates this folder, if it does not exist. Also creates any ancestor files which do not exist.
     *
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void createFolder() throws FileSystemException {
        synchronized (fileSystem) {
            // VFS-210: we create a folder only if it does not already exist. So this check should be safe.
            if (getType().hasChildren()) {
                // Already exists as correct type
                return;
            }
            if (getType() != FileType.IMAGINARY) {
                throw new FileSystemException("vfs.provider/create-folder-mismatched-type.error", fileName);
            }
            /*
             * VFS-210: checking for writeable is not always possible as the security constraint might be more complex
             * if (!isWriteable()) { throw new FileSystemException("vfs.provider/create-folder-read-only.error", name);
             * }
             */

            // Traverse up the hierarchy and make sure everything is a folder
            final FileObject parent = getParent();
            if (parent != null) {
                parent.createFolder();
            }

            try {
                // Create the folder
                doCreateFolder();

                // Update cached info
                handleCreate(FileType.FOLDER);
            } catch (final RuntimeException re) {
                throw re;
            } catch (final Exception exc) {
                throw new FileSystemException("vfs.provider/create-folder.error", fileName, exc);
            }
        }
    }

    /**
     * Deletes this file.
     * <p>
     * TODO - This will not fail if this is a non-empty folder.
     * </p>
     *
     * @return true if this object has been deleted
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public boolean delete() throws FileSystemException {
        return delete(Selectors.SELECT_SELF) > 0;
    }

    /**
     * Deletes this file, and all children matching the {@code selector}.
     *
     * @param selector The FileSelector.
     * @return the number of deleted files.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public int delete(final FileSelector selector) throws FileSystemException {
        int nuofDeleted = 0;

        /*
         * VFS-210 if (getType() == FileType.IMAGINARY) { // File does not exist return nuofDeleted; }
         */

        // Locate all the files to delete
        final ArrayList<FileObject> files = new ArrayList<>();
        findFiles(selector, true, files);

        // Delete 'em
        final int count = files.size();
        for (int i = 0; i < count; i++) {
            final AbstractFileObject file = FileObjectUtils.getAbstractFileObject(files.get(i));
            // file.attach();

            // VFS-210: It seems impossible to me that findFiles will return a list with hidden files/directories
            // in it, else it would not be hidden. Checking for the file-type seems ok in this case
            // If the file is a folder, make sure all its children have been deleted
            if (file.getType().hasChildren() && file.getChildren().length != 0) {
                // Skip - as the selector forced us not to delete all files
                continue;
            }

            // Delete the file
            if (file.deleteSelf()) {
                nuofDeleted++;
            }
        }

        return nuofDeleted;
    }

    /**
     * Deletes this file and all children. Shorthand for {@code delete(Selectors.SELECT_ALL)}
     *
     * @return the number of deleted files.
     * @throws FileSystemException if an error occurs.
     * @see #delete(FileSelector)
     * @see Selectors#SELECT_ALL
     */
    @Override
    public int deleteAll() throws FileSystemException {
        return this.delete(Selectors.SELECT_ALL);
    }

    /**
     * Deletes this file, once all its children have been deleted
     *
     * @return true if this file has been deleted
     * @throws FileSystemException if an error occurs.
     */
    private boolean deleteSelf() throws FileSystemException {
        synchronized (fileSystem) {
            // Its possible to delete a read-only file if you have write-execute access to the directory

            /*
             * VFS-210 if (getType() == FileType.IMAGINARY) { // File does not exist return false; }
             */

            try {
                // Delete the file
                doDelete();

                // Update cached info
                handleDelete();
            } catch (final RuntimeException re) {
                throw re;
            } catch (final Exception exc) {
                throw new FileSystemException("vfs.provider/delete.error", exc, fileName);
            }

            return true;
        }
    }

    /**
     * Detaches this file, invalidating all cached info. This will force a call to {@link #doAttach} next time this file
     * is used.
     *
     * @throws Exception if an error occurs.
     */
    private void detach() throws Exception {
        synchronized (fileSystem) {
            if (attached) {
                try {
                    doDetach();
                } finally {
                    attached = false;
                    setFileType(null);
                    parent = null;

                    // fs.fileDetached(this);

                    removeChildrenCache();
                    // children = null;
                }
            }
        }
    }

    /**
     * Attaches this file object to its file resource.
     * <p>
     * This method is called before any of the doBlah() or onBlah() methods. Sub-classes can use this method to perform
     * lazy initialization.
     * </p>
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @throws Exception if an error occurs.
     */
    protected void doAttach() throws Exception {
        // noop
    }

    /**
     * Create a FileContent implementation.
     *
     * @return The FileContent.
     * @throws FileSystemException if an error occurs.
     * @since 2.0
     */
    protected FileContent doCreateFileContent() throws FileSystemException {
        return new DefaultFileContent(this, getFileContentInfoFactory());
    }

    /**
     * Creates this file as a folder. Is only called when:
     * <ul>
     * <li>{@link #doGetType} returns {@link FileType#IMAGINARY}.</li>
     * <li>The parent folder exists and is writeable, or this file is the root of the file system.</li>
     * </ul>
     * This implementation throws an exception.
     *
     * @throws Exception if an error occurs.
     */
    protected void doCreateFolder() throws Exception {
        throw new FileSystemException("vfs.provider/create-folder-not-supported.error");
    }

    /**
     * Deletes the file. Is only called when:
     * <ul>
     * <li>{@link #doGetType} does not return {@link FileType#IMAGINARY}.</li>
     * <li>{@link #doIsWriteable} returns true.</li>
     * <li>This file has no children, if a folder.</li>
     * </ul>
     * This implementation throws an exception.
     *
     * @throws Exception if an error occurs.
     */
    protected void doDelete() throws Exception {
        throw new FileSystemException("vfs.provider/delete-not-supported.error");
    }

    /**
     * Detaches this file object from its file resource.
     * <p>
     * Called when this file is closed. Note that the file object may be reused later, so should be able to be
     * reattached.
     * </p>
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @throws Exception if an error occurs.
     */
    protected void doDetach() throws Exception {
        // noop
    }

    /**
     * Returns the attributes of this file. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns an empty map.
     * </p>
     *
     * @return The attributes of the file.
     * @throws Exception if an error occurs.
     */
    protected Map<String, Object> doGetAttributes() throws Exception {
        return Collections.emptyMap();
    }

    /**
     * Returns the certificates used to sign this file. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns null.
     * </p>
     *
     * @return The certificates used to sign the file.
     * @throws Exception if an error occurs.
     */
    protected Certificate[] doGetCertificates() throws Exception {
        return null;
    }

    /**
     * Returns the size of the file content (in bytes). Is only called if {@link #doGetType} returns
     * {@link FileType#FILE}.
     *
     * @return The size of the file in bytes.
     * @throws Exception if an error occurs.
     */
    protected abstract long doGetContentSize() throws Exception;

    /**
     * Creates an input stream to read the file content from. Is only called if {@link #doGetType} returns
     * {@link FileType#FILE}.
     * <p>
     * It is guaranteed that there are no open output streams for this file when this method is called.
     * </p>
     * <p>
     * The returned stream does not have to be buffered.
     * </p>
     *
     * @return An InputStream to read the file content.
     * @throws Exception if an error occurs.
     */
    protected InputStream doGetInputStream() throws Exception {
        // Backward compatibility.
        return doGetInputStream(DEFAULT_BUFFER_SIZE);
    }

    /**
     * Creates an input stream to read the file content from. Is only called if {@link #doGetType} returns
     * {@link FileType#FILE}.
     * <p>
     * It is guaranteed that there are no open output streams for this file when this method is called.
     * </p>
     * <p>
     * The returned stream does not have to be buffered.
     * </p>
     * @param bufferSize Buffer size hint.
     * @return An InputStream to read the file content.
     * @throws Exception if an error occurs.
     */
    protected InputStream doGetInputStream(final int bufferSize) throws Exception {
        throw new UnsupportedOperationException(DO_GET_INPUT_STREAM_INT);
    }

    /**
     * Returns the last modified time of this file. Is only called if {@link #doGetType} does not return
     * <p>
     * This implementation throws an exception.
     * </p>
     *
     * @return The last modification time.
     * @throws Exception if an error occurs.
     */
    protected long doGetLastModifiedTime() throws Exception {
        throw new FileSystemException("vfs.provider/get-last-modified-not-supported.error");
    }

    /**
     * Creates an output stream to write the file content to. Is only called if:
     * <ul>
     * <li>{@link #doIsWriteable} returns true.
     * <li>{@link #doGetType} returns {@link FileType#FILE}, or {@link #doGetType} returns {@link FileType#IMAGINARY},
     * and the file's parent exists and is a folder.
     * </ul>
     * It is guaranteed that there are no open stream (input or output) for this file when this method is called.
     * <p>
     * The returned stream does not have to be buffered.
     * </p>
     * <p>
     * This implementation throws an exception.
     * </p>
     *
     * @param bAppend true if the file should be appended to, false if it should be overwritten.
     * @return An OutputStream to write to the file.
     * @throws Exception if an error occurs.
     */
    protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception {
        throw new FileSystemException("vfs.provider/write-not-supported.error");
    }

    /**
     * Creates access to the file for random i/o. Is only called if {@link #doGetType} returns {@link FileType#FILE}.
     * <p>
     * It is guaranteed that there are no open output streams for this file when this method is called.
     * </p>
     *
     * @param mode The mode to access the file.
     * @return The RandomAccessContext.
     * @throws Exception if an error occurs.
     */
    protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception {
        throw new FileSystemException("vfs.provider/random-access-not-supported.error");
    }

    /**
     * Determines the type of this file. Must not return null. The return value of this method is cached, so the
     * implementation can be expensive.
     *
     * @return the type of the file.
     * @throws Exception if an error occurs.
     */
    protected abstract FileType doGetType() throws Exception;

    /**
     * Determines if this file is executable. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns false.
     * </p>
     *
     * @return true if the file is executable, false otherwise.
     * @throws Exception if an error occurs.
     */
    protected boolean doIsExecutable() throws Exception {
        return false;
    }

    /**
     * Determines if this file is hidden. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns false.
     * </p>
     *
     * @return true if the file is hidden, false otherwise.
     * @throws Exception if an error occurs.
     */
    protected boolean doIsHidden() throws Exception {
        return false;
    }

    /**
     * Determines if this file can be read. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns true.
     * </p>
     *
     * @return true if the file is readable, false otherwise.
     * @throws Exception if an error occurs.
     */
    protected boolean doIsReadable() throws Exception {
        return true;
    }

    /**
     * Checks if this fileObject is the same file as {@code destFile} just with a different name. E.g. for case
     * insensitive file systems like windows.
     *
     * @param destFile The file to compare to.
     * @return true if the FileObjects are the same.
     * @throws FileSystemException if an error occurs.
     */
    protected boolean doIsSameFile(final FileObject destFile) throws FileSystemException {
        return false;
    }

    /**
     * Determines if this file is a symbolic link. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns false.
     * </p>
     *
     * @return true if the file is readable, false otherwise.
     * @throws Exception if an error occurs.
     * @since 2.4
     */
    protected boolean doIsSymbolicLink() throws Exception {
        return false;
    }

    /**
     * Determines if this file can be written to. Is only called if {@link #doGetType} does not return
     * {@link FileType#IMAGINARY}.
     * <p>
     * This implementation always returns true.
     * </p>
     *
     * @return true if the file is writable.
     * @throws Exception if an error occurs.
     */
    protected boolean doIsWriteable() throws Exception {
        return true;
    }

    /**
     * Lists the children of this file. Is only called if {@link #doGetType} returns {@link FileType#FOLDER}. The return
     * value of this method is cached, so the implementation can be expensive.
     *
     * @return a possible empty String array if the file is a directory or null or an exception if the file is not a
     *         directory or can't be read.
     * @throws Exception if an error occurs.
     */
    protected abstract String[] doListChildren() throws Exception;

    /**
     * Lists the children of this file.
     * <p>
     * Is only called if {@link #doGetType} returns {@link FileType#FOLDER}.
     * </p>
     * <p>
     * The return value of this method is cached, so the implementation can be expensive.
     * Other than {@code doListChildren} you could return FileObject's to e.g. reinitialize the type of the file.
     * </p>
     * <p>
     * (Introduced for Webdav: "permission denied on resource" during getType())
     * </p>
     *
     * @return The children of this FileObject.
     * @throws Exception if an error occurs.
     */
    protected FileObject[] doListChildrenResolved() throws Exception {
        return null;
    }

    /**
     * Removes an attribute of this file.
     * <p>
     * Is only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
     * </p>
     * <p>
     * This implementation throws an exception.
     * </p>
     *
     * @param attrName The name of the attribute to remove.
     * @throws Exception if an error occurs.
     * @since 2.0
     */
    protected void doRemoveAttribute(final String attrName) throws Exception {
        throw new FileSystemException("vfs.provider/remove-attribute-not-supported.error");
    }

    /**
     * Renames the file.
     * <p>
     * Is only called when:
     * </p>
     * <ul>
     * <li>{@link #doIsWriteable} returns true.</li>
     * </ul>
     * <p>
     * This implementation throws an exception.
     * </p>
     *
     * @param newFile A FileObject with the new file name.
     * @throws Exception if an error occurs.
     */
    protected void doRename(final FileObject newFile) throws Exception {
        throw new FileSystemException("vfs.provider/rename-not-supported.error");
    }

    /**
     * Sets an attribute of this file.
     * <p>
     * Is only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
     * </p>
     * <p>
     * This implementation throws an exception.
     * </p>
     *
     * @param attrName The attribute name.
     * @param value The value to be associated with the attribute name.
     * @throws Exception if an error occurs.
     */
    protected void doSetAttribute(final String attrName, final Object value) throws Exception {
        throw new FileSystemException("vfs.provider/set-attribute-not-supported.error");
    }

    /**
     * Make the file executable.
     * <p>
     * Only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
     * </p>
     * <p>
     * This implementation returns false.
     * </p>
     *
     * @param executable True to allow access, false to disallow.
     * @param ownerOnly If {@code true}, the permission applies only to the owner; otherwise, it applies to everybody.
     * @return true if the operation succeeded.
     * @throws Exception Any Exception thrown is wrapped in FileSystemException.
     * @see #setExecutable(boolean, boolean)
     * @since 2.1
     */
    protected boolean doSetExecutable(final boolean executable, final boolean ownerOnly) throws Exception {
        return false;
    }

    /**
     * Sets the last modified time of this file.
     * <p>
     * Is only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
     * </p>
     * <p>
     * This implementation throws an exception.
     * </p>
     *
     * @param modtime The last modification time.
     * @return true if the time was set.
     * @throws Exception Any Exception thrown is wrapped in FileSystemException.
     */
    protected boolean doSetLastModifiedTime(final long modtime) throws Exception {
        throw new FileSystemException("vfs.provider/set-last-modified-not-supported.error");
    }

    /**
     * Make the file or folder readable.
     * <p>
     * Only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
     * </p>
     * <p>
     * This implementation returns false.
     * </p>
     *
     * @param readable True to allow access, false to disallow
     * @param ownerOnly If {@code true}, the permission applies only to the owner; otherwise, it applies to everybody.
     * @return true if the operation succeeded
     * @throws Exception Any Exception thrown is wrapped in FileSystemException.
     * @see #setReadable(boolean, boolean)
     * @since 2.1
     */
    protected boolean doSetReadable(final boolean readable, final boolean ownerOnly) throws Exception {
        return false;
    }

    /**
     * Make the file or folder writeable.
     * <p>
     * Only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
     * </p>
     *
     * @param writable True to allow access, false to disallow
     * @param ownerOnly If {@code true}, the permission applies only to the owner; otherwise, it applies to everybody.
     * @return true if the operation succeeded
     * @throws Exception Any Exception thrown is wrapped in FileSystemException.
     * @see #setWritable(boolean, boolean)
     * @since 2.1
     */
    protected boolean doSetWritable(final boolean writable, final boolean ownerOnly) throws Exception {
        return false;
    }

    /**
     * Called when the output stream for this file is closed.
     *
     * @throws Exception if an error occurs.
     */
    protected void endOutput() throws Exception {
        if (getType() == FileType.IMAGINARY) {
            // File was created
            handleCreate(FileType.FILE);
        } else {
            // File has changed
            onChange();
        }
    }

    /**
     * Determines if the file exists.
     *
     * @return true if the file exists, false otherwise,
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public boolean exists() throws FileSystemException {
        return getType() != FileType.IMAGINARY;
    }

    private FileName[] extractNames(final FileObject[] objects) {
        if (objects == null) {
            return null;
        }

        final FileName[] names = new FileName[objects.length];
        for (int iterObjects = 0; iterObjects < objects.length; iterObjects++) {
            names[iterObjects] = objects[iterObjects].getName();
        }

        return names;
    }

    @Override
    protected void finalize() throws Throwable {
        fileSystem.fileObjectDestroyed(this);

        super.finalize();
    }

    /**
     * Finds the set of matching descendants of this file, in depthwise order.
     *
     * @param selector The FileSelector.
     * @return list of files or null if the base file (this object) do not exist
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileObject[] findFiles(final FileSelector selector) throws FileSystemException {
        final List<FileObject> list = this.listFiles(selector);
        return list == null ? null : list.toArray(new FileObject[list.size()]);
    }

    /**
     * Traverses the descendants of this file, and builds a list of selected files.
     *
     * @param selector The FileSelector.
     * @param depthwise if true files are added after their descendants, before otherwise.
     * @param selected A List of the located FileObjects.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void findFiles(final FileSelector selector, final boolean depthwise, final List<FileObject> selected)
            throws FileSystemException {
        try {
            if (exists()) {
                // Traverse starting at this file
                final DefaultFileSelectorInfo info = new DefaultFileSelectorInfo();
                info.setBaseFolder(this);
                info.setDepth(0);
                info.setFile(this);
                traverse(info, selector, depthwise, selected);
            }
        } catch (final Exception e) {
            throw new FileSystemException("vfs.provider/find-files.error", fileName, e);
        }
    }

    /**
     * Returns the file system this file belongs to.
     *
     * @return The FileSystem this file is associated with.
     */
    protected AFS getAbstractFileSystem() {
        return fileSystem;
    }

    /**
     * Returns a child of this file.
     *
     * @param name The name of the child to locate.
     * @return The FileObject for the file or null if the child does not exist.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileObject getChild(final String name) throws FileSystemException {
        // TODO - use a hashtable when there are a large number of children
        final FileObject[] children = getChildren();
        for (final FileObject element : children) {
            final FileName child = element.getName();
            // TODO - use a comparator to compare names
            if (child.getBaseName().equals(name)) {
                return resolveFile(child);
            }
        }
        return null;
    }

    /**
     * Returns the children of the file.
     *
     * @return an array of FileObjects, one per child.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileObject[] getChildren() throws FileSystemException {
        synchronized (fileSystem) {
            // VFS-210
            if (!fileSystem.hasCapability(Capability.LIST_CHILDREN)) {
                throw new FileNotFolderException(fileName);
            }

            /*
             * VFS-210 if (!getType().hasChildren()) { throw new
             * FileSystemException("vfs.provider/list-children-not-folder.error", name); }
             */
            attach();

            // Use cached info, if present
            if (children != null) {
                return resolveFiles(children);
            }

            // allow the filesystem to return resolved children. e.g. prefill type for webdav
            FileObject[] childrenObjects;
            try {
                childrenObjects = doListChildrenResolved();
                children = extractNames(childrenObjects);
            } catch (final FileSystemException exc) {
                // VFS-210
                throw exc;
            } catch (final Exception exc) {
                throw new FileSystemException("vfs.provider/list-children.error", exc, fileName);
            }

            if (childrenObjects != null) {
                return childrenObjects;
            }

            // List the children
            final String[] files;
            try {
                files = doListChildren();
            } catch (final FileSystemException exc) {
                // VFS-210
                throw exc;
            } catch (final Exception exc) {
                throw new FileSystemException("vfs.provider/list-children.error", exc, fileName);
            }

            if (files == null) {
                // VFS-210
                // honor the new doListChildren contract
                // return null;
                throw new FileNotFolderException(fileName);
            } else if (files.length == 0) {
                // No children
                children = EMPTY_FILE_ARRAY;
            } else {
                // Create file objects for the children
                final FileName[] cache = new FileName[files.length];
                for (int i = 0; i < files.length; i++) {
                    final String file = "./" + files[i]; // VFS-741: assume scheme prefix is filename only
                    cache[i] = fileSystem.getFileSystemManager().resolveName(fileName, file, NameScope.CHILD);
                }
                // VFS-285: only assign the children file names after all of them have been
                // resolved successfully to prevent an inconsistent internal state
                children = cache;
            }

            return resolveFiles(children);
        }
    }

    /**
     * Returns the file's content.
     *
     * @return the FileContent for this FileObject.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileContent getContent() throws FileSystemException {
        synchronized (fileSystem) {
            attach();
            if (content == null) {
                content = doCreateFileContent();
            }
            return content;
        }
    }

    /**
     * Creates the FileContentInfo factory.
     *
     * @return The FileContentInfoFactory.
     */
    protected FileContentInfoFactory getFileContentInfoFactory() {
        return fileSystem.getFileSystemManager().getFileContentInfoFactory();
    }

    /**
     * @return FileOperations interface that provides access to the operations API.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileOperations getFileOperations() throws FileSystemException {
        if (operations == null) {
            operations = new DefaultFileOperations(this);
        }

        return operations;
    }

    /**
     * Returns the file system this file belongs to.
     *
     * @return The FileSystem this file is associated with.
     */
    @Override
    public FileSystem getFileSystem() {
        return fileSystem;
    }

    /**
     * Returns an input stream to use to read the content of the file.
     *
     * @return The InputStream to access this file's content.
     * @throws FileSystemException if an error occurs.
     */
    public InputStream getInputStream() throws FileSystemException {
        return getInputStream(DEFAULT_BUFFER_SIZE);
    }

    /**
     * Returns an input stream to use to read the content of the file.
     *
     * @param bufferSize buffer size hint.
     * @return The InputStream to access this file's content.
     * @throws FileSystemException if an error occurs.
     */
    public InputStream getInputStream(final int bufferSize) throws FileSystemException {
        // Get the raw input stream
        try {
            return doGetInputStream(bufferSize);
        } catch (final org.apache.commons.vfs2.FileNotFoundException exc) {
            throw new org.apache.commons.vfs2.FileNotFoundException(fileName, exc);
        } catch (final FileNotFoundException exc) {
            throw new org.apache.commons.vfs2.FileNotFoundException(fileName, exc);
        } catch (final FileSystemException exc) {
            throw exc;
        } catch (final UnsupportedOperationException exc) {
            // Backward compatibility for subclasses before 2.5.0
            if (DO_GET_INPUT_STREAM_INT.equals(exc.getMessage())) {
                try {
                    // Invoke old API.
                    return doGetInputStream();
                } catch (Exception e) {
                    if (e instanceof FileSystemException) {
                        throw (FileSystemException) e;
                    }
                    throw new FileSystemException("vfs.provider/read.error", fileName, exc);
                }
            }
            throw exc;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/read.error", fileName, exc);
        }
    }

    /**
     * Returns the name of the file.
     *
     * @return The FileName, never {@code null}.
     */
    @Override
    public FileName getName() {
        return fileName;
    }

    /**
     * Prepares this file for writing. Makes sure it is either a file, or its parent folder exists. Returns an output
     * stream to use to write the content of the file to.
     *
     * @return An OutputStream where the new contents of the file can be written.
     * @throws FileSystemException if an error occurs.
     */
    public OutputStream getOutputStream() throws FileSystemException {
        return getOutputStream(false);
    }

    /**
     * Prepares this file for writing. Makes sure it is either a file, or its parent folder exists. Returns an output
     * stream to use to write the content of the file to.
     *
     * @param bAppend true when append to the file.
     *            Note: If the underlying file system does not support appending, a FileSystemException is thrown.
     * @return An OutputStream where the new contents of the file can be written.
     * @throws FileSystemException if an error occurs; for example:
     *             bAppend is true, and the underlying FileSystem does not support it
     */
    public OutputStream getOutputStream(final boolean bAppend) throws FileSystemException {
        /*
         * VFS-210 if (getType() != FileType.IMAGINARY && !getType().hasContent()) { throw new
         * FileSystemException("vfs.provider/write-not-file.error", name); } if (!isWriteable()) { throw new
         * FileSystemException("vfs.provider/write-read-only.error", name); }
         */

        if (bAppend && !fileSystem.hasCapability(Capability.APPEND_CONTENT)) {
            throw new FileSystemException("vfs.provider/write-append-not-supported.error", fileName);
        }

        if (getType() == FileType.IMAGINARY) {
            // Does not exist - make sure parent does
            final FileObject parent = getParent();
            if (parent != null) {
                parent.createFolder();
            }
        }

        // Get the raw output stream
        try {
            return doGetOutputStream(bAppend);
        } catch (final RuntimeException re) {
            throw re;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/write.error", exc, fileName);
        }
    }

    /**
     * Returns the parent of the file.
     *
     * @return the parent FileObject.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileObject getParent() throws FileSystemException {
        if (this.compareTo(fileSystem.getRoot()) == 0) // equals is not implemented :-/
        {
            if (fileSystem.getParentLayer() == null) {
                // Root file has no parent
                return null;
            }
            // Return the parent of the parent layer
            return fileSystem.getParentLayer().getParent();
        }

        synchronized (fileSystem) {
            // Locate the parent of this file
            if (parent == null) {
                final FileName name = fileName.getParent();
                if (name == null) {
                    return null;
                }
                parent = fileSystem.resolveFile(name);
            }
            return parent;
        }
    }

    /**
     * Returns the receiver as a URI String for public display, like, without a password.
     *
     * @return A URI String without a password, never {@code null}.
     */
    @Override
    public String getPublicURIString() {
        return fileName.getFriendlyURI();
    }

    /**
     * Returns an input/output stream to use to read and write the content of the file in and random manner.
     *
     * @param mode The RandomAccessMode.
     * @return The RandomAccessContent.
     * @throws FileSystemException if an error occurs.
     */
    public RandomAccessContent getRandomAccessContent(final RandomAccessMode mode) throws FileSystemException {
        /*
         * VFS-210 if (!getType().hasContent()) { throw new FileSystemException("vfs.provider/read-not-file.error",
         * name); }
         */

        if (mode.requestRead()) {
            if (!fileSystem.hasCapability(Capability.RANDOM_ACCESS_READ)) {
                throw new FileSystemException("vfs.provider/random-access-read-not-supported.error");
            }
            if (!isReadable()) {
                throw new FileSystemException("vfs.provider/read-not-readable.error", fileName);
            }
        }

        if (mode.requestWrite()) {
            if (!fileSystem.hasCapability(Capability.RANDOM_ACCESS_WRITE)) {
                throw new FileSystemException("vfs.provider/random-access-write-not-supported.error");
            }
            if (!isWriteable()) {
                throw new FileSystemException("vfs.provider/write-read-only.error", fileName);
            }
        }

        // Get the raw input stream
        try {
            return doGetRandomAccessContent(mode);
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/random-access.error", fileName, exc);
        }
    }

    /**
     * Returns the file's type.
     *
     * @return The FileType.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileType getType() throws FileSystemException {
        synchronized (fileSystem) {
            attach();

            // VFS-210: get the type only if requested for
            try {
                if (type == null) {
                    setFileType(doGetType());
                }
                if (type == null) {
                    setFileType(FileType.IMAGINARY);
                }
            } catch (final Exception e) {
                throw new FileSystemException("vfs.provider/get-type.error", e, fileName);
            }

            return type;
        }
    }

    /**
     * Returns a URL representation of the file.
     *
     * @return The URL representation of the file.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public URL getURL() throws FileSystemException {
        try {
            return AccessController.doPrivileged((PrivilegedExceptionAction<URL>) () -> {
                final StringBuilder buf = new StringBuilder();
                final String scheme = UriParser.extractScheme(fileSystem.getContext().getFileSystemManager().getSchemes(), fileName.getURI(), buf);
                return new URL(scheme, "", -1, buf.toString(),
                        new DefaultURLStreamHandler(fileSystem.getContext(), fileSystem.getFileSystemOptions()));
            });
        } catch (final PrivilegedActionException e) {
            throw new FileSystemException("vfs.provider/get-url.error", fileName, e.getException());
        }
    }

    /**
     * Called when this file is changed.
     * <p>
     * This will only happen if you monitor the file using {@link org.apache.commons.vfs2.FileMonitor}.
     * </p>
     *
     * @throws Exception if an error occurs.
     */
    protected void handleChanged() throws Exception {
        // Notify the file system
        fileSystem.fireFileChanged(this);
    }

    /**
     * Called when this file is created. Updates cached info and notifies the parent and file system.
     *
     * @param newType The type of the file.
     * @throws Exception if an error occurs.
     */
    protected void handleCreate(final FileType newType) throws Exception {
        synchronized (fileSystem) {
            if (attached) {
                // Fix up state
                injectType(newType);

                removeChildrenCache();

                // Notify subclass
                onChange();
            }

            // Notify parent that its child list may no longer be valid
            notifyParent(this.getName(), newType);

            // Notify the file system
            fileSystem.fireFileCreated(this);
        }
    }

    /**
     * Called when this file is deleted. Updates cached info and notifies subclasses, parent and file system.
     *
     * @throws Exception if an error occurs.
     */
    protected void handleDelete() throws Exception {
        synchronized (fileSystem) {
            if (attached) {
                // Fix up state
                injectType(FileType.IMAGINARY);
                removeChildrenCache();

                // Notify subclass
                onChange();
            }

            // Notify parent that its child list may no longer be valid
            notifyParent(this.getName(), FileType.IMAGINARY);

            // Notify the file system
            fileSystem.fireFileDeleted(this);
        }
    }

    /**
     * This method is meant to add an object where this object holds a strong reference then. E.g. a archive-file system
     * creates a list of all children and they shouldn't get garbage collected until the container is garbage collected
     *
     * @param strongRef The Object to add.
     */
    // TODO should this be a FileObject?
    public void holdObject(final Object strongRef) {
        if (objects == null) {
            objects = new ArrayList<>(INITIAL_LIST_SIZE);
        }
        objects.add(strongRef);
    }

    protected void injectType(final FileType fileType) {
        setFileType(fileType);
    }

    /**
     * Check if the internal state is "attached".
     *
     * @return true if this is the case
     */
    @Override
    public boolean isAttached() {
        return attached;
    }

    /**
     * Check if the content stream is open.
     *
     * @return true if this is the case
     */
    @Override
    public boolean isContentOpen() {
        if (content == null) {
            return false;
        }

        return content.isOpen();
    }

    /**
     * Determines if this file is executable.
     *
     * @return {@code true} if this file is executable, {@code false} if not.
     * @throws FileSystemException On error determining if this file exists.
     */
    @Override
    public boolean isExecutable() throws FileSystemException {
        try {
            return exists() ? doIsExecutable() : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/check-is-executable.error", fileName, exc);
        }
    }

    /**
     * Checks if this file is a regular file by using its file type.
     *
     * @return true if this file is a regular file.
     * @throws FileSystemException if an error occurs.
     * @see #getType()
     * @see FileType#FILE
     */
    @Override
    public boolean isFile() throws FileSystemException {
        // Use equals instead of == to avoid any class loader worries.
        return FileType.FILE.equals(this.getType());
    }

    /**
     * Checks if this file is a folder by using its file type.
     *
     * @return true if this file is a regular file.
     * @throws FileSystemException if an error occurs.
     * @see #getType()
     * @see FileType#FOLDER
     */
    @Override
    public boolean isFolder() throws FileSystemException {
        // Use equals instead of == to avoid any class loader worries.
        return FileType.FOLDER.equals(this.getType());
    }

    /**
     * Determines if this file can be read.
     *
     * @return true if the file is a hidden file, false otherwise.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public boolean isHidden() throws FileSystemException {
        try {
            return exists() ? doIsHidden() : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/check-is-hidden.error", fileName, exc);
        }
    }

    /**
     * Determines if this file can be read.
     *
     * @return true if the file can be read, false otherwise.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public boolean isReadable() throws FileSystemException {
        try {
            return exists() ? doIsReadable() : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/check-is-readable.error", fileName, exc);
        }
    }

    /**
     * Checks if this fileObject is the same file as {@code destFile} just with a different name. E.g. for case
     * insensitive file systems like windows.
     *
     * @param destFile The file to compare to.
     * @return true if the FileObjects are the same.
     * @throws FileSystemException if an error occurs.
     */
    protected boolean isSameFile(final FileObject destFile) throws FileSystemException {
        attach();
        return doIsSameFile(destFile);
    }

    /**
     * Determines if this file can be read.
     *
     * @return true if the file can be read, false otherwise.
     * @throws FileSystemException if an error occurs.
     * @since 2.4
     */
    @Override
    public boolean isSymbolicLink() throws FileSystemException {
        try {
            return exists() ? doIsSymbolicLink() : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/check-is-symbolic-link.error", fileName, exc);
        }
    }

    /**
     * Determines if this file can be written to.
     *
     * @return true if the file can be written to, false otherwise.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public boolean isWriteable() throws FileSystemException {
        try {
            if (exists()) {
                return doIsWriteable();
            }
            final FileObject parent = getParent();
            if (parent != null) {
                return parent.isWriteable();
            }
            return true;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/check-is-writeable.error", fileName, exc);
        }
    }

    /**
     * Returns an iterator over a set of all FileObject in this file object.
     *
     * @return an Iterator.
     */
    @Override
    public Iterator<FileObject> iterator() {
        try {
            return listFiles(Selectors.SELECT_ALL).iterator();
        } catch (final FileSystemException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Lists the set of matching descendants of this file, in depthwise order.
     *
     * @param selector The FileSelector.
     * @return list of files or null if the base file (this object) do not exist or the {@code selector} is null
     * @throws FileSystemException if an error occurs.
     */
    public List<FileObject> listFiles(final FileSelector selector) throws FileSystemException {
        if (!exists() || selector == null) {
            return null;
        }

        final ArrayList<FileObject> list = new ArrayList<>();
        this.findFiles(selector, true, list);
        return list;
    }

    /**
     * Moves (rename) the file to another one.
     *
     * @param destFile The target FileObject.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void moveTo(final FileObject destFile) throws FileSystemException {
        if (canRenameTo(destFile)) {
            if (!getParent().isWriteable()) {
                throw new FileSystemException("vfs.provider/rename-parent-read-only.error", getName(),
                        getParent().getName());
            }
        } else {
            if (!isWriteable()) {
                throw new FileSystemException("vfs.provider/rename-read-only.error", getName());
            }
        }

        if (destFile.exists() && !isSameFile(destFile)) {
            destFile.deleteAll();
            // throw new FileSystemException("vfs.provider/rename-dest-exists.error", destFile.getName());
        }

        if (canRenameTo(destFile)) {
            // issue rename on same filesystem
            try {
                attach();
                // remember type to avoid attach
                final FileType srcType = getType();

                doRename(destFile);

                FileObjectUtils.getAbstractFileObject(destFile).handleCreate(srcType);
                destFile.close(); // now the destFile is no longer imaginary. force reattach.

                handleDelete(); // fire delete-events. This file-object (src) is like deleted.
            } catch (final RuntimeException re) {
                throw re;
            } catch (final Exception exc) {
                throw new FileSystemException("vfs.provider/rename.error", exc, getName(), destFile.getName());
            }
        } else {
            // different fs - do the copy/delete stuff

            destFile.copyFrom(this, Selectors.SELECT_SELF);

            if ((destFile.getType().hasContent()
                    && destFile.getFileSystem().hasCapability(Capability.SET_LAST_MODIFIED_FILE)
                    || destFile.getType().hasChildren()
                            && destFile.getFileSystem().hasCapability(Capability.SET_LAST_MODIFIED_FOLDER))
                    && fileSystem.hasCapability(Capability.GET_LAST_MODIFIED)) {
                destFile.getContent().setLastModifiedTime(this.getContent().getLastModifiedTime());
            }

            deleteSelf();
        }

    }

    /**
     * Clled after this file-object closed all its streams.
     */
    protected void notifyAllStreamsClosed() {
        // noop
    }

    /**
     * Notify the parent of a change to its children, when a child is created or deleted.
     *
     * @param childName The name of the child.
     * @param newType The type of the child.
     * @throws Exception if an error occurs.
     */
    private void notifyParent(final FileName childName, final FileType newType) throws Exception {
        if (parent == null) {
            final FileName parentName = fileName.getParent();
            if (parentName != null) {
                // Locate the parent, if it is cached
                parent = fileSystem.getFileFromCache(parentName);
            }
        }

        if (parent != null) {
            FileObjectUtils.getAbstractFileObject(parent).childrenChanged(childName, newType);
        }
    }

    /**
     * Called when the type or content of this file changes.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @throws Exception if an error occurs.
     */
    protected void onChange() throws Exception {
        // noop
    }

    /**
     * Called when the children of this file change. Allows subclasses to refresh any cached information about the
     * children of this file.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @param child The name of the child that changed.
     * @param newType The type of the file.
     * @throws Exception if an error occurs.
     */
    protected void onChildrenChanged(final FileName child, final FileType newType) throws Exception {
        // noop
    }

    /**
     * This will prepare the fileObject to get resynchronized with the underlying file system if required.
     *
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public void refresh() throws FileSystemException {
        // Detach from the file
        try {
            detach();
        } catch (final Exception e) {
            throw new FileSystemException("vfs.provider/resync.error", fileName, e);
        }
    }

    private void removeChildrenCache() {
        children = null;
    }

    private FileObject resolveFile(final FileName child) throws FileSystemException {
        return fileSystem.resolveFile(child);
    }

    /**
     * Finds a file, relative to this file.
     *
     * @param path The path of the file to locate. Can either be a relative path, which is resolved relative to this
     *            file, or an absolute path, which is resolved relative to the file system that contains this file.
     * @return The FileObject.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileObject resolveFile(final String path) throws FileSystemException {
        final FileName otherName = fileSystem.getFileSystemManager().resolveName(fileName, path);
        return fileSystem.resolveFile(otherName);
    }

    /**
     * Returns a child by name.
     *
     * @param name The name of the child to locate.
     * @param scope the NameScope.
     * @return The FileObject for the file or null if the child does not exist.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public FileObject resolveFile(final String name, final NameScope scope) throws FileSystemException {
        // return fs.resolveFile(this.name.resolveName(name, scope));
        return fileSystem.resolveFile(fileSystem.getFileSystemManager().resolveName(this.fileName, name, scope));
    }

    private FileObject[] resolveFiles(final FileName[] children) throws FileSystemException {
        if (children == null) {
            return null;
        }

        final FileObject[] objects = new FileObject[children.length];
        for (int iterChildren = 0; iterChildren < children.length; iterChildren++) {
            objects[iterChildren] = resolveFile(children[iterChildren]);
        }

        return objects;
    }

    @Override
    public boolean setExecutable(final boolean readable, final boolean ownerOnly) throws FileSystemException {
        try {
            return exists() ? doSetExecutable(readable, ownerOnly) : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/set-executable.error", fileName, exc);
        }
    }

    private void setFileType(final FileType type) {
        if (type != null && type != FileType.IMAGINARY) {
            try {
                fileName.setType(type);
            } catch (final FileSystemException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        this.type = type;
    }

    @Override
    public boolean setReadable(final boolean readable, final boolean ownerOnly) throws FileSystemException {
        try {
            return exists() ? doSetReadable(readable, ownerOnly) : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/set-readable.error", fileName, exc);
        }
    }

    // --- OPERATIONS ---

    @Override
    public boolean setWritable(final boolean readable, final boolean ownerOnly) throws FileSystemException {
        try {
            return exists() ? doSetWritable(readable, ownerOnly) : false;
        } catch (final Exception exc) {
            throw new FileSystemException("vfs.provider/set-writeable.error", fileName, exc);
        }
    }

    /**
     * Returns the URI as a String.
     *
     * @return Returns the URI as a String.
     */
    @Override
    public String toString() {
        return fileName.getURI();
    }
}
