/*
 * 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.openide.filesystems;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.openide.util.Enumerations;
import org.openide.util.NbBundle;
import org.openide.util.BaseUtilities;

/** Implementation of the file object that simplyfies common
* tasks with hierarchy of objects for AbstractFileObject and MultiFileObject.
*
* @author Jaroslav Tulach,
*/
abstract class AbstractFolder extends FileObject {
    /** empty array */
    private static final AbstractFolder[] EMPTY_ARRAY = new AbstractFolder[0];

    /** default extension separator */
    private static final char EXT_SEP = '.';

    /** file system */
    private FileSystem system;

    /** name of the file (only name and extension) */
    protected String name;

    /** strong reference to parent (can be null for root) */
    protected final AbstractFolder parent;

    /** Stores the system name of the file system to test
    * validity later.
    */
    boolean validFlag;

    /** If root changes, all AbstractFolders in hierarchy are invalidated.
     *@see #isValid()*/
    private final AbstractFolder validRoot;

    /** list of children */
    private String[] children;

    /** map that assigns file object to names. */
    private Map<String,Reference<AbstractFolder>> map;

    /** listeners */
    private ListenerList<FileChangeListener> listeners;

    /** Constructor. Takes reference to file system this file belongs to.
    *
    * @param fs the file system
    * @param parent the parent object (folder)
    * @param name name of the object (e.g. <code>filename.ext</code>)
    */
    public AbstractFolder(FileSystem fs, AbstractFolder parent, String name) {
        this.system = fs;
        this.parent = parent;
        this.name = name;
        validFlag = true;
        validRoot = (parent != null) ? (AbstractFolder) fs.getRoot() : null;
    }

    /* Get the name without extension of this file or folder.
    * Period at first position is not considered as extension-separator
    * @return name of the file or folder(in its enclosing folder)
    */
    public final String getName() {
        int i = name.lastIndexOf('.');

        /** period at first position is not considered as extension-separator */
        return (i <= 0) ? name : name.substring(0, i);
    }

    /* Get the extension of this file or folder.
    * Period at first position is not considered as extension-separator
    * This is the string after the last dot of the full name, if any.
    *
    * @return extension of the file or folder(if any) or empty string if there is none
    */
    public final String getExt() {
        int i = name.lastIndexOf('.') + 1;

        /** period at first position is not considered as extension-separator */
        return ((i <= 1) || (i == name.length())) ? "" : name.substring(i); // NOI18N
    }

    /** Overrides the get name and ext method to make it faster then
     * default implementation.
     */
    @Override
    public final String getNameExt() {
        return name;
    }

    /** Overridden in AbstractFolder */
    @Override
    final boolean isHasExtOverride() {
        return true;
    }

    /** Overridden in AbstractFolder */
    @Override
    boolean hasExtOverride(String ext) {
        if (ext == null) {
            return false;
        }

        /** period at first position is not considered as extension-separator */
        if ((name.length() - ext.length()) <= 1) {
            return false;
        }

        boolean ret = name.endsWith(ext);

        if (!ret) {
            return false;
        }

        if (name.charAt(name.length() - ext.length() - 1) != '.') {
            return false;
        }

        return true;
    }

    /* Getter for the right file system */
    public final FileSystem getFileSystem() {
        return system;
    }

    //
    // Info
    //

    /* Test whether this object is the root folder.
    * The root should always be a folder.
    * @return true if the object is the root of a file system
    */
    public final boolean isRoot() {
        return parent == null;
    }

    /* Test whether the file is valid. The file can be invalid if it has been deserialized
    * and the file no longer exists on disk; or if the file has been deleted.
    *
    * @return true if the file object is valid
    */
    public final boolean isValid() {
        // valid
        if (parent == null) {
            return this == system.getRoot();
        }

        boolean isValidRoot = getFileSystem().getRoot() == validRoot;

        return validFlag && isValidRoot;
    }

