/*
 * 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.Externalizable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.OutputStream;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.Enumerations;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;

/** Implementation of the file object for multi file system.
*
* @author Jaroslav Tulach,
*/
final class MultiFileObject extends AbstractFolder implements FileObject.PriorityFileChangeListener {
    /** generated Serialized Version UID */
    static final long serialVersionUID = -2343651324897646809L;

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

    /** default path separator */
    private static final char PATH_SEP = '/';
    static final String WEIGHT_ATTRIBUTE = "weight"; // NOI18N
    private static final FileSystem.AtomicAction markAtomicAction = new FileSystem.AtomicAction() {
            public void run() {
            }
        };

    static final ThreadLocal<FileObject> attrAskedFileObject = new ThreadLocal<FileObject>();

    /** list of objects that we delegate to and that already
    * has been created.
    */
    private Set delegates;

    /** current delegate (the first object to delegate to), never null */
    private FileObject leader;

    /** Reference to lock or null */
    private Reference<MfLock> lock;
    
    /** Stack trace of locking thread. */
    protected Throwable lockedBy;

    /** listener */
    private FileChangeListener weakL;

    /**performance trick:  holds delegetad FileObject used last time to getAttributes.
     * It may looks simply, but all more sophisticated solutions was less efficient.
     */
    private static Map<MultiFileObject, AttributeCache> fo2AttribCache = 
            new WeakHashMap<MultiFileObject, AttributeCache>();

    /** 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 MultiFileObject(MultiFileSystem fs, MultiFileObject parent, String name) {
        super(fs, parent, name);

        weakL = org.openide.util.WeakListeners.create(
                FileObject.PriorityFileChangeListener.class, FileChangeListener.class, this, null
            );

        update();

        if (leader == null) {
            leader = new AbstractFileObject.Invalid(name);
            validFlag = false;
        }
    }

    /** Constructor for root.
    *
    * @param fs the file system
    */
    public MultiFileObject(MultiFileSystem fs) {
        this(fs, null, ""); // NOI18N
    }

    /** File system.
    */
    public FileSystem getLeaderFileSystem() throws FileStateInvalidException {
        return leader.getFileSystem();
    }

    static synchronized void freeAllAttribCaches() {
        fo2AttribCache.clear();
    }
    
    /** Frees cached objects - added with perf.changes  */
    private void freeAttribCache() {
        getAttributeCache().free();
    }
    
    private AttributeCache getAttributeCache() {
        synchronized (MultiFileObject.class) {
            AttributeCache retval = fo2AttribCache.get(this);
            if (retval == null) {
                retval = new AttributeCache();
                fo2AttribCache.put(this, retval);
            }
            return retval;
        }
    }

    private boolean setAttributeOnFO(FileObject fo, String attrToSet, Object value, boolean fire) throws IOException {
        if (fo instanceof AbstractFolder) {
            ((AbstractFolder) fo).setAttribute(attrToSet, value, false);
        } else {
            /** cannot disable firing from underlaying delegate if not AbstractFolder
             * without API change. So I don`t fire events from this method with one
             * exception - root.
             */
            fire = fire && fo.isRoot();
            fo.setAttribute(attrToSet, value);
        }
        return fire;
    }

    /** Updates list of all references.
    */
    private void update() {
        MultiFileSystem mfs = getMultiFileSystem();
        FileSystem[] arr = mfs.getDelegates();

        Set now = (delegates == null) ? Collections.emptySet(): delegates;
        Set<FileObject> del = new HashSet<FileObject>(arr.length * 2);
        Number maxWeight = 0;
        FileObject led = null;

        String name = getPath();
        FileSystem writable = mfs.writableLayer(name);

        for (int i = 0; i < arr.length; i++) {
            if (arr[i] != null) {
                FileObject fo = mfs.findResourceOn(arr[i], name);

                if (fo != null) {
                    del.add(fo);

                    if (!now.remove(fo)) {
                        // now there yet
                        fo.addFileChangeListener(weakL);
                    }

                    if (fo.isValid()) {
                        Number weight = 
                            fo.isRoot() &&
                            !mfs.canHaveRootAttributeOnReadOnlyFS(WEIGHT_ATTRIBUTE)
                            ? 0 : weightOf(fo, writable);
                        if (led == null || weight.doubleValue() > maxWeight.doubleValue()) {
                            led = fo;
                            maxWeight = weight;
                        }
                    }
                }
            }
        }

        Iterator<FileObject> it = now.iterator();
        while (it.hasNext()) {
            FileObject fo = it.next();
            fo.removeFileChangeListener(weakL);
        }

        if (led != null) {
            // otherwise leave the leader to be last file that represented
            // this one
            if (!led.equals(this.leader) && (this.leader != null)) {
                // isValid prevents here from firing events after MFO.delete
                if (isData() && isValid()) {
                    fileChanged0(new FileEvent(this));
                }

                getMultiFileSystem().notifyMigration(this);
            }

            this.leader = led;
        }

        this.delegates = del;
    }

    /** Update all existing subobjects.
    */
    void updateAll() {
        FileSystem mfs = getMultiFileSystem();

        try {
            mfs.beginAtomicAction();

            // enumeration of all existing objects
            Enumeration<AbstractFolder> en = existingSubFiles(true);

            while (en.hasMoreElements()) {
                MultiFileObject mfo = (MultiFileObject) en.nextElement();

                if (mfo.isFolder() && !mfo.isInitialized()) {
                    continue;
                }

                mfo.freeAttribCache();
                mfo.superRefresh(true);
            }
        } finally {
            mfs.finishAtomicAction();
        }
    }

    /** This method was added to achieve firing events that attributes
     * were changed after setDelegates. Events are not fired reliable but this solution was
     * choosed because of performance reasons. Attributes name is set to null - what means
     * that one of attributes was probably changed.
     */
    void updateAllAfterSetDelegates(FileSystem[] oldFileSystems) {
        try {
            getMultiFileSystem().beginAtomicAction();

            FileSystem[] fileSystems = getMultiFileSystem().getDelegates();
            Enumeration<AbstractFolder> en = existingSubFiles(true);

            while (en.hasMoreElements()) {
                MultiFileObject mfo = (MultiFileObject) en.nextElement();

                if (mfo.isFolder() && !mfo.isInitialized()) {
                    continue;
                }

                if (mfo.hasListeners()) {
                    String path = mfo.getPath();
                    FileObject oldLeader = findLeader(oldFileSystems, path);
                    FileObject newLeader = findLeader(fileSystems, path);

                    if ((oldLeader != null) && (newLeader != null) && !oldLeader.equals(newLeader)) {
                        mfo.fileAttributeChanged0(new FileAttributeEvent(mfo, null, null, null));
                    }
                }

                mfo.freeAttribCache();
                mfo.refresh(true);
            }
        } finally {
            getMultiFileSystem().finishAtomicAction();
        }
    }

    private void refreshAfterEvent(FileEvent fe) {
        FileObject fFile = fe.getFile();
        superRefresh(false);

        MultiFileObject mFile = (MultiFileObject) getFileObject(fFile.getName(), fFile.getExt());

        if (mFile != null) {
            mFile.superRefresh(false);
        }
    }

    private void superRefresh(boolean expected) {
        super.refresh(expected);
    }