    @Override
    public String toString() {
      if (!isValid()) {
        return super.toString() + " parent: " + parent + " validFlag: " + validFlag + " validRoot: " + validRoot + " isValidRoot: " + (getFileSystem().getRoot() == validRoot);
      }
      return super.toString();
    }

    //
    // List
    //

    /* Get parent folder.
    * The returned object will satisfy {@link #isFolder}.
    *
    * @return the parent folder or <code>null</code> if this object {@link #isRoot}.
    */
    public final FileObject getParent() {
        return parent;
    }

    /* Get all children of this folder (files and subfolders). If the file does not have children
    * (does not exist or is not a folder) then an empty array should be returned. No particular order is assumed.
    *
    * @return array of direct children
    * @see #getChildren(boolean)
    * @see #getFolders
    * @see #getData
    */
    public final synchronized FileObject[] getChildren() {
        check();

        if (children == null) {
            return new FileObject[0];
        }

        int size = children.length;
        ArrayList<FileObject> aList = new ArrayList<FileObject>();

        for (int i = 0; i < size; i++) {
            FileObject f = getChild(children[i]);

            if (f != null) {
                aList.add(f);
            }
        }

        return aList.toArray(new FileObject[0]);
    }

    /** Tries to find a resource.
    * @param en enumeration of strings to scan
    * @return found object or null
    */
    final FileObject find(Enumeration<String> en) {
        AbstractFolder fo = this;

        while ((fo != null) && en.hasMoreElements()) {
            // try to go on
            // lock to provide safety for getChild
            synchronized (fo) {
                // JST: Better to call the check only here,
                // than in getChild, than it is not called
                // so often.
                fo.check();
                final String next = en.nextElement();
                if ("..".equals(next)) {
                    fo = (AbstractFolder)fo.getParent();
                } else {
                    fo = fo.getChild(next);
                }
            }
        }

        // no next requirements or not found
        return fo;
    }

    /** Tries to find a resource if it exists in memory.
    * @param en enumeration of strings to scan
    * @return found object or null
    */
    final FileObject findIfExists(Enumeration<String> en) {
        Reference<AbstractFolder> r = findRefIfExists(en);

        return (r == null) ? null : r.get();
    }

    /** Tries to find a resource if it exists in memory.
    * @param en enumeration of strings to scan
    * @return found object or null
    */
    final Reference<AbstractFolder> findRefIfExists(Enumeration<String> en) {
        AbstractFolder fo = this;

        while ((fo != null) && en.hasMoreElements()) {
            if (fo.map == null) {
                // this object is not initialized yet
                return null;
            }

            // try to go on
            // lock to provide safety for getChild
            synchronized (fo) {
                String tmpName = en.nextElement();

                if (en.hasMoreElements()) {
                    fo = fo.getChild(tmpName);
                } else {
                    return fo.map.get(tmpName);
                }
            }
        }

        // no next requirements or not found
        return null;
    }

    /** Finds one child for given name .
    * @param name the name of the child
    * @return the file object or null if it does not exist
    */
    protected final AbstractFolder getChild(String name) {
        return getChild(name, true);
    }

    private final AbstractFolder getChild(String name, boolean onlyValid) {
        Reference<AbstractFolder> r = map.get(name);

        if (r == null) {
            //On OpenVMS, see if the name is stored in a different case
            //to work around a JVM bug.
            //
            if (BaseUtilities.getOperatingSystem() == BaseUtilities.OS_VMS) {
                if (Character.isLowerCase(name.charAt(0))) {
                    r = map.get(name.toUpperCase());
                } else {
                    r = map.get(name.toLowerCase());
                }

                if (r == null) {
                    return null;
                }
            } else {
                return null;
            }
        }

        AbstractFolder fo = r.get();

        if (fo == null) {
            // object does not exist => have to recreate it
            fo = createFile(name);

            if ((fo != null) && fo.isValid()) {
                map.put(name, (fo != null) ? createReference(fo) : null);
            } else {
                if (onlyValid) {
                    fo = null;
                }
            }
        }

        return fo;
    }

    /** Get method for children array .
    * @return array of children
    */
    final String[] getChildrenArray() {
        return children;
    }

    /** Creates Reference. In FileSystem, which subclasses AbstractFileSystem, you can overload method
    * createReference(FileObject fo) to achieve another type of Reference (weak, strong etc.)
    * @param fo FileObject
    * @return Reference to FileObject
    */
    protected Reference<AbstractFolder> createReference(AbstractFolder fo) {
        return (new WeakReference<AbstractFolder>(fo));
    }

    /** Obtains enumeration of all existing subfiles.
    */
    final synchronized AbstractFolder[] subfiles() {
        if (map == null) {
            return EMPTY_ARRAY;
        }

        Iterator it = map.values().iterator();
        ArrayList<FileObject> ll = new ArrayList<FileObject>(map.size() + 2);

        while (it.hasNext()) {
            Reference r = (Reference) it.next();

            if (r == null) {
                continue;
            }

            AbstractFolder fo = (AbstractFolder) r.get();

            if (fo != null /*&& (!fo.isFolder () || fo.map != null)*/    ) {
                // if the file object exists and either is not folder (then
                // we have to check the time) or it is folder and it has
                // some children
                // => use it
                ll.add(fo);
            }
        }

        return  ll.toArray(EMPTY_ARRAY);
    }

    final boolean isInitialized() {
        return this.map != null;
    }

    /** Creates enumeration of existing subfiles in all tree
    * of files.
    *
    * @param rec should it be recursive or not
    * @return enumeration of AbstractFolders
    */
    final Enumeration<AbstractFolder> existingSubFiles(boolean rec) {
        if (!rec) {
            return Enumerations.array(subfiles());
        } else {
            class P implements org.openide.util.Enumerations.Processor<AbstractFolder, AbstractFolder> {
                public AbstractFolder process(AbstractFolder af, Collection<AbstractFolder> toAdd) {
                    toAdd.addAll(Arrays.asList(af.subfiles()));

                    return af;
                }
            }

            return Enumerations.queue(Enumerations.singleton(this), new P());
        }
    }

    /* helper method for MFO.setAttribute. MFO can disable firing from underlaying
     * layers. Should be reviewed in 3.4 or later
      *@see MultiFileObject#setAttribute*/
    abstract void setAttribute(String attrName, Object value, boolean fire)
    throws IOException;

    /** Retrieve file or folder contained in this folder by name.
    * <em>Note</em> that neither file nor folder is created on disk.
    * @param name basename of the file or folder (in this folder)
    * @param ext extension of the file; <CODE>null</CODE> or <code>""</code>
    *    if the file should have no extension or if folder is requested
    * @return the object representing this file or <CODE>null</CODE> if the file
    *   or folder does not exist
    * @exception IllegalArgumentException if <code>this</code> is not a folder
    */
    @Override
    public final FileObject getFileObject(String name, String ext) {
        getFileSystem().waitRefreshed();
        return getFileObjectImpl(name, ext);
    }
        
    private synchronized FileObject getFileObjectImpl(String name, String ext) {
        check();

        if ((ext == null) || ext.equals("")) { // NOI18N

            return getChild(name);
        } else {
            StringBuffer sb = new StringBuffer(name.length() + 1 + ((ext == null) ? 0 : ext.length()));
            sb.append(name).append(EXT_SEP).append(ext);

            return getChild(sb.toString());
        }
    }

    /* Refresh the contents of a folder. Rescans the list of children names.
    */
    public void refresh(boolean expected) {
        if (!isInitialized() && isFolder()) {
            return;
        }

        refresh(null, null, true, expected);
    }

    //
    // Listeners section
    //

    /* Add new listener to this object.
    * @param l the listener
    */
    public final void addFileChangeListener(FileChangeListener fcl) {
        synchronized (EMPTY_ARRAY) {
            if (listeners == null) {
                listeners = new ListenerList<FileChangeListener>();
            }
        }

        listeners.add(fcl);
    }