    private FileObject findLeader(FileSystem[] fs, String path) {
        MultiFileSystem mfs = getMultiFileSystem();

        Number maxWeight = 0;
        FileObject _leader = null;
        FileSystem writable = mfs.writableLayer(path);
        
        for (FileSystem f : fs) {
            if (f == null) {
                continue;
            }
            FileObject fo = mfs.findResourceOn(f, path);

            if (fo != null) {
                Number weight = weightOf(fo, writable);
                if (_leader == null || weight.doubleValue() > maxWeight.doubleValue()) {
                    _leader = fo;
                    maxWeight = weight;
                }
            }
        }

        return _leader;
    }

    private static Number weightOf(FileObject f, FileSystem writable) {
        try {
            if (f.getFileSystem() == writable) {
                return Double.MAX_VALUE;
            }
        } catch (FileStateInvalidException x) {/* ignore */}
        Object weight = f.getAttribute(WEIGHT_ATTRIBUTE);
        if (weight instanceof Number) {
            return (Number) weight;
        } else if (weight == null) {
            return 0;
        } else {
            try {
                Logger.getLogger(MultiFileObject.class.getName()).log(
                        Level.WARNING, "File {0} in {1} has nonnumeric weight {2} of type {3}",
                        new Object[] {f.getPath(), f.getFileSystem(), weight, weight.getClass().getName()});
            } catch (FileStateInvalidException x) {/* ignore */}
            return 0;
        }
    }

    /** Getter for the right file system */
    private MultiFileSystem getMultiFileSystem() {
        return (MultiFileSystem) getFileSystem();
    }

    /** Getter for one of children.
    */
    private MultiFileObject getMultiChild(String name) {
        return (MultiFileObject) getChild(name);
    }

    /** Converts the file to be writable.
    * The file has to be locked!
    *
    * @return file object (new leader) that is writable
    * @exception IOException if the object cannot be writable
    */
    private FileObject writable(boolean copyContents) throws IOException {
        MultiFileSystem fs = getMultiFileSystem();
        FileSystem single = fs.createWritableOn(getPath());

        if (single != leader.getFileSystem()) {
            // if writing to a file that is not on writable fs =>
            // copy it
            if (leader.isFolder()) {
                leader = FileUtil.createFolder(root(single), getPath());
            } else {
                FileObject folder = FileUtil.createFolder(root(single), getParent().getPath());
                if (copyContents) {
                    leader = leader.copy(folder, leader.getName(), leader.getExt());
                } else {
                    leader = folder.createData(leader.getNameExt());
                }
            }

            MfLock l = ((lock == null) ? null : lock.get());

            if (l != null) {
                // update the lock
                l.addLock(leader);
            }
        }

        return leader;
    }

    /** All objects that are beyond this one.
    * @return enumeration of FileObject
    */
    private Enumeration<FileObject> delegates() {
        return getMultiFileSystem().delegates(getPath());
    }

    /** Method that goes upon list of folders and updates its locks. This is used when
    * an object is masked which may lead to creation of folders on a disk.
    *
    * @param fo folder to check
    * @exception IOException if something locks cannot be updated
    */
    private static void updateFoldersLock(FileObject fo)
    throws IOException {
        while (fo != null) {
            MultiFileObject mfo = (MultiFileObject) fo;

            MfLock l = (mfo.lock == null) ? null : mfo.lock.get();

            if (l != null) {
                // the file has been locked => update the lock
                mfo.writable(true);
            }

            fo = fo.getParent();
        }
    }

    //
    // List
    //

    /** Method that allows subclasses to return its children.
    *
    * @return names (name . ext) of subfiles
    */
    protected final String[] list() {
        Properties exclude = new Properties();
        List<String> addList = new LinkedList<String>();
        Set<String> addSet = new HashSet<String>(101);

        Enumeration<FileObject> it = delegates();

        while (it.hasMoreElements()) { // cycle 1

            FileObject folder = it.nextElement();

            if ((folder == null) || !folder.isFolder()) {
                continue;
            }

            FileObject[] arr = folder.getChildren();
            Properties local = null;

            for (int i = 0; i < arr.length; i++) {
                String name = arr[i].getNameExt();

                if (name.endsWith(MultiFileSystem.MASK)) {
                    String basename = name.substring(0, name.length() - MultiFileSystem.MASK.length());

                    // this name should be excluded from next rounds of cycle 1
                    if (local == null) {
                        local = new Properties();
                        local.putAll(exclude);
                    }

                    local.setProperty(basename, basename);

                    if (!getMultiFileSystem().getPropagateMasks()) {
                        // By default, unused mask files are not displayed as file children.
                        // When propagate masks is turned on, though, they should be.
                        // This is useful e.g. when using MFSs as delegates for other MFSs.
                        continue;
                    }
                }

                // lets add this name, if not added yet and is not excluded by
                // previous level
                if (!addSet.contains(name) && (exclude.getProperty(name) == null)) {
                    addSet.add(name);
                    addList.add(name);
                }
            }

            // change the excludes to the new ones produced at this round
            if (local != null) {
                exclude = local;
            }
        }

        if (getMultiFileSystem().getPropagateMasks()) {
            // remove all masked files from the array, even if they were
            // masked on the same level as they were added
            addList.removeAll(exclude.keySet());
        }

        String[] res = addList.toArray(new String[addList.size()]);

        return res;
    }

    /** When refreshing, also update the state of delegates.
    */
    /** [PENDING] expected rename of some refresh method */

    /*protected void internalRefresh (
        String add, String remove, boolean fire, boolean expected, String[] list
     **/
    protected void refresh(String add, String remove, boolean fire, boolean expected) {
        try {
            getFileSystem().beginAtomicAction();

            synchronized (this) {
                update();

                /** [PENDING] expected rename of some refresh method */

                //super.internalRefresh (add, remove, fire, expected, list);
                super.refresh(add, remove, fire, expected);
            }

            validFlag &= leader.isValid();
        } finally {
            getFileSystem().finishAtomicAction();
        }
    }

    /** Method to create a file object for given subfile.
    * @param name of the subfile
    * @return the file object
    */
    protected final AbstractFolder createFile(String name) {
        return new MultiFileObject(getMultiFileSystem(), this, name);
    }

    //
    // Info
    //

    /* Test whether this object is a folder.
    * @return true if the file object is a folder (i.e., can have children)
    */
    public boolean isFolder() {
        return (parent == null) || leader.isFolder();
    }

    /*
    * Get last modification time.
    * @return the date
    */
    public java.util.Date lastModified() {
        return leader.lastModified();
    }

    /* Test whether this object is a data object.
    * This is exclusive with {@link #isFolder}.
    * @return true if the file object represents data (i.e., can be read and written)
    */
    public boolean isData() {
        return leader.isData();
    }

    /* Test whether this FileObject is Virtual.
    * @return true if the file object represents a Virtual File
    */
    public boolean isVirtual() {
        return leader.isVirtual();
    }

    @Deprecated // have to override for compat
    public boolean isReadOnly() {
        MultiFileSystem fs = getMultiFileSystem();

        if (fs.isReadOnly()) {
            return true;
        }

        if (leader.isReadOnly()) {
            // if we can make it writable then nothing
            try {
                FileSystem simple = fs.createWritableOn(getPath());

                return simple == leader.getFileSystem();
            } catch (IOException e) {
                return true;
            }
        }

        return false;
    }

    public boolean canWrite() {
        MultiFileSystem fs = getMultiFileSystem();

        if (fs.isReadOnly()) {
            return false;
        }

        if (!leader.canWrite()) {
            // if we can make it writable then nothing
            try {
                FileSystem simple = fs.createWritableOn(getPath());

                return simple != leader.getFileSystem();
            } catch (IOException e) {
                return false;
            }
        }

        return true;
    }

    /* Get the MIME type of this file.
    * The MIME type identifies the type of the file's contents and should be used in the same way as in the <B>Java
    * Activation Framework</B> or in the {@link java.awt.datatransfer} package.
    * <P>
    * The default implementation calls {@link FileUtil#getMIMEType}.
    *
    * @return the MIME type textual representation, e.g. <code>"text/plain"</code>
    */
    public String getMIMEType() {
        return leader.getMIMEType();
    }

    /* Get the size of the file.
    * @return the size of the file in bytes or zero if the file does not contain data (does not
    *  exist or is a folder).
    */
    public long getSize() {
        return leader.getSize();
    }

    /** Get input stream.
    * @return an input stream to read the contents of this file
    * @exception FileNotFoundException if the file does not exists, is a folder
    * rather than a regular file  or is invalid
    */
    public InputStream getInputStream() throws java.io.FileNotFoundException {
        return leader.getInputStream();
    }

    /* Get output stream.
    * @param lock the lock that belongs to this file (obtained by a call to
    *   {@link #lock})
    * @return output stream to overwrite the contents of this file
    * @exception IOException if an error occures (the file is invalid, etc.)
    */
    public OutputStream getOutputStream(FileLock lock)
    throws java.io.IOException {
        MfLock l;
        FileLock lWritable;
        FileObject fo;

        try {
            getFileSystem().beginAtomicAction(markAtomicAction);

            synchronized (this) {
                l = testLock(lock);

                // this can also change lock in l.lock
                fo = writable(false);
                lWritable = l.findLock(fo);
            }

            return fo.getOutputStream(lWritable);
        } finally {
            getFileSystem().finishAtomicAction();
        }
    }

    @Override
    public synchronized boolean isLocked() {
        return lock != null && lock.get() != null;
    }
    
    /* Lock this file.
    * @return lock that can be used to perform various modifications on the file
    * @throws FileAlreadyLockedException if the file is already locked
    */
    public synchronized FileLock lock() throws IOException {
        if (lock != null) {
            FileLock f = (FileLock) lock.get();

            if (f != null) {
                FileAlreadyLockedException alreadyLockedException = new FileAlreadyLockedException(getPath());
                alreadyLockedException.initCause(lockedBy);
                throw alreadyLockedException;
            }
        }

        Set<? extends FileSystem> set = getMultiFileSystem().createLocksOn(getPath());
        MfLock l = new MfLock(leader, delegates(), set);

        lock = new WeakReference<MfLock>(l);

        assert (lockedBy = new Throwable("Locked by:")) != null;  //NOI18N
        return l;
    }