    /* Remove listener from this object.
    * @param l the listener
    */
    public final void removeFileChangeListener(FileChangeListener fcl) {
        if (listeners != null) {
            listeners.remove(fcl);
        }
    }

    /** Fires event */
    protected final void fileDeleted0(FileEvent fileevent) {
        super.fireFileDeletedEvent(listeners(), fileevent);

        if (fileevent.getFile().equals(this) && (parent != null)) {
            FileEvent ev = new FileEvent(parent, fileevent.getFile(), fileevent.isExpected());
            try {
                ev.inheritPostNotify(fileevent);
                parent.fileDeleted0(ev);
            } finally {
                ev.setPostNotify(null);
            }
        }
    }

    /** Fires event */
    protected final void fileCreated0(FileEvent fileevent, boolean isData) {
        /*
                if(isData)
                    super.fireFileDataCreatedEvent(listeners (), fileevent);
                else
                    super.fireFileFolderCreatedEvent(listeners (), fileevent);
        */
        dispatchEvent(listeners(), fileevent);

        if (fileevent.getFile().equals(this) && (parent != null)) {
            FileEvent ev = new FileEvent(parent, fileevent.getFile(), fileevent.isExpected());
            try {
                ev.inheritPostNotify(fileevent);
                parent.fileCreated0(ev, isData);
            } finally {
                ev.setPostNotify(null);
            }
        }
    }

    /** Creates nad fires event */
    protected final void fileCreated0(FileObject src, FileObject file, boolean expected) {
        fileCreated0(new FileEvent(src, file, expected), false);
    }

    /** Fires event */
    protected final void fileChanged0(FileEvent fileevent) {
        super.fireFileChangedEvent(listeners(), fileevent);

        if (fileevent.getFile().equals(this) && (parent != null)) {
            FileEvent ev = new FileEvent(parent, fileevent.getFile(), fileevent.isExpected(), fileevent.getTime());
            try {
                ev.inheritPostNotify(fileevent);
                parent.fileChanged0(ev);
            } finally {
                ev.setPostNotify(null);
            }
        }
    }

    /** Fires event - but doesn`t fork events*/
    final void fileChanged1(FileEvent fileevent) {
        super.fireFileChangedEvent(listeners(), fileevent);
    }

    /** Fires event */
    protected final void fileRenamed0(FileRenameEvent filerenameevent) {
        super.fireFileRenamedEvent(listeners(), filerenameevent);

        if (filerenameevent.getFile().equals(this) && (parent != null)) {
            FileRenameEvent ev = new FileRenameEvent(
                    parent, filerenameevent.getFile(), filerenameevent.getName(), filerenameevent.getExt(),
                    filerenameevent.isExpected()
                );
            try {
                ev.inheritPostNotify(filerenameevent);
                parent.fileRenamed0(ev);
            } finally {
                ev.setPostNotify(null);
            }
        }
    }

    /** Fires event */
    protected final void fileAttributeChanged0(FileAttributeEvent fileattributeevent) {
        super.fireFileAttributeChangedEvent(listeners(), fileattributeevent);

        if (fileattributeevent.getFile().equals(this) && (parent != null)) {
            FileAttributeEvent ev = new FileAttributeEvent(
                    parent, fileattributeevent.getFile(), fileattributeevent.getName(), fileattributeevent.getOldValue(),
                    fileattributeevent.getNewValue(), fileattributeevent.isExpected()
                );
            try {
                ev.inheritPostNotify(fileattributeevent);
                parent.fileAttributeChanged0(ev);
            } finally {
                ev.setPostNotify(null);
            }
        }
    }

    /** @return true if there is a listener
    */
    protected final boolean hasListeners() {
        boolean fsHas = getFileSystem().getFCLSupport().hasListeners();
        boolean repHas = false;
        Repository rep = getFileSystem().getRepository();

        if (rep != null) {
            repHas = rep.getFCLSupport().hasListeners();
        }

        return (listeners != null && listeners.hasListeners()) || repHas || fsHas;
    }

    /** @return true if this folder or its parent have listeners
    */
    protected final boolean hasAtLeastOneListeners() {
        return hasListeners() || ((parent != null) && parent.hasListeners());
    }

    /** @return enumeration of all listeners.
    */
    private final Enumeration<FileChangeListener> listeners() {
        if (listeners == null) {
            return Enumerations.empty();
        } else {
            return Collections.enumeration(listeners.getAllListeners());
        }
    }

    //
    // Refreshing the state of the object
    //

    /** Test if the file has been checked and if not, refreshes its
    * content.
    */
    private final void check() {
        if (map == null) {
            refresh(null, null, false, false);

            if (map == null) {
                // create empty map to mark that we are initialized
                map = Collections.emptyMap();

                if (children == null) {
                    children = new String[] {  };
                }
            }
        }
    }

    /** Refresh the content of file. Ignores changes to the files provided,
    * instead returns its file object.
    * @param added do not notify addition of this file
    * @param removed do not notify removing of this file
    */
    protected final void refresh(String added, String removed) {
        this.refresh(added, removed, false);
    }

    /** Refresh the content of file. Ignores changes to the files provided,
    * instead returns its file object.
    * @param added do not notify addition of this file
    * @param removed do not notify removing of this file
    * @param reuseChildren a flag reuse children?
    */
    protected final void refresh(String added, String removed, boolean reuseChildren) {
        String[] prev = children;
        if (reuseChildren && removed != null && prev != null) {
            String[] nc = prev.clone();
            for (int i = nc.length; --i >= 0;) {
                if (removed.equals(nc[i])) {
                    nc[i] = null;
                    break;
                }
            }

            refresh(added, removed, true, false, nc);
        } else {
            refresh(added, removed, true, false, null);
        }
    }

    /** Method that allows subclasses to return its children.
    *
    * @return names (name . ext) of subfiles
    */
    protected abstract String[] list();

    /** Method to create a file object for given subfile.
    * @param name of the subfile
    * @return the file object
    */
    protected abstract AbstractFolder createFile(String name);

    /** Refresh the content of file. Ignores changes to the files provided,
    * instead returns its file object.
    * @param added do not notify addition of this file
    * @param removed do not notify removing of this file
    * @param fire true if we should fire changes
    * @param expected true if the change has been expected by the user
    */
    protected void refresh(String added, String removed, boolean fire, boolean expected) {
        this.refresh(added, removed, fire, expected, null);
    }

    void registerChild(String name) {
        synchronized (this) {
            if (map == null) {
                check();
            }

            Reference<AbstractFolder> o = map.put(name, new WeakReference<AbstractFolder>(null));

            if (o != null) {
                map.put(name, o);
            } else {
                String[] newChildren = new String[children.length + 1];
                System.arraycopy(children, 0, newChildren, 0, children.length);
                newChildren[children.length] = name;
                children = newChildren;
            }
        }
    }