    /** Tests the lock if it is valid, if not throws exception.
    * @param l lock to test
    * @return the mf lock for this file object
    */
    private MfLock testLock(FileLock l) throws java.io.IOException {
        if (lock == null) {
            throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_InvalidLock",
                    l, getPath(), getMultiFileSystem().getDisplayName(), lock));
        }

        if (lock.get() != l) {
            throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_InvalidLock",
                    l, getPath(), getMultiFileSystem().getDisplayName(), lock.get()));
        }

        return (MfLock) l;
    }

    @Deprecated // have to override for compat
    public void setImportant(boolean b) {
        Enumeration<FileObject> en = delegates();

        while (en.hasMoreElements()) {
            FileObject fo = en.nextElement();
            fo.setImportant(b);
        }

        if (!b) {
            getMultiFileSystem().markUnimportant(this);
        }
    }

    /** Add one void-wrapping to an object. */
    private static final Object voidify(Object o) {
        if (o == null) {
            return new VoidValue(0);
        } else if (o instanceof VoidValue) {
            VoidValue vv = (VoidValue) o;

            return new VoidValue(vv.level + 1);
        } else {
            return o;
        }
    }

    /** Strip off one void-wrapping from an object. */
    private static final Object devoidify(Object o) {
        if (o instanceof VoidValue) {
            VoidValue vv = (VoidValue) o;

            if (vv.level == 0) {
                return null;
            } else {
                return new VoidValue(vv.level - 1);
            }
        } else {
            return o;
        }
    }

    /* Get the file attribute with the specified name.
    * @param attrName name of the attribute
    * @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason)
    */
    @Override
    public Object getAttribute(String attrName) {
        // Performance optimization (avoid calling getPath() too many times):
        final String path = getPath();
        if (path.isEmpty() && attrName.indexOf('\\') >= 0) {
            return null;
        }
        return getAttribute(attrName, path);
    }

    /** Special attributes which should not be checked for weight. See RemoveWritablesTest. */
    private static final Set<String> SPECIAL_ATTR_NAMES = new HashSet<String>(Arrays.asList(FileObject.REMOVE_WRITABLES_ATTR, WEIGHT_ATTRIBUTE, "java.io.File")); // NOI18N
    private final Object getAttribute(String attrName, String path) {
        // Look for attribute in any file system starting at the front.
        // Additionally, look for attribute in root folder, where
        // the relative path from the folder to the target file is
        // prepended to the attribute name, all separated with slashes.
        // This search scheme permits writable front systems to set file
        // attributes on deeply buried files in back systems without
        // actually creating copies of the files or even their parent folders.
        // [PENDING] consider the effects of mask files
        String prefixattr = ((path.length() == 0) ? null : (path.replace('/', '\\') + '\\' + attrName));

        { /*There was proved that this block enhances performance*/

            Object oPerf;
            FileObject localFo = getAttributeCache().getDelegate();
            String cachedAttrName = getAttributeCache().getAttributeName();

            if ((localFo != null) && !localFo.equals(this) && cachedAttrName.equals(attrName)) {
                if (localFo.isRoot() && (prefixattr != null)) {
                    try {
                        FileSystem foFs = localFo.getFileSystem();
                        if (!foFs.isReadOnly() || getMultiFileSystem().canHaveRootAttributeOnReadOnlyFS(prefixattr)) {
                            localFo = foFs.getRoot();
                            oPerf = getAttribute(localFo, prefixattr, ""); // NOI18N

                            if (oPerf != null) {
                                return devoidify(oPerf);
                            }
                        }
                    } catch (FileStateInvalidException fiex) {
                        //then continue
                    }
                }

                /** There is no chance to cache localFo.getPath(), because every
                 *  rename up to it in hierarchy makes this cache invalid.
                 */
                oPerf = getAttribute(localFo, attrName, localFo.getPath());

                if (oPerf != null) {
                    return devoidify(oPerf);
                }
            }
        }

        FileSystem[] systems = getMultiFileSystem().getDelegates();

        Number maxWeight = 0;
        Object attr = null;
        FileSystem writable = getMultiFileSystem().writableLayer(path);
        
        boolean revealEntries = isFolder() && "revealEntries".equals(attrName) &&
                writable != null && !writable.isReadOnly(); //NOI18N
        
        if (revealEntries) {
            return collectRevealedFiles();
        }

        //        boolean isLoaderAttr = /* DataObject.EA_ASSIGNED_LOADER */ "NetBeansAttrAssignedLoader".equals (attrName); // NOI18N                
        for (int i = 0; i < systems.length; i++) {
            if (systems[i] == null) {
                continue;
            }

            // Don't check for any assigned loader overrides except for leader & writable systems.
            // Note that this prevents front layers from overriding default loader!
            // Could be revisited but in the meantime this is a performance optimization.
            //            if (isLoaderAttr && leaderfs != null && systems[i] != leaderfs && systems[i].isReadOnly ()) {
            //                continue;
            //            }
            // The normal check:
            FileObject fo = getMultiFileSystem().findResourceOn(systems[i], path);

            if (fo != null) {
                Object o = getAttribute(fo, attrName, fo.getPath()); // Performance tricks:                

                if (o != null) {
                    if (SPECIAL_ATTR_NAMES.contains(attrName)) {
                        return devoidify(o);
                    }
                    Number weight = weightOf(fo, writable);
                    if (attr == null || weight.doubleValue() > maxWeight.doubleValue()) {
                        getAttributeCache().setDelegate(fo);
                        getAttributeCache().setAttributeName(attrName);
                        attr = o;
                        maxWeight = weight;
                    }
                }
            } 

            if (prefixattr != null) {
                if (
                    !systems[i].isReadOnly() ||
                    getMultiFileSystem().canHaveRootAttributeOnReadOnlyFS(prefixattr)
                ) {
                    fo = systems[i].getRoot();

                    Object o = getAttribute(fo, prefixattr, ""); // NOI18N

                    if (o != null) {
                        Number weight = weightOf(fo, writable);
                        if (attr == null || weight.doubleValue() > maxWeight.doubleValue()) {
                            getAttributeCache().setDelegate(fo);
                            getAttributeCache().setAttributeName(attrName);
                            attr = o;
                            maxWeight = weight;
                        }
                    }
                }
            }
        }

        return devoidify(attr);
    }

    private static boolean sameFullName(FileObject f1, FileObject f2) {
        while (f1 != null && f2 != null) {
            if (!f1.getNameExt().equals(f2.getNameExt())) {
                return false;
            }
            f1 = f1.getParent();
            f2 = f2.getParent();
        }
        return f1 == null && f2 == null;
    }

    private Object getAttribute(FileObject fo, String attrName, String path) {
        Object o;

        FileObject previousFO = attrAskedFileObject.get();

        if (previousFO == null || !sameFullName(previousFO, this))  {
            attrAskedFileObject.set(this);
        }

        try {
            if (fo instanceof MultiFileObject) {
                o = ((MultiFileObject) fo).getAttribute(attrName, path);
            } else if (fo instanceof AbstractFileObject) {
                o = ((AbstractFileObject) fo).getAttribute(attrName, path);
            } else {
                o = fo.getAttribute(attrName);
            }
        } finally {
            attrAskedFileObject.set(previousFO);
        }
        return o;
    }

    /* Set the file attribute with the specified name.
    * @param attrName name of the attribute
    * @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular file systems may or may not use serialization to store attribute values.
    * @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}.
    */
    public void setAttribute(String attrName, Object value)
    throws IOException {
        setAttribute(attrName, value, true); //NOI18N        
    }

    /* helper method for MFO.setAttribute. MFO can disable firing from underlaying
     * layers. Should be reviewed in 3.4 or later
      *@see MultiFileObject#setAttribute*/
    void setAttribute(String attrName, Object value, boolean fire)
    throws IOException {
        // Similar to getAttribute. Here we use createWritableOn to decide which fs
        // the attribute should be stored on; it is stored on the actual file if
        // possible, if not then the lowest containing folder.
        String path = getPath();
        FileSystem fs = getMultiFileSystem().createWritableOn(path);
        FileObject fo = getMultiFileSystem().findResourceOn(fs, path);
        Object oldValue = null;
        String attrToSet = attrName;

        if (fire) {
            oldValue = getAttribute(attrName);
            if (oldValue == value) {
                return;
            }
        }

        if (fo == null) {
            fo = fs.getRoot();
            attrToSet = path.replace('/', '\\') + '\\' + attrName;
        }

        getAttributeCache().setDelegate(fo);
        getAttributeCache().setAttributeName(attrToSet);

        if (value == null) {
            fire = setAttributeOnFO(fo, attrToSet, null, fire);
            if (fo.getAttribute(attrToSet) != null) {
                fire = setAttributeOnFO(fo, attrToSet, voidify(null), fire);
            }
        } else {
            fire = setAttributeOnFO(fo, attrToSet, voidify(value), fire);
        }

        // fire changes for original attribute name even if the attr is actually
        // stored in the root FO if FO instanceof AbstractFolder (that has supressed firing).

        /* [PENDING] only this MultiFileObject should fire event and it`s delegate
         * should not fire event (nevertheless this delegate is physically used
         * to write given attribute - only implementation detail -
         * nobody should rely on current implementation).
         **/
        if (fire && (oldValue != value) && hasAtLeastOneListeners()) {
            fileAttributeChanged0(new FileAttributeEvent(this, attrName, oldValue, value));
        }
    }

    /* Get all file attribute names for this file.
    * @return enumeration of keys (as strings)
    */
    public Enumeration<String> getAttributes() {
        return getAttributes(getPath());
    }

    private final Enumeration<String> getAttributes(String path) {
        Set<String> s = new HashSet<String>();
        FileSystem[] systems = getMultiFileSystem().getDelegates();
        final boolean empty = path.length() == 0;

        // [PENDING] will not remove from the enumeration voided-out attributes
        // (though this is probably not actually necessary)
        String prefix = empty ? null : (path.replace('/', '\\') + '\\');

        for (int i = 0; i < systems.length; i++) {
            if (systems[i] == null) {
                continue;
            }

            FileObject fo = getMultiFileSystem().findResourceOn(systems[i], path);

            if (fo != null) {
                Enumeration<String> e = fo.getAttributes();

                while (e.hasMoreElements()) {
                    String attr = e.nextElement();
                    if (empty && attr.indexOf('\\') >= 0) {
                        continue;
                    }
                    s.add(attr);
                }
            }

            if (prefix != null) {
                fo = systems[i].getRoot();

                Enumeration<String> e;

                if (fo instanceof MultiFileObject) {
                    e = ((MultiFileObject) fo).getAttributes(""); // NOI18N
                } else if (fo instanceof AbstractFileObject) {
                    e = ((AbstractFileObject) fo).getAttributes(""); // NOI18N
                } else {
                    e = fo.getAttributes();
                }

                while (e.hasMoreElements()) {
                    String attr = e.nextElement();

                    if (attr.startsWith(prefix) && (attr.substring(prefix.length()).indexOf('\\') == -1)) {
                        s.add(attr.substring(prefix.length()));
                    }
                }
            }
        }

        return Collections.enumeration(s);
    }

    /* Create a new folder below this one with the specified name. Fires
    * <code>fileCreated</code> event.
    *
    * @param name the name of folder to create (without extension)
    * @return the new folder
    * @exception IOException if the folder cannot be created (e.g. already exists)
    */
    @Override
    public FileObject createFolder(String name) throws IOException {
        MultiFileObject fo;

        try {
            getFileSystem().beginAtomicAction();

            synchronized (this) {
                MultiFileSystem fs = getMultiFileSystem();

                if (fs.isReadOnly()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FSisRO", fs.getDisplayName()));
                }

                if (isReadOnly()) {
                    IOException ex = new IOException("Read only: " + leader + " delegates: " + delegates); // NOI18N
                    Exceptions.attachLocalizedMessage(ex, NbBundle.getMessage(MultiFileObject.class, "EXC_FisRO", name, fs.getDisplayName()));
                    throw ex;
                }

                String fullName = getPath() + PATH_SEP + name;

                if (!isFolder()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FoNotFolder", name, getPath(), fs.getDisplayName()));
                }

                if (this.getFileObject(name) != null) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FolderAlreadyExist", name, fs.getDisplayName(), getPath()));
                }

                FileSystem simple = fs.createWritableOn(fullName);

                // create
                FileUtil.createFolder(root(simple), fullName);

                // try to unmask if necessary
                getMultiFileSystem().unmaskFileOnAll(simple, fullName);

                /** [PENDING] expected rename of some refresh method */
                //internalRefresh (name, null, true, false,null);
                refresh(name, null, true, false);

                fo = getMultiChild(name);

                if (fo == null) {
                    // system error
                    throw new FileStateInvalidException(NbBundle.getMessage(MultiFileObject.class, "EXC_ApplicationCreateError", getPath(), name));
                }

                FileObject[] chlds = fo.getChildren();

                for (int i = 0; i < chlds.length; i++) {
                    getMultiFileSystem().maskFile(simple, chlds[i].getPath());
                }

                if (hasListeners()) {
                    fileCreated0(new FileEvent(this, fo), false);
                }
            }
        } finally {
            getFileSystem().finishAtomicAction();
        }

        return fo;
    }

    /* Create new data file in this folder with the specified name. Fires
    * <code>fileCreated</code> event.
    *
    * @param name the name of data object to create (should not contain a period)
    * @param ext the extension of the file (or <code>null</code> or <code>""</code>)
    *
    * @return the new data file object
    * @exception IOException if the file cannot be created (e.g. already exists)
    */
    public FileObject createData(String name, String ext)
    throws IOException {
        MultiFileObject fo;

        try {
            getFileSystem().beginAtomicAction();

            synchronized (this) {
                MultiFileSystem fs = getMultiFileSystem();

                if (fs.isReadOnly()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FSisRO", fs.getDisplayName()));
                }

                if (isReadOnly()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FisRO", name, fs.getDisplayName()));
                }

                String n = "".equals(ext) ? name : (name + EXT_SEP + ext); // NOI18N

                if (!isFolder()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FoNotFolder", n, getPath(), fs.getDisplayName()));
                }

                if (this.getFileObject(name, ext) != null) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_DataAlreadyExist", n, fs.getDisplayName(), getPath()));
                }

                String fullName = getPath() + PATH_SEP + n;

                FileSystem simple = fs.createWritableOn(fullName);

                // create
                FileUtil.createData(root(simple), fullName);

                // try to unmask if necessary
                getMultiFileSystem().unmaskFileOnAll(simple, fullName);

                /** [PENDING] expected rename of some refresh method */
                //internalRefresh (n, null, true, false,null);
                refresh(n, null, true, false);

                fo = getMultiChild(n);

                if (fo == null) {
                    // system error
                    throw new FileStateInvalidException(NbBundle.getMessage(
                            MultiFileObject.class,
                            "EXC_ApplicationCreateErrorMulti", //NOI18N
                            getPath(), n, simple,
                            simple.getRoot().getFileObject(fullName)));
                }

                if (hasListeners()) {
                    fileCreated0(new FileEvent(this, fo), true);
                }
            }
        } finally {
            getFileSystem().finishAtomicAction();
        }

        return fo;
    }

    /* Renames this file (or folder).
    * Both the new basename and new extension should be specified.
    * <p>
    * Note that using this call, it is currently only possible to rename <em>within</em>
    * a parent folder, and not to do moves <em>across</em> folders.
    * Conversely, implementing file systems need only implement "simple" renames.
    * If you wish to move a file across folders, you should call {@link FileUtil#moveFile}.
    * @param lock File must be locked before renaming.
    * @param name new basename of file
    * @param ext new extension of file (ignored for folders)
    */
    public void rename(FileLock lock, String name, String ext)
    throws IOException {
        MultiFileSystem fs = getMultiFileSystem();

        if (parent == null) {
            throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_CannotRenameRoot", fs.getDisplayName()));
        }

        try {
            getFileSystem().beginAtomicAction();

            synchronized (parent) {
                // synchronize on your folder
                MfLock l = testLock(lock);

                String newFullName = parent.getPath() + PATH_SEP + name;

                if (isData() && ext != null && ext.trim().length() > 0) {
                    newFullName += (EXT_SEP + ext);
                }

                String oldFullName = getPath();

                if (isReadOnly()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_CannotRenameFromTo",
                            getPath(), getMultiFileSystem().getDisplayName(), newFullName));
                }

                if (getFileSystem().isReadOnly()) {
                    throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FSisRO", getMultiFileSystem().getDisplayName()));
                }

                String on = getName();
                String oe = getExt();

                //!!!      getMultiFileSystem ().change.rename (oldFullName, newFullName);
                FileSystem single = fs.createWritableOnForRename(oldFullName, newFullName);

                if (single == leader.getFileSystem()) {
                    // delete the file if we can on the selected
                    // system
                    leader.rename(l.findLock(leader), name, ext);
                    getMultiFileSystem().unmaskFileOnAll(single, newFullName);
                    copyContent(this, leader);
                } else {
                    // rename file that is on different file system
                    // means to copy it
                    FileObject previousLeader = leader;

                    if (isData()) {
                        // data
                        FileObject folder = FileUtil.createFolder(root(single), getParent().getPath());
                        leader = leader.copy(folder, name, ext);
                        copyAttrs(this, leader);
                    } else {
                        // folder
                        FileObject fo = FileUtil.createFolder(root(single), newFullName);
                        copyContent(this, fo);

                        leader = fo;
                        this.name = name; // must be done before update                        
                        update();
                    }

                    // releases lock for previousLeader and aquiares 
                    // new for leader
                    l.changeLocks(previousLeader, leader);
                }

                if (getMultiFileSystem().delegates(oldFullName).hasMoreElements()) {
                    // if there is older version of the file
                    // then we have to mask it
                    getMultiFileSystem().maskFile(single, oldFullName);
                    updateFoldersLock(getParent());
                }

                if (isData()) {
                    name = name + EXT_SEP + ext;
                }

                String oldName = this.name;
                this.name = name;

                /*
                      System.out.println ("Resulting file is: " + getPath ());
                      System.out.println ("Bedw      file is: " + newFullName);
                      System.out.println ("Name: " + name);
                      System.out.println ("Old : " + oldName);
                */

                /** [PENDING] expected to delete*/
                parent.refresh(name, oldName);

                //!!!      getMultiFileSystem ().attr.renameAttributes (oldFullName, newFullName);
                if (hasAtLeastOneListeners()) {
                    fileRenamed0(new FileRenameEvent(this, on, oe));
                }
            }
        } finally {
            getFileSystem().finishAtomicAction();
        }
    }

    /* 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
    */
    void handleDelete(FileLock lock) throws IOException {
        if (parent == null) {
            throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_CannotDeleteRoot", getMultiFileSystem().getDisplayName()));
        }

        MultiFileSystem fs = getMultiFileSystem();

        try {
            getFileSystem().beginAtomicAction();

            synchronized (parent) {
                String fullName = getPath();
                FileSystem single = fs.createWritableOn(fullName);

                if (needsMask(lock, true)) {
                    getMultiFileSystem().maskFile(single, fullName);
                    updateFoldersLock(getParent());
                }

                String n = name;
                validFlag = false;

                /** [PENDING] expected rename of some refresh method */
                //parent.internalRefresh (null, n, true, false, null);
                parent.refresh(null, n, true, false);

                if (hasAtLeastOneListeners()) {
                    fileDeleted0(new FileEvent(this));
                }
            }
        } finally {
            getFileSystem().finishAtomicAction();
        }
    }

    //
    // Transfer
    //

    /** Copies this file. This allows the filesystem to perform any additional
    * operation associated with the copy. But the default implementation is simple
    * copy of the file and its attributes
    *
    * @param target target folder to move this file to
    * @param name new basename of file
    * @param ext new extension of file (ignored for folders)
    * @return the newly created file object representing the moved file
    */
    public FileObject copy(FileObject target, String name, String ext)
    throws IOException {
        if (FileUtil.isParentOf(this, target)) {
            throw new FSException(NbBundle.getMessage(FileObject.class, "EXC_CopyChild", this, target)); // NOI18N
        }
        return leader.copy(target, name, ext);
    }

    /** Moves this file. This allows the filesystem to perform any additional
    * operation associated with the move. But the default implementation is encapsulated
    * as copy and delete.
    *
    * @param lock File must be locked before renaming.
    * @param target target folder to move this file to
    * @param name new basename of file
    * @param ext new extension of file (ignored for folders)
    * @return the newly created file object representing the moved file
    */
    public FileObject move(FileLock lock, FileObject target, String name, String ext)
    throws IOException {
        if (FileUtil.isParentOf(this, target)) {
            throw new FSException(NbBundle.getMessage(FileObject.class, "EXC_MoveChild", this, target)); // NOI18N
        }
        MultiFileSystem fs = getMultiFileSystem();

        try {
            fs.beginAtomicAction();

            if (parent == null) {
                throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_CannotDeleteRoot", fs.getDisplayName()));
            }

            MfLock lck = testLock(lock);
            FileLock l = lck.findLock(leader);

            FileSystem simple = fs.createWritableOn(getPath());

            if (fs.isReadOnly()) {
                throw new FSException(NbBundle.getMessage(MultiFileObject.class, "EXC_FSisRO", fs.getDisplayName()));
            }

            if ((l == null) && (leader.getFileSystem() != simple)) {
                leader = writable(true);
                l = lck.findLock(leader);
            }

            if (needsMask(lock, false)) {
                getMultiFileSystem().maskFile(simple, getPath());
                updateFoldersLock(getParent());
            }
            FileObject ret = leader.move(l, target, name, ext);
            FileObjectLkp.reassign(this, ret);
            return ret;
        } finally {
            fs.finishAtomicAction();
        }
    }

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

        Enumeration<FileObject> en = delegates();

        while (en.hasMoreElements()) {
            FileObject fo = en.nextElement();
            fo.refresh(expected);
        }

        super.refresh(expected);
    }

    //
    // Listeners
    //

    /** Fired when a new folder is created. This action can only be
     * listened to in folders containing the created folder up to the root of
     * file system.
     *
     * @param fe the event describing context where action has taken place
     */
    public void fileFolderCreated(FileEvent fe) {
        /*One of underlaing layers notifies that new folder was created.
         And this folder may have any other childern anywhere deep in hierarchy
         and then must be updated and refreshed deep down*/
        updateAll();
    }

    /** Fired when a new file is created. This action can only be
     * listened in folders containing the created file up to the root of
     * file system.
     *
     * @param fe the event describing context where action has taken place
     */
    public void fileDataCreated(FileEvent fe) {
        refreshAfterEvent(fe);
    }

    /** Fired when a file is changed.
     * @param fe the event describing context where action has taken place
     */
    public void fileChanged(FileEvent fe) {
        FileObject changedFile = this;

        if (fe.getSource().equals(leader) && hasAtLeastOneListeners() && !fe.firedFrom(markAtomicAction)) {
            /**There should not dissapear information about source and changed file*/
            if (!fe.getFile().equals(fe.getSource())) {
                changedFile = getFileObject(fe.getFile().getName(), fe.getFile().getExt());
            }

            /**fileChanged1 - should not fire event for this.getParent ().
             * I think that already bottom layer forked event.*/
            /** [PENDING] fix of #16926, #16895. But there should be investigated
             *  why this MFO doesn`t know about child ?*/
            if (changedFile != null) {
                fileChanged1(new FileEvent(this, changedFile, fe.getTime()));
            }
        }
    }

    /** Fired when a file is deleted.
     * @param fe the event describing context where action has taken place
     */
    public void fileDeleted(FileEvent fe) {
        if (fe.getFile().isFolder()) {
            updateAll();
        } else {
            refreshAfterEvent(fe);
        }
    }

    /** Fired when a file is renamed.
     * @param fe the event describing context where action has taken place
     *           and the original name and extension.
     */
    public void fileRenamed(FileRenameEvent fe) {
        updateAll();
    }

    /** Fired when a file attribute is changed.
     * @param fe the event describing context where action has taken place,
     *           the name of attribute and the old and new values.
     */
    public void fileAttributeChanged(FileAttributeEvent fe) {
        // [PENDING] this is not at all sufficient to notify every change in attributes.
        // One, parent dirs of front filesystems can now hold attributes for missing
        // files. Two, non-leader files can have attributes too which are merged in.
        // In principle all files/folders whose path is a prefix of this path on all
        // contained filesystems should be listened to for attribute change events.
        if (!hasAtLeastOneListeners() || (leader == null)) {
            return;
        }

        /** If change is not fired from leader then leader may mask this attribute
         *  and then event should not be fired */
        if (!fe.getFile().equals(leader) && (fe.getName() != null) && (leader.getAttribute(fe.getName()) != null)) {
            return;
        }

        /** If change is not fired from leader then another delegate may mask this attribute
         *  and then event should not be fired. */
        if (
            !fe.getFile().equals(leader) && (fe.getNewValue() != null) && (fe.getName() != null) &&
                !fe.getNewValue().equals(getAttribute(fe.getName()))
        ) {
            return;
        }
        final FileAttributeEvent ev = new FileAttributeEvent(this, fe.getName(), fe.getOldValue(), fe.getNewValue());
        try {
            ev.inheritPostNotify(fe);
            fileAttributeChanged0(ev);
        } finally {
            ev.setPostNotify(null);
        }
    }

    /** Copies content of one folder into another.
    * @param source source folder
    * @param target target folder
    * @exception IOException if it fails
    */
    private static void copyContent(FileObject source, FileObject target)
    throws IOException {
        FileObject[] srcArr = source.getChildren();

        copyAttrs(source, target); //added

        for (int i = 0; i < srcArr.length; i++) {
            FileObject child = srcArr[i];

            if (MultiFileSystem.isMaskFile(child)) {
                continue;
            }

            if (target.getFileObject(child.getName(), child.getExt()) == null) {
                if (child.isData()) {
                    FileObject fo = FileUtil.copyFile(child, target, child.getName(), child.getExt());

                    if (fo != null) {
                        copyAttrs(child, fo);
                    }
                } else {
                    FileObject targetChild = target.createFolder(child.getName());
                    copyContent(child, targetChild);
                }
            }
        }
    }

    /** Copies attributes of one FileObject into another.
    * @param source source folder or file
    * @param target target folder or file
    * @exception IOException if it fails
    */
    private static void copyAttrs(FileObject source, FileObject target) {
        Enumeration<String> en = source.getAttributes();

        while (en.hasMoreElements()) {
            String key = en.nextElement();
            Object value = source.getAttribute(key);

            try {
                target.setAttribute(key, value);
            } catch (IOException ie) {
            }
        }
    }

    /**
     * auxiliary method that returns true if mask is needed and deletes all delegates
     * on writable layers if deleteDelegates is true.
     * @param lock
     * @param deleteDelegates if true all delegates on writable layers will be deleted
     * @throws IOException is thrown  if lock is not valid.
     * @return  true if mask is necessary*/
    private boolean needsMask(FileLock lock, boolean deleteDelegates)
    throws IOException {
        MfLock lck = testLock(lock);
        Enumeration<FileObject> e = getMultiFileSystem().delegates(getPath());
        boolean needsMask = false;

        while (e.hasMoreElements()) {
            FileObject fo = e.nextElement();
            FileLock lockForFo = lck.findLock(fo);

            if (lockForFo == null) {
                // we will need to create mask
                needsMask = true;
            } else {
                if (deleteDelegates) {
                    fo.delete(lockForFo);
                }
            }
        }

        return needsMask;
    }

    /** Finds a root for given file system. It also counts with
    * redefined method findResourceOn.
    *
    * @param fs the filesystem to seach on
    * @return the root on the fs
    */
    private FileObject root(FileSystem fs) {
        return getMultiFileSystem().findResourceOn(fs, ""); // NOI18N
    }

    final FileObject getLeader() {
        return leader;
    }

    /** Special value used to indicate null masking of an attribute.
     * The level is zero in simple cases; incremented when one MFS asks
     * another to store a VoidValue.
     */
    static final class VoidValue implements Externalizable {
        // Externalizable:
        private static final long serialVersionUID = -2743645909916238684L;
        int level;

        VoidValue(int level) {
            this.level = level;
        }

        public VoidValue() {
        }

        public String toString() {
            return "org.openide.filesystems.MultiFileObject.VoidValue#" + level; // NOI18N
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeInt(level);
        }

        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            level = in.readInt();
        }
    }

    /** Implementation of lock for abstract files.
    */
    private class MfLock extends FileLock {
        /** lock for all files (map from FileObject to FileLock) */
        private Map<FileObject, FileLock> map = new HashMap<FileObject, FileLock>(11);

        /**
        * @param leader leader file object
        * @param delegates all delegates for this file object
        * @param systems a set of filesystems we should create lock on
        * @exception IOException if the lock cannot be obtained
        */
        public MfLock(FileObject leader, Enumeration<FileObject> delegates, Set<? extends FileSystem> systems)
        throws IOException {
            while (delegates.hasMoreElements()) {
                FileObject fo = delegates.nextElement();

                if (systems.contains(fo.getFileSystem())) {
                    FileLock l = fo.lock();
                    map.put(fo, l);
                }
            }

            /* JST: Commented out because cause problems when locking a file that
                    is only of filesystems that are readonly (for example when
                    file is only on shared installation and locks are allowed only
                    on local => then there is nothing to lock.

                      if (map.isEmpty()) {
                          // trouble, the filesystem returned wrong set of systems to lock
                          // on => warn about possibly wrong implementation
                          // to correct the problem override appropriatelly createLocksOn
                          // method of MultiFileSystem
                          throw new IOException ("Writable file is not on filesystem from createLocksOn"); // NOI18N
                          }
            */
        }

        /** Finds lock for given file object.
        * @param fo one of delegates
        * @return the lock or null
        */
        public FileLock findLock(FileObject fo) {
            return map.get(fo);
        }

        /** Adds another lock into this lock.
        * @param fo file object to keep the lock for
        * @exception IOException if the lock cannot be obtained
        */
        public void addLock(FileObject fo) throws IOException {
            map.put(fo, fo.lock());
        }

        /** Releases lock for old file object and
        * takes new one from newFo
        */
        public void changeLocks(FileObject old, FileObject n)
        throws IOException {
            FileLock l = map.remove(old);

            if (l != null) {
                l.releaseLock();
            }

            addLock(n);
        }

        public void releaseLock() {
            if (this.isValid()) {
                super.releaseLock();
                releaseLockForDelegates();

                if (getCurrentMfLock() == this) {
                    // clears the reference to this lock from the file object
                    MultiFileObject.this.lock = null;
                }
            }
        }

        private FileLock getCurrentMfLock() {
            FileLock currentLock = null;
            ;

            if (lock != null) {
                currentLock = (FileLock) lock.get();
            }

            return currentLock;
        }

        private void releaseLockForDelegates() {
            for (FileLock l : map.values()) {
                l.releaseLock();
            }

            map.clear();
        }

        // for better debugging
        public String toString() {
            return super.toString() + " for " + MultiFileObject.this + " valid=" + isValid(); // NOI18N
        }
    }    
    // MfLock
    
    private static class AttributeCache {
        private FileObject delegate;
        private String attribName = ""; // NOI18N        
        
        private void free() {
            delegate = null;
            attribName = ""; // NOI18N            
        }
        
        private void setDelegate(FileObject delegate) {
            this.delegate = delegate;            
        }
        
        private void setAttributeName(String attribName) {
            this.attribName = attribName;
        }
                        
        private FileObject getDelegate() {
            return delegate;
        }
        
        private String getAttributeName() {
            return attribName;
        }
    }
    
    /**
     * Revealed FileObject represents a FileObject which has been either overriden by
     * the writable layer, or masked out by the layer. It delegates to the original object,
     * and its Callable mixing interface allows to revert the change by removing the 
     * override file.
     */
    private final class RevealedFileObject extends FileObject implements Callable<FileObject> {
        /**
         * The masking file. It may be either _hidden file, or a replacing
         * file on the writable layer
         */
        private final FileObject  maskFile;
        
        /**
         * Original file on lower layer
         */
        private final FileObject  delegate;

        public RevealedFileObject(FileObject parent, FileObject delegate) {
            this.maskFile = parent;
            this.delegate = delegate;
        }

        @Override
        public Object getAttribute(String attrName) {
            return delegate.getAttribute(attrName);
        }

        /**
         * Return the revived file from the MFS
         * @return the revived file, if the operation succeeds.
         * @throws IOException from the deletion of the masking file.
         */
        @Override
        public FileObject call() throws IOException {
            if (maskFile.isValid()) {
                maskFile.delete();
            }
            return MultiFileObject.this.getFileObject(maskFile.getNameExt());
        }

        @Override
        public String getName() {
            return delegate.getName();
        }

        @Override
        public String getExt() {
            return delegate.getExt();
        }

        @Override
        public void rename(FileLock lock, String name, String ext) throws IOException {
            throw new IOException("Unsupported oepration"); // NOI18N
        }

        @Override
        public FileObject copy(FileObject target, String name, String ext) throws IOException {
            return delegate.copy(target, name, ext);
        }

        @Override
        public FileObject move(FileLock lock, FileObject target, String name, String ext) throws IOException {
            throw new IOException("Unsupported oepration"); // NOI18N
        }

        @Override
        public String toString() {
            return delegate.toString();
        }

        @Override
        public String getPath() {
            return delegate.getPath();
        }

        @Override
        public String getPackageNameExt(char separatorChar, char extSepChar) {
            return delegate.getPackageNameExt(separatorChar, extSepChar);
        }

        @Override
        public String getPackageName(char separatorChar) {
            return delegate.getPackageName(separatorChar);
        }

        @Override
        public String getNameExt() {
            return delegate.getNameExt();
        }

        @Override
        public FileSystem getFileSystem() throws FileStateInvalidException {
            return MultiFileObject.this.getFileSystem();
        }

        @Override
        public FileObject getParent() {
            return MultiFileObject.this;
        }

        @Override
        public boolean isFolder() {
            return delegate.isFolder();
        }

        @Override
        public Date lastModified() {
            return delegate.lastModified();
        }

        @Override
        public boolean isRoot() {
            return delegate.isRoot();
        }

        @Override
        public boolean isData() {
            return delegate.isData();
        }

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

        @Override
        public boolean existsExt(String ext) {
            return delegate.existsExt(ext);
        }

        @Override
        public void delete(FileLock lock) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public Lookup getLookup() {
            return delegate.getLookup();
        }

        @Override
        public void setAttribute(String attrName, Object value) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public Enumeration<String> getAttributes() {
            return delegate.getAttributes();
        }

        @Override
        boolean isHasExtOverride() {
            return delegate.isHasExtOverride();
        }

        @Override
        boolean hasExtOverride(String ext) {
            return delegate.hasExtOverride(ext);
        }

        @Override
        public void addFileChangeListener(FileChangeListener fcl) {
            delegate.addFileChangeListener(fcl);
        }

        @Override
        public void removeFileChangeListener(FileChangeListener fcl) {
            delegate.removeFileChangeListener(fcl);
        }

        @Override
        public void addRecursiveListener(FileChangeListener fcl) {
            delegate.addRecursiveListener(fcl);
        }

        @Override
        public void removeRecursiveListener(FileChangeListener fcl) {
            delegate.removeRecursiveListener(fcl);
        }

        @Override
        protected void fireFileDataCreatedEvent(Enumeration<FileChangeListener> en, FileEvent fe) {
        }

        @Override
        protected void fireFileFolderCreatedEvent(Enumeration<FileChangeListener> en, FileEvent fe) {
        }

        @Override
        protected void fireFileChangedEvent(Enumeration<FileChangeListener> en, FileEvent fe) {
        }

        @Override
        protected void fireFileDeletedEvent(Enumeration<FileChangeListener> en, FileEvent fe) {
        }

        @Override
        protected void fireFileAttributeChangedEvent(Enumeration<FileChangeListener> en, FileAttributeEvent fe) {
        }

        @Override
        protected void fireFileRenamedEvent(Enumeration<FileChangeListener> en, FileRenameEvent fe) {
        }

        @Override
        public String getMIMEType() {
            return delegate.getMIMEType();
        }

        @Override
        public String getMIMEType(String... withinMIMETypes) {
            return delegate.getMIMEType(withinMIMETypes);
        }

        @Override
        public long getSize() {
            return delegate.getSize();
        }

        @Override
        public InputStream getInputStream() throws FileNotFoundException {
            return delegate.getInputStream();
        }

        @Override
        public byte[] asBytes() throws IOException {
            return delegate.asBytes();
        }

        @Override
        public String asText(String encoding) throws IOException {
            return delegate.asText(encoding);
        }

        @Override
        public String asText() throws IOException {
            return delegate.asText();
        }

        @Override
        public List<String> asLines() throws IOException {
            return delegate.asLines();
        }

        @Override
        public List<String> asLines(String encoding) throws IOException {
            return delegate.asLines(encoding);
        }

        @Override
        public OutputStream getOutputStream(FileLock lock) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public FileLock lock() throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public boolean isLocked() {
            return delegate.isLocked();
        }

        @Override
        public void setImportant(boolean b) {
        }

        @Override
        public FileObject[] getChildren() {
            return new FileObject[0];
        }

        @Override
        public Enumeration<? extends FileObject> getChildren(boolean rec) {
            return Enumerations.empty();
        }

        @Override
        public Enumeration<? extends FileObject> getFolders(boolean rec) {
            return Enumerations.empty();
        }

        @Override
        public Enumeration<? extends FileObject> getData(boolean rec) {
            return Enumerations.empty();
        }

        @Override
        public FileObject getFileObject(String name, String ext) {
            if (maskFile.isFolder()) {
                return maskFile.getFileObject(name, ext);
            } else {
                return null;
            }
        }

        @Override
        public FileObject getFileObject(String relativePath) {
            if (maskFile.isFolder()) {
                return maskFile.getFileObject(relativePath);
            } else {
                return null;
            }
        }

        @Override
        public FileObject createFolder(String name) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public FileObject createData(String name, String ext) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public FileObject createData(String name) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public OutputStream createAndOpen(String name) throws IOException {
            throw new IOException("Unsupported oepration"); //NOI18N
        }

        @Override
        public boolean isReadOnly() {
            return delegate.isReadOnly();
        }

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

        @Override
        public boolean canRead() {
            return delegate.canRead();
        }

        @Override
        public void refresh(boolean expected) {
            delegate.refresh(expected);
        }

        @Override
        public void refresh() {
            delegate.refresh();
        }

        @Override
        public boolean isVirtual() {
            return delegate.isVirtual();
        }
        
        public int hashCode() {
            return 0xaa ^ delegate.hashCode() << 3;
        }
        
        public boolean equals(Object o) {
            if (!(o instanceof RevealedFileObject)) {
                return false;
            }
            RevealedFileObject other = (RevealedFileObject)o;
            return other.delegate.equals(delegate) && other.maskFile.equals(maskFile);
        }
    }
    
    /**
     * The returned collection contains hidden or removed FileObjects, from
     * the lower layers. The returned FileObjects are 'special' in that DataObjects
     * cannot be created from them, and they cannot be changed. All mutation methods
     * will throw IOException - unsupported operation.
     */
    private Collection<FileObject> collectRevealedFiles() {
        String path = getPath();
        FileSystem[] systems = getMultiFileSystem().getDelegates();
        FileSystem writable = getMultiFileSystem().writableLayer(path);
        
        if (writable == null) {
            return Collections.emptyList();
        }
        FileObject writableFolder = writable.findResource(path);
        // no overrides as the containing folder does not exist on the writable fs.
        if (writableFolder == null) {
            return Collections.emptyList();
        }
        FileObject[] ch = writableFolder.getChildren();
        int sl = systems.length;
        
        Collection<FileObject> result = null;
        String parentPath = writableFolder.getPath();
        for (FileObject mask : ch) {
            String fn = mask.getNameExt();
            if (fn.endsWith(MultiFileSystem.MASK)) {
                fn = fn.substring(0, fn.length() - MultiFileSystem.MASK.length());
            }
            String p = parentPath + "/" + fn; // NOI18N
            for (int i = 0; i < sl; i++) {
                if (writable == systems[i]) {
                    continue;
                }
                FileObject hiddenFo = systems[i].findResource(p);
                if (hiddenFo != null) {
                    if (result == null) {
                        result = new ArrayList<FileObject>(ch.length);
                    }

                    result.add(new RevealedFileObject(mask, hiddenFo));
                    break;
                }
            }
        }
        if (result == null) {
            return Collections.emptyList();
        } else {
            return result;
        }
    }
}