    /** Refresh the content of file. Ignores changes to the files provided,
    * instead returns its file object.
    * @param added do not notify addition of this file
    * @param removed do not notify removing of this file
    * @param fire true if we should fire changes
    * @param expected true if the change has been expected by the user
    * @param list contains list of children
    */
    protected final void refreshFolder(
        String added, String removed, boolean fire, boolean expected, final String[] list
    ) {
        try {
            getFileSystem().beginAtomicAction();

            // refresh of folder checks children
            final String[] newChildren = getNewChildren(list);
            final Set<String> addedNames;
            final Map<String, AbstractFolder> removedPairs;

            synchronized (this) {
                if ((children == null) && (newChildren == null)) {
                    return;
                }

                final int initialCapacity = (newChildren != null) ? (((newChildren.length * 4) / 3) + 1) : 0;
                final HashMap<String, Reference<AbstractFolder>> newMap = new HashMap<String, Reference<AbstractFolder>>(initialCapacity);

                /*Just for firing event*/
                addedNames = new HashSet<String>(initialCapacity);

                if (newChildren != null) {
                    final Reference<AbstractFolder> removedRef = ((map != null) ? map.get(removed) : null);

                    for (int i = 0; i < newChildren.length; i++) {
                        final String child = newChildren[i];
                        Reference<AbstractFolder> foRef = null;

                        if (map != null) {
                            foRef = map.remove(child);

                            if (((foRef != null) && (added != null) && (removed != null) && child.equals(removed))) {
                                // needs cvs checkout
                                foRef = null;
                            }

                            if ((added != null) && (removed != null) && child.equals(added)) {
                                foRef = removedRef;
                            }

                            if (((foRef != null) && (added == null) && (removed != null) && child.equals(removed))) {
                                // needs cvs checkout
                                foRef = null;
                            }
                        }

                        if (foRef == null) {
                            if (!child.equals(added)) {
                                addedNames.add(child);
                            }

                            // create new empty reference
                            foRef = new WeakReference<AbstractFolder>(null);
                        }

                        newMap.put(child, foRef);
                    }
                }

                removedPairs = (map != null) ? dereferenceValues(map) : null;
                map = newMap;
                children = newChildren;
            }

            if (fire && (addedNames != null) && hasAtLeastOneListeners()) {
                // fire these files has been added
                filesCreated(addedNames, expected);
            }

            if (fire && (removedPairs != null)) {
                if (removed != null) {
                    removedPairs.remove(removed);
                }

                filesDeleted(removedPairs, expected);
            }

            if (
                fire && (added == null) && (removed == null) && !getFileSystem().isReadOnly() &&
                    !(this instanceof MultiFileObject)
            ) {
                Set<String> nameFilter = nameFilter = new HashSet<String>();

                if (addedNames != null) {
                    nameFilter.addAll(addedNames);
                }

                if (removedPairs != null) {
                    nameFilter.addAll(removedPairs.keySet());
                }

                refreshChildren(existingSubFiles(false), nameFilter, expected);
            }
        } finally {
            getFileSystem().finishAtomicAction();
        }
    }

    private void refreshChildren(final Enumeration<? extends AbstractFolder> subfiles, final Collection nameFilter, boolean expected) {
        while (subfiles.hasMoreElements()) {
            AbstractFolder child = subfiles.nextElement();

            if (child.isData()) {
                if (!nameFilter.contains(child.getNameExt())) {
                    child.refresh(expected);
                }
            }
        }
    }

    private void filesDeleted(final Map<String,AbstractFolder> removedToFire, final boolean expected) {
        for (AbstractFolder fo : removedToFire.values()) {
            fo.validFlag = false;

            if (hasAtLeastOneListeners() || fo.hasAtLeastOneListeners()) {
                FileEvent ev = new FileEvent(fo, fo, expected);
                fo.fileDeleted0(ev);
            }
        }
    }

    private void filesCreated(final Set<String> addedToFire, final boolean expected) {
        for (String s : addedToFire) {
            AbstractFolder fo = getChild(s);

            if (fo != null) {
                fileCreated0(this, fo, expected);
            }
        }
    }

    private Map<String, AbstractFolder> dereferenceValues(final Map<String, Reference<AbstractFolder>>  map) {
        Map<String, AbstractFolder> retVal = new HashMap<String, AbstractFolder>(map.size());
        for (String name : map.keySet()) {
            AbstractFolder child = getChild(name, false);

            if (child != null) {
                retVal.put(name, child);
            }
        }

        return retVal;
    }

    private String[] getNewChildren(final String[] list) {
        String[] newChildren = (list != null) ? list : list();

        if (isRoot() && (newChildren == null)) {
            newChildren = new String[0];
        }

        if (newChildren != null) {
            newChildren = stripNulls(newChildren);
        }

        return newChildren;
    }

    private static String[] stripNulls(final String[] children) {
        String[] newChildren = children;
        Collection<String> childrenList = new ArrayList<String>(Arrays.asList(newChildren));

        for (Iterator<String> iterator = childrenList.iterator(); iterator.hasNext();) {
            String child = iterator.next();

            if (child == null) {
                iterator.remove();
            }
        }

        if (childrenList.size() != newChildren.length) {
            newChildren = childrenList.toArray(new String[childrenList.size()]);
        }

        return newChildren;
    }

    /** Refresh the content of file. Ignores changes to the files provided,
    * instead returns its file object.
    * @param added do not notify addition of this file
    * @param removed do not notify removing of this file
    * @param fire true if we should fire changes
    * @param expected true if the change has been expected by the user
    * @param list contains list of children
    */
    protected void refresh(String added, String removed, boolean fire, boolean expected, String[] list) {
        if (isFolder()) {
            refreshFolder(added, removed, fire, expected, list);
        }
    }

    /** Notification that the output stream has been closed.
     * @param fireFileChanged defines if FileEvent should be fired to notify about
     * change of file after close of stream
     */
    protected void outputStreamClosed(boolean fireFileChanged) {
        if (fireFileChanged) {
            fileChanged0(new FileEvent(AbstractFolder.this, AbstractFolder.this, lastModified().getTime()));
        }
    }

    //
    // Serialization
    //
    public final Object writeReplace() {
        return new Replace(this);
    }

    /* Delete this file. If the file is a folder and it is not empty then
    * all of its contents are also recursively deleted.
    *
    * @param lock the lock obtained by a call to {@link #lock}
    * @exception IOException if the file could not be deleted
    */
    final public void delete(FileLock lock) throws IOException {
        if (isFolder()) {
            FileObject[] fos = this.getChildren();

            for (int i = 0; i < fos.length; i++) {
                FileObject fo = fos[i];
                FileLock foLock = fo.lock();

                try {
                    fo.delete(foLock);
                } catch (IOException iex) {
                    String message = NbBundle.getMessage(AbstractFolder.class, "EXC_CannotDelete",
                        // XXX use FileUtil.getFileDisplayName instead?
                        getPath(), fo.getFileSystem().getDisplayName()
                        );
                    ExternalUtil.annotate(iex, message); //NOI18N
                    throw iex;
                } finally {
                    foLock.releaseLock();
                }
            }
        }

        handleDelete(lock);
    }

    abstract void handleDelete(FileLock lock) throws IOException;

    @SuppressWarnings("deprecation") // have to delegate for compat
    public boolean canWrite() {
        return !isReadOnly();
    }

    private static final class Replace implements Serializable {
        private static final long serialVersionUID = 1L;
        private transient FileObject f;
        private String fsname;
        private String path;
        private URL url;

        public Replace(FileObject f) {
            this.f = f;
        }

        @SuppressWarnings("deprecation") // FileSystem.systemName historical part of serial form
        private void writeObject(ObjectOutputStream oos)
        throws IOException {
            fsname = f.getFileSystem().getSystemName();
            path = f.getPath();
            url = f.toURL();
            assert url != null : "No URL for " + path;
            oos.defaultWriteObject();
        }

        private void readObject(ObjectInputStream ois)
        throws IOException, ClassNotFoundException {
            ois.defaultReadObject();
            assert fsname != null : "Should always have a non-null fsname here";

            @SuppressWarnings("deprecation") // historical part of serial form
            FileSystem fs = Repository.getDefault().findFileSystem(fsname);

            if (fs != null) {
                assert path != null : "Should always have a non-null path here";
                f = fs.findResource(path);
            }

            if (f == null) {
                // Fall back to URL.
                assert url != null : "Should always have a non-null URL here";

                f = URLMapper.findFileObject(url);

                if (f == null) {
                    throw new FileNotFoundException("Could not restore: " + url); // NOI18N
                }
            }
        }

        public Object readResolve() {
            assert f != null : "Did not read " + url;

            return f;
        }
    }
}
