/*
 * 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.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Externalizable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.PushbackInputStream;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.openide.util.Enumerations;
import org.openide.util.NbBundle;
import org.openide.util.BaseUtilities;
import org.openide.util.io.NbMarshalledObject;
import org.openide.xml.XMLUtil;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/** Implementation of <code>AbstractFileSystem.Attr</code> using a special file
 * in each folder for holding attributes.
 * It needs to hide
 * the file from the rest of system, so it also implements
 * <code>AbstractFileSystem.List</code> to exclude the file from the children list
 * (it can then serve to filter a plain list implementation).
 *
 *Description of format of special file ilustrates best DTD file that is showed in next lines:
 *<!ELEMENT attributes (fileobject)*>
 * <!ATTLIST attributes version CDATA #REQUIRED>
 * <!ELEMENT fileobject (attr)*>
 * <!ATTLIST fileobject name CDATA #REQUIRED>
 * <!ELEMENT attr EMPTY>
 * <!ATTLIST attr name CDATA #REQUIRED>
 * <!ATTLIST attr bytevalue CDATA #IMPLIED>
 * <!ATTLIST attr shortvalue CDATA #IMPLIED>
 * <!ATTLIST attr intvalue CDATA #IMPLIED>
 * <!ATTLIST attr longvalue CDATA #IMPLIED>
 * <!ATTLIST attr floatvalue CDATA #IMPLIED>
 * <!ATTLIST attr doublevalue CDATA #IMPLIED>
 * <!ATTLIST attr boolvalue CDATA #IMPLIED>
 * <!ATTLIST attr charvalue CDATA #IMPLIED>
 * <!ATTLIST attr stringvalue CDATA #IMPLIED>
 * <!ATTLIST attr methodvalue CDATA #IMPLIED>
 * <!ATTLIST attr serialvalue CDATA #IMPLIED>
 * <!ATTLIST attr urlvalue CDATA #IMPLIED>
 *
 * @author Jaroslav Tulach
 */
@SuppressWarnings("unchecked")
public class DefaultAttributes extends Object implements AbstractFileSystem.Attr, AbstractFileSystem.List {
    static final long serialVersionUID = -5801291358293736478L;

    /** File name of special file in each folder where attributes are saved.
     * @deprecated does not handle XML attributes
     */
    @Deprecated
    public final static String ATTR_NAME = "filesystem"; // NOI18N

    /** Extension of special file in each folder where attributes are saved.
     * @deprecated does not handle XML attributes
     */
    @Deprecated
    public final static String ATTR_EXT = "attributes"; // NOI18N

    /** Name with extension of special file in each folder where attributes are saved.
     * @deprecated does not handle XML attributes
     */
    @Deprecated
    public final static String ATTR_NAME_EXT = ATTR_NAME + '.' + ATTR_EXT;
    private final static String ATTR_NAME_EXT_XML = System.getProperty(
            "org.openide.filesystems.DefaultAttributes.ATTR_NAME_EXT_XML", ".nbattrs"
        ); // NOI18N

    /**  readOnlyAttrs is name of virtual attribute. This name of virtual attribute
     * is shared between classes (and cannot be changed without breaking compatibility):
     * - org.openide.filesystems.DefaultAttributes
     * - org.openide.loaders.ExecutionSupport
     * - org.openide.loaders.CompilerSupport
     * - org.netbeans.core.ExJarFileSystem
     */
    private final static String READONLY_ATTRIBUTES = "readOnlyAttrs"; //NOI18N

    // <?xml version="1.0"?>
    // <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD DefaultAttributes 1.0//EN" "http://www.netbeans.org/dtds/attributes-1_0.dtd">
    // <attributes>...</attributes>
    private static final String PUBLIC_ID = "-//NetBeans//DTD DefaultAttributes 1.0//EN"; // NOI18N
    private static final String DTD_PATH = "org/openide/filesystems/attributes.dtd"; // NOI18N    

    /** description of the fs to work on - info about files */
    private AbstractFileSystem.Info info;

    /** description of the fs to work on - work with files */
    private AbstractFileSystem.Change change;

    /** description of the fs to work on - listing of files */
    private AbstractFileSystem.List list;

    /** file name of attributes (default value corresponds to ATTR_NAME_EXT_XML) */
    private String fileName;

    /** Cache of attributes.
    * For name of folder gives map of maps of attibutes
    * (String, Reference (Table))
    */
    private transient Map<String, SoftReference<Table>> cache;

    /** Constructor.
    * @param info file object information to use
    * @param change file change hooks to use
    * @param list list to filter (can be <code>null</code>, but then this object cannot work as a list)
    */
    public DefaultAttributes(
        AbstractFileSystem.Info info, AbstractFileSystem.Change change, AbstractFileSystem.List list
    ) {
        this.info = info;
        this.change = change;
        this.list = list;
        fileName = ATTR_NAME_EXT_XML;
    }

    /** Constructor.
     *
     * @param info file object information to use
     * @param change file change hooks to use
     * @param list list to filter (can be <code>null</code>, but then this object cannot work as a list)
     * @param fileName
     * @since 4.35
     */
    protected DefaultAttributes(
        AbstractFileSystem.Info info, AbstractFileSystem.Change change, AbstractFileSystem.List list, String fileName
    ) {
        this(info, change, list);
        this.fileName = fileName;
    }

    /** Methods to ensure backward compatibility for storing and
    * loading classes.
    */
    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
        ObjectInputStream.GetField fields = ois.readFields();

        Object o1 = AbstractFileSystem.readImpl("change", fields); // NOI18N
        Object o2 = AbstractFileSystem.readImpl("info", fields); // NOI18N
        Object o3 = AbstractFileSystem.readImpl("list", fields); // NOI18N

        change = (AbstractFileSystem.Change) o1;
        info = (AbstractFileSystem.Info) o2;
        list = (AbstractFileSystem.List) o3;
    }

    /** Get the children list, filtering out the special attributes file.
    * You <em>must</em> have provided a non-<code>null</code> {@link AbstractFileSystem.List}
    * in the constructor for this to work. If you did not, the rest of the class will work
    * fine, but this method should not be called and this object should not be used
    * as a <code>List</code> implementation.
    *
    * @param f the folder, by name; e.g. <code>top/next/afterthat</code>
    * @return a list of children of the folder, as <code>file.ext</code> (no path)
    */
    public String[] children(String f) {
        String[] arr = list.children(f);
        int lookUpIndex = 0;

        if (arr == null) {
            return null;
        }

        int size = arr.length;

        if (size == 1) {
            // In NB 3.2.x for OpenVMS, we had to use  "_nbattrs." as a attribute file.
            // However, OpenVMS now supports a file name beginning with "."
            // So we now have to copy the existing "_nbattrs." file into ".nbattrs"
            //
            if ((BaseUtilities.getOperatingSystem() == BaseUtilities.OS_VMS) && (arr[0] != null) && (f != null)) {
                if (arr[0].equalsIgnoreCase("_nbattrs.")) {
                    try {
                        deleteFile(f + "/" + arr[0]); // NOI18N
                    } catch (IOException ioe) {
                    }

                    arr[0] = getFileName();
                }
            }

            if ((getFileName().equals(arr[0]) || ATTR_NAME_EXT_XML.equals(arr[0]) || ATTR_NAME_EXT.equals(arr[0]))) {
                try {
                    this.change.delete(f + "/" + arr[0]);
                } catch (IOException iox) {
                }

                return new String[] {  };
            }
        }

        for (int i = 0; i < size; i++) {
            // In NB 3.2.x for OpenVMS, we had to use  "_nbattrs." as a attribute file.
            // However, OpenVMS now supports a file name beginning with "."
            // So we now have to copy the existing "_nbattrs." file into ".nbattrs"
            //
            if ((BaseUtilities.getOperatingSystem() == BaseUtilities.OS_VMS) && (arr[i] != null) && (f != null)) {
                if (arr[i].equalsIgnoreCase("_nbattrs.")) {
                    try {
                        File fp = new File(f + "/" + ".nbattrs");

                        if (!fp.exists()) {
                            cache = null;
                            copyVMSAttrFile(f);
                        }
                    } catch (IOException ioe) {
                    }

                    arr[i] = getFileName();
                }
            }

            String safeNbAttrsCopy = getFileName() + "~"; //NOI18N

            if (
                getFileName().equals(arr[i]) || ATTR_NAME_EXT.equals(arr[i]) || ATTR_NAME_EXT_XML.equals(arr[i]) ||
                    safeNbAttrsCopy.equals(arr[i])
            ) {
                // exclude this index
                arr[i] = null;

                // there can be two files with attributes
                if (++lookUpIndex >= 2) {
                    break;
                }
            }
        }

        return arr;
    }

    /** Renames the attribute file for OpenVMS platform.
     *  The method renames "_nbattrs." into ".nbattrs".
     *  We cannot simply use the change.rename method
     *  because of the special property of OpenVMS having to do with
     *  a file name starting with "."
     *
     *  @param f the folder containg the attribute file
     */
    private void copyVMSAttrFile(String f) throws IOException {
        InputStream is = null;
        OutputStream os = null;

        try {
            change.createData(f + "/" + getFileName());
            is = info.inputStream(f + "/" + "_nbattrs.");
            os = info.outputStream(f + "/" + getFileName());

            byte[] buf = new byte[256];
            int readi;

            while ((readi = is.read(buf, 0, 256)) >= 0x0) {
                os.write(buf, 0, readi);
            }

            is.close();

            //change.delete (f+"/"+"_nbattrs.");
            is = null;
        } catch (IOException ie) {
        } finally {
            if (is != null) {
                is.close();
            }

            if (os != null) {
                os.close();
            }
        }
    }

    // JST: Description
    //
    //
    // The class should be written in such a way that the access to disk is
    // synchronized (this). But during the access nobody is allowed to
    // perform serialization and deserialization
    // of unknown objects, so all objects should be wrapped into NbMarshalledObject
    // serialized or in reverse target NbMarshalledObject should be deserialized
    // and then not holding the lock the object obtained from it by a call to
    // marshall.get ().
    //
    // JST: Got it?

    /* Get the file attribute with the specified name.
    * @param name the file
    * @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)
    */
    public Object readAttribute(String name, String attrName) {
        Table t;
        String[] arr = new String[2];
        split(name, arr);

        /** At the momement substitutes lack of API */
        if (attrName.equals(READONLY_ATTRIBUTES)) {
            return info.readOnly(arr[0]) ? Boolean.TRUE : Boolean.FALSE;
        }

        synchronized (this) {
            // synchronized so only one table for each folder
            // can exist
            t = loadTable(arr[0]);
        }

        // JST:
        // had to split the code to do getAttr out of synchronized block
        // because the attribute can be serialized FileObject and
        // so the code returns back to FileSystem (that is usually synchronized)
        //
        // this leads to deadlocks between FS & DefaultAttributes implementation
        //
        // I do not know if the table should not be somehow synchronized,
        // but it seems ok.
        return t.getAttr(arr[1], attrName);
    }

    /* Set the file attribute with the specified name.
    * @param name the file
    * @param attrName name of the attribute
    * @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular filesystems 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 writeAttribute(String name, String attrName, Object value)
    throws IOException {
        // create object that should be serialized
        //NbMarshalledObject marshall = new NbMarshalledObject (value);
        int objType;

        String[] arr = new String[2];
        split(name, arr);

        for (;;) {
            int version;
            Table t;

            synchronized (this) {
                t = loadTable(arr[0]);
                version = t.version;
            }

            // Tests if the attribute is changing
            Object prev = t.getAttr(arr[1], attrName);

            if (prev == value /*|| (value != null && value.equals (prev))*/    ) {
                return;
            }

            synchronized (this) {
                Table t2 = loadTable(arr[0]);

                if ((t == t2) && (version == t2.version)) {
                    // no modification between reading of the value =>
                    // save!
                    //Class cls = value.getClass();                    
                    if (value == null) {
                        t.setAttr(arr[1], attrName, null); // clear the attribute
                    } else {
                        if (
                            (objType = XMLMapAttr.Attr.distinguishObject(value)) == XMLMapAttr.Attr.isValid(
                                    "SERIALVALUE"
                                )
                        ) { // NOI18N
                            t.setAttr(arr[1], attrName, value); //change value instead of marshall
                        } else {
                            t.setAttr(arr[1], attrName, XMLMapAttr.createAttribute(objType, value.toString()));
                        }
                    }

                    saveTable(arr[0], t);

                    // ok, saved
                    return;
                }
            }

            // otherwise try it again
        }
    }

    /* Get all file attribute names for the file.
    * @param name the file
    * @return enumeration of keys (as strings)
    */
    public synchronized Enumeration<String> attributes(String name) {
        String[] arr = new String[2];
        split(name, arr);

        Table t = loadTable(arr[0]);

        return t.attrs(arr[1]);
    }

    /* Called when a file is renamed, to appropriatelly update its attributes.
    * <p>
    * @param oldName old name of the file
    * @param newName new name of the file
    */
    public synchronized void renameAttributes(String oldName, String newName) {
        try {
            String[] arr = new String[2];
            split(oldName, arr);

            Table t = loadTable(arr[0]);
            Map v = (Map) t.remove(arr[1]);

            //      System.out.println ("ARg[0] = " + arr[0] + " arr[1] = " + arr[1] + " value: " + v); // NOI18N
            if (v == null) {
                // no attrs no change
                return;
            }

            split(newName, arr);

            // Remove transient attributes:
            Iterator<Map.Entry> it = v.entrySet().iterator();

            while (it.hasNext()) {
                Map.Entry pair = it.next();

                if (FileUtil.transientAttributes.contains(pair.getKey())) {
                    it.remove();
                }
            }
            t.put(arr[1], v);

            //      System.out.println ("xyz[0] = " + arr[0] + " xyz[1] = " + arr[1] + " value: " + v); // NOI18N
            saveTable(arr[0], t);
        } catch (IOException e) {
            ExternalUtil.exception(e);
        }
    }

    /* Called when a file is deleted to also delete its attributes.
    *
    * @param name name of the file
    */
    public synchronized void deleteAttributes(String name) {
        try {
            String[] arr = new String[2];
            split(name, arr);

            Table t = loadTable(arr[0]);

            if (t.remove(arr[1]) != null) {
                // if there is a change
                saveTable(arr[0], t);
            }
        } catch (IOException e) {
            ExternalUtil.exception(e);
        }
    }

    /** Getter for the cache.
    */
    private Map<String, SoftReference<Table>> getCache() {
        if (cache == null) {
            cache = new HashMap<>(31);
        }

        return cache;
    }

    /** Splits name of a file to name of folder and to name of the file.
    * @param name of file
    * @param arr arr[0] will hold name of folder and arr[1] name of the file
    */
    private static void split(String name, String[] arr) {
        int i = name.lastIndexOf('/');

        if (i == -1) {
            arr[0] = ""; // NOI18N
            arr[1] = name;

            return;
        }

        // folder name
        arr[0] = name.substring(0, i);

        // increase the i to be beyond the length
        if (++i == name.length()) {
            arr[1] = ""; // NOI18N
        } else {
            // split it
            arr[1] = name.substring(i);
        }
    }

    /** Save attributes.
    * @param name name of folder to save attributes for
    * @param map map to save
    */
    private void saveTable(String name, Table map) throws IOException {
        String fullName = ((name.length() == 0) ? "" : (name + '/')) + getFileName(); // NOI18N

        /** OpenVMS now supports various special characters including "~"*/
        String safeName = fullName + "~"; // NOI18N        

        if (info.folder(fullName)) {
            if (map.size() == 0) {
                // ok no need to delete
                return;
            }

            // find parent
            change.createData(fullName);
        } else {
            if (map.size() == 0) {
                deleteFile(fullName);

                return;
            }
        }

        PrintWriter pw = null;
        IOException ioexc = null;

        try {
            pw = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(info.outputStream(safeName)), "UTF8")); // NOI18N            
            map.writeToXML(pw);
            pw.flush();
        } catch (IOException iex) {
            ioexc = iex;
        } finally {
            if (pw != null) {
                pw.close();
            }

            if (ioexc != null) {
                try {
                    deleteFile(safeName);
                } catch (IOException ioe) {
                    if (ioe.getCause() == null) {
                        ioe.initCause(ioexc);
                    }
                    throw ioe;
                }
                throw ioexc;
            } else {
                for (int counter = 0; ;counter++) {
                    try {
                        deleteFile(fullName);
                    } catch (IOException iex2) {
                        /** if delete fails, then also rename fails and exception will
                         * be fired
                         */
                        FileSystem.LOG.log(Level.INFO, "Cannot delete " + fullName, iex2); // NOI18N
                    }
                    try {
                        this.change.rename(safeName, fullName);
                        break;
                    } catch (IOException ex) {
                        FileSystem.LOG.log(Level.INFO, "Cannot rename " + fullName + " to " + safeName, ex); // NOI18N
                        if (counter > 10) {
                            throw ex;
                        }
                    }
                }
            }
        }
    }

    /** Load attributes from cache or
    * from disk.
    * @param name of folder to load data from
    */
    private Table loadTable(String name) { //throws IOException {

        SoftReference<Table> r = getCache().get(name);

        if (r != null) {
            Table m = r.get();

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

        // have to load new table
        Table t = load(name);
        t.attach(name, this);

        getCache().put(name, new SoftReference<Table>(t));

        return t;
    }

    /** Loads the table. Does no initialization.
    */
    private Table load(String name) {
        String[] acceptNames = {
            ((name.length() == 0) ? "" : (name + '/')) + getFileName(), // NOI18N
            ((name.length() == 0) ? "" : (name + '/')) + ATTR_NAME_EXT
        }; // NOI18N

        for (int i = 0; i < acceptNames.length; i++) {
            if (info.size(acceptNames[i]) > 0L) {
                try {
                    InputStream fis = info.inputStream(acceptNames[i]);

                    try {
                        return loadTable(fis, acceptNames[i]);
                    } finally {
                        try {
                            fis.close();
                        } catch (IOException e) {
                            // ignore--who cares?
                        }
                    }
                } catch (FileNotFoundException ex) {
                    ExternalUtil.exception(ex);
                }
            }
        }

        return new Table();
    }

    /** Loads the Table of extended attributes for a input stream from binary serialized file or from XML.
    * @param is input stream
    * @param folderName name of file for better error message
    * @return the attributes table for this input stream
    */
    static Table loadTable(InputStream is, String folderName) {
        Table retTable = new Table();
        PushbackInputStream pbStream = null;
        boolean isSerialized = false;

        try {
            if (folderName.endsWith(ATTR_NAME_EXT)) {
                pbStream = new PushbackInputStream(is, 4); //is.available()
                isSerialized = isSerialized(pbStream);
            }

            if (isSerialized && (pbStream != null)) {
                BufferedInputStream fis = new BufferedInputStream(pbStream);
                ObjectInputStream ois = new org.openide.util.io.NbObjectInputStream(fis);
                Object o = ois.readObject();

                if (o instanceof Table) {
                    return (Table) o;
                }
            } else {
                BufferedInputStream bis = (pbStream != null) ? new BufferedInputStream(pbStream)
                                                             : new BufferedInputStream(is);
                retTable.readFromXML(bis, false);

                return retTable;
            }
        } catch (Exception e) {
            // [PENDING] use multi-arg getMessage (MessageFormat-style) properly here:
            IOException summaryEx = new IOException(
                    NbBundle.getMessage(DefaultAttributes.class, "EXC_DefAttrReadErr") + ": " + folderName
                );
            ExternalUtil.copyAnnotation(summaryEx, e);
            ExternalUtil.exception(summaryEx);
        }

        // create empty table, what else
        return new Table();
    }

    /** Tests whether InputStream contains serialized data
    * @param pbStream is pushback input stream; tests 4 bytes and then returns them back
    * @return true if the file has serialized form
    */
    static private final boolean isSerialized(PushbackInputStream pbStream)
    throws IOException {
        int[] serialPattern = { '\u00AC', '\u00ED', '\u0000', '\u0005' }; //NOI18N patern for serialized objects
        byte[] checkedArray = new byte[serialPattern.length];
        int unsignedConv = 0;

        pbStream.read(checkedArray, 0, checkedArray.length);
        pbStream.unread(checkedArray);

        for (int i = 0; i < checkedArray.length; i++) {
            unsignedConv = (checkedArray[i] < 0) ? (checkedArray[i] + 256) : checkedArray[i];

            if (serialPattern[i] != unsignedConv) {
                return false;
            }
        }

        return true;
    }

    /** Remove from cache */
    synchronized void removeTable(String name) {
        getCache().remove(name);
    }

    //
    // FileUtil.extractJar methods
    //

    /** Does the name seems like file with extended attributes?
    * @param name the name
    * @return true if so
    */
    static boolean acceptName(String name) {
        return (name.endsWith(ATTR_NAME_EXT) || name.endsWith(ATTR_NAME_EXT_XML));
    }

    private String getFileName() {
        if (fileName == null) {
            fileName = ATTR_NAME_EXT_XML;
        }

        return fileName;
    }
    
    private void deleteFile(String name) throws IOException {
        OutputStream os = null;
        this.info.lock(name);
        try {
            //added because of mutual exclusion of streams (waits a while until stream is closed)
            os = this.info.outputStream(name);
            os.close(); os = null;
            this.change.delete(name);
        } finally {
            if (os != null) {
                os.close();
            }            
            this.info.unlock(name);
        }
    }
    

    /** Table that hold mapping between files and attributes.
    * Hold mapping of type (String, Map (String, Object))
    */
    final static class Table extends HashMap implements Externalizable {
        static final long serialVersionUID = 2353458763249746934L;

        /** name of folder we belong to */
        private transient String name;

        /** attributes to belong to */
        private transient DefaultAttributes attrs;

        /** version counting */
        private transient int version = 0;

        /** Constructor */
        public Table() {
            super(11);
        }

        /** Attaches to file in attributes */
        public void attach(String name, DefaultAttributes attrs) {
            this.name = name;
            this.attrs = attrs;
        }

        /** Remove itself from the cache if finalized.
        */
        @Override
        protected void finalize() {
            //      System.out.println ("Finalizing table for: " + name); // NOI18N
            attrs.removeTable(name);
        }

        /** For given file finds requested attribute.
         * @param fileName name of the file
         * @param attrName name of the attribute
         * @return attribute or null (if not found)
         */
        public Object getAttr(String fileName, String attrName) {
            XMLMapAttr m = (XMLMapAttr) get(fileName);

            if (m != null) {
                Object o = null;

                try {
                    o = m.getAttribute(attrName);
                } catch (Exception e) {
                    ExternalUtil.annotate(e, "fileName = " + fileName); //NOI18N                                     
                    ExternalUtil.exception(e);
                }

                if (o == null) {
                    return null;
                }

                if (!(o instanceof NbMarshalledObject)) {
                    return o;
                }

                NbMarshalledObject mo = (NbMarshalledObject) o;

                try {
                    return (mo == null) ? null : mo.get();
                } catch (IOException e) {
                    ExternalUtil.log("Cannot load attribute " + attrName + " from " + fileName); // NOI18N
                    ExternalUtil.exception(e);
                } catch (ClassNotFoundException e) {
                    ExternalUtil.log("Cannot load attribute " + attrName + " from " + fileName); // NOI18N
                    ExternalUtil.exception(e);
                }
            }

            return null;
        }

        /** Sets an marshaled attribute to the table.
        */
        final void setMarshalledAttr(String fileName, String attrName, NbMarshalledObject obj) {
            setAttr(fileName, attrName, obj);
        }

        /**
         * Sets an attribute to the table.
         * New added - for Sandwich project (XML format instead of serialization) .
         * @param fileName - name of file
         * @param attrName - name of attribute
         * @param obj - attribute
         */
        final void setAttr(String fileName, String attrName, Object obj) {
            XMLMapAttr m = (XMLMapAttr) get(fileName);

            if (m == null) {
                m = new XMLMapAttr(); //HashMap (7);//XMLMapAttr();
                put(fileName, m);
            }

            m.put(attrName, obj, false);

            if ((obj == null) && (m.size() == 1)) {
                remove(fileName);
            }

            // increments the version
            version++;
        }

        /** Enum of attributes for one file.
        */
        public Enumeration<String> attrs(String fileName) {
            Map m = (Map) get(fileName);

            if (m == null) {
                return Enumerations.empty();
            } else {
                HashSet<String> s = new HashSet<>(m.keySet());

                return Collections.enumeration(s);
            }
        }

        /**
         * Parses element:  <CODE><Attributes version="1.0"></CODE>
         * @return new instance of subclass (anonymous class)of ElementHandler
         */
        private ElementHandler parseFirstLevel() {
            ElementHandler elemService = new ElementHandler() {
                    private final String[] ELM_KEYS = { "ATTRIBUTES" }; // NOI18N
                    private final String[] MANDAT_ATTR_KEYS = { "VERSION" }; // NOI18N

                    @Override
                    public void internalStartElement(String elemName, HashMap mapMandatory, HashMap mapAllowed)
                    throws SAXException {
                        // later can check version
                    }

                    @Override
                    protected String[] getKeys() {
                        return ELM_KEYS;
                    }

                    @Override
                    protected String[] getMandatoryAttrs() {
                        return MANDAT_ATTR_KEYS;
                    }
                };

            return elemService;
        }

        /**
         * Parses element:  <CODE><fileobject name="fileName"></CODE>
         * @param fileName is parsed from XML
         * @return new instance of subclass (anonymous class)of ElementHandler
         */
        private ElementHandler parseSecondLevel(final StringBuffer fileName) {
            ElementHandler elemService = new ElementHandler() {
                    private final String[] ELM_KEYS = { "FILEOBJECT" }; // NOI18N
                    private final String[] MANDAT_ATTR_KEYS = { "NAME" }; // NOI18N

                    @Override
                    public void internalStartElement(String elemName, HashMap mapMandatory, HashMap mapAllowed)
                    throws SAXException {
                        String temp;
                        fileName.delete(0, fileName.length());
                        temp = (String) mapMandatory.get("NAME"); // NOI18N

                        if (temp == null) {
                            temp = (String) mapMandatory.get("name"); // NOI18N
                        }

                        if (temp != null) {
                            fileName.append(temp);
                        }
                    }

                    @Override
                    public void endElement(String elementName)
                    throws SAXException {
                    }

                    @Override
                    protected String[] getKeys() {
                        return ELM_KEYS;
                    }

                    @Override
                    protected String[] getMandatoryAttrs() {
                        return MANDAT_ATTR_KEYS;
                    }
                };

            return elemService;
        }

        /**
         * Parses element:  <CODE><attr StringValue="This is attribute"></CODE>
         * @param fileName is name of fileobject, which is assigned to attribute
         * @return new instance of subclass (anonymous class)of ElementHandler
         */
        private ElementHandler parseThirdLevel(final StringBuffer fileName) {
            ElementHandler elemService = new ElementHandler() {
                    private final String[] ELM_KEYS = { "ATTR" }; // NOI18N
                    private final String[] MANDAT_ATTR_KEYS = { "NAME" }; // NOI18N

                    @Override
                    public void internalStartElement(String elemName, HashMap mapMandatory, HashMap mapAllowed)
                    throws SAXException {
                        String attrName;

                        if (mapAllowed.isEmpty()) {
                            return;
                        }

                        attrName = (String) mapMandatory.get("NAME"); // NOI18N

                        if (attrName == null) {
                            attrName = (String) mapMandatory.get("name"); // NOI18N
                        }

                        if (attrName == null) {
                            return;
                        }

                        Iterator<Map.Entry<String, String>> it = mapAllowed.entrySet().iterator();

                        while (it.hasNext()) {
                            Map.Entry<String, String> pair = it.next();

                            if (XMLMapAttr.Attr.isValid(pair.getKey()) != -1) {
                                XMLMapAttr.Attr attr = XMLMapAttr.createAttributeAndDecode(
                                        pair.getKey(), pair.getValue()
                                    );
                                setAttr(fileName.toString(), attrName, attr);
                            }
                        }
                    }

                    @Override
                    protected String[] getKeys() {
                        return ELM_KEYS;
                    }

                    @Override
                    protected String[] getMandatoryAttrs() {
                        return MANDAT_ATTR_KEYS;
                    }

                    @Override
                    protected String[] getAllowedAttrs() {
                        return XMLMapAttr.Attr.getAttrTypes();
                    }
                     //ALLOWED_ATTR_KEYS
                };

            return elemService;
        }

        /** Writes itself to XML
         * @param pw is PrintWriter
         */
        public void writeToXML(PrintWriter pw) /*throws IOException */ {
            // list of names
            Iterator<String> it = new TreeSet(keySet()).iterator();
            XMLMapAttr.writeHeading(pw);

            while (it.hasNext()) {
                String file = it.next();
                XMLMapAttr attr = (XMLMapAttr) get(file);

                if ((attr != null) && !attr.isEmpty()) {
                    attr.write(pw, file, "    "); // NOI18N
                }
            }

            XMLMapAttr.writeEnding(pw);
        }

        /**
         * Reads itself from XML format
         * New added - for Sandwich project (XML format instead of serialization) .
         * @param is input stream (which is parsed)
         * @return Table
         */
        public void readFromXML(InputStream is, boolean validate)
        throws SAXException {
            StringBuffer fileName = new StringBuffer();
            ElementHandler[] elmKeyService = { parseFirstLevel(), parseSecondLevel(fileName), parseThirdLevel(fileName) }; //
            String dtd = getClass().getClassLoader().getResource(DTD_PATH).toExternalForm();
            InnerParser parser = new InnerParser(PUBLIC_ID, dtd, elmKeyService);

            try {
                parser.parseXML(is, validate);
            } catch (Exception ioe) {
                throw (SAXException) ExternalUtil.copyAnnotation(
                    new SAXException(NbBundle.getMessage(DefaultAttributes.class, "EXC_DefAttrReadErr")), ioe
                );
            } catch (FactoryConfigurationError fce) {
                // ??? see http://openide.netbeans.org/servlets/ReadMsg?msgId=340881&listName=dev
                throw (SAXException) ExternalUtil.copyAnnotation(
                    new SAXException(NbBundle.getMessage(DefaultAttributes.class, "EXC_DefAttrReadErr")), fce
                );
            }
        }

        /** Writes external.
         * @param oo
         * @throws IOException  */
        public void writeExternal(ObjectOutput oo) throws IOException {
            // list of names
            Iterator<String> it = keySet().iterator();

            while (it.hasNext()) {
                String file = it.next();
                Map attr = (Map) get(file);

                if ((attr != null) && !attr.isEmpty()) {
                    oo.writeObject(file);

                    Iterator<Map.Entry> entries = attr.entrySet().iterator();

                    while (entries.hasNext()) {
                        Map.Entry entry = entries.next();
                        String key = (String) entry.getKey();
                        Object value = entry.getValue();

                        if ((key != null) && (value != null)) {
                            oo.writeObject(key);
                            oo.writeObject(value);
                        }
                    }

                    oo.writeObject(null);
                }
            }

            oo.writeObject(null);
        }

        /** Reads external.
        */
        public void readExternal(ObjectInput oi) throws IOException, ClassNotFoundException {
            for (;;) {
                String file = (String) oi.readObject();

                if (file == null) {
                    break;
                }

                for (;;) {
                    String attr = (String) oi.readObject();

                    if (attr == null) {
                        break;
                    }

                    Object o = oi.readObject();

                    // backward compatibility
                    if (o instanceof java.rmi.MarshalledObject) {
                        o = ((java.rmi.MarshalledObject) o).get();
                        o = new NbMarshalledObject(o);
                    }

                    // end of backward compatibility
                    if (o instanceof NbMarshalledObject) {
                        setAttr(file, attr, o);
                    }
                }
            }
        }
    }

    /** Element handler should be used as superclass for future classes. These future classes should be passed
     * to constructor of InnerParser as Array. (ElementHandler[]). Each subclass of ElementHandler is responsible for
     * processing one or more elements in XML file.
     * Each subclass of ElementHandler should overwrite one or more of these methods:
     * - protected   String[] getKeys()
     * - protected   String[] getMandatoryAttrs()
     * - protected   String[] getAllowedAttrs()
     * - protected  void endElement(String name) throws SAXException {}
     * - protected  void characters(char[] ch, int start, int length) throws SAXException {}
     * - protected  void  internalStartElement(String elemName, HashMap mapMandatory,HashMap mapAllowed) throws SAXException {}
     */
    static abstract class ElementHandler {
        private static final String[] EMPTY = {  };
        private int mandatAttrCount;

        public void startElement(String elemName, Attributes attrs)
        throws SAXException {
            HashMap<String, String> mapAllowed   = new HashMap<>();
            HashMap<String, String> mapMandatory = new HashMap<>();

            if (checkAttributes(attrs, mapMandatory, mapAllowed) == false) {
                throw new SAXException(
                    NbBundle.getMessage(DefaultAttributes.class, "XML_InaccurateParam") + ": " + elemName
                ); // NOI18N 
            }

            internalStartElement(elemName, mapMandatory, mapAllowed);
        }

        /** Inner parser calls this method to notify this class that start element was parsed (<someelement>)
         * @param elemName  name of element
         * @param mapMandatory  map(String attributeName,String attributeValue) which holds pairs attributeName and attributeValue, which are mandatory for this element
         * @param mapAllowed map(String attributeName,String attributeValue) which holds pairs attributeName and attributeValue, which are optional for this element
         * @throws SAXException
         */
        protected void internalStartElement(String elemName, HashMap mapMandatory, HashMap mapAllowed)
        throws SAXException {
        }

        /** Inner parser calls this method to notify this class that there is content between start element and end element
         * @param ch[]  array of characters found between start and end element
         * @param start is start position in ch[]
         * @param length is length of content
         * @throws SAXException
         */
        protected void characters(char[] ch, int start, int length)
        throws SAXException {
        }

        /** Inner parser calls this method to notify this class that end element was parsed
         * @param elemName  name of element
         * @throws SAXException
         */
        protected void endElement(String elemName) throws SAXException {
        }

        /** @return names of elements which this class can process
         */
        protected String[] getKeys() {
            return EMPTY;
        }

        /** @return names of attributes which are checked and are mandatory
         */
        protected String[] getMandatoryAttrs() {
            return getKeys();
        }

        /** @return names of attributes which are allowed, are expected, but are not mandatory
         */
        protected String[] getAllowedAttrs() {
            return EMPTY;
        }

        private int isMyTag(String name) {
            return isInArray(name, getKeys());
        }

        private int isAllowedAttr(String name) {
            return isInArray(name, getAllowedAttrs());
        }

        private boolean isMandatOK() {
            return (mandatAttrCount == getMandatoryAttrs().length);
        }

        private int isMandatoryAttr(String name) {
            int retValue = isInArray(name, getMandatoryAttrs());

            if (retValue != -1) {
                mandatAttrCount++;
            }

            return retValue;
        }

        private int isInArray(String name, String[] arr) {
            if ((arr == null) || (name == null)) {
                return -1;
            }

            String correctStr = name.trim();

            for (int i = 0; i < arr.length; i++) {
                if (correctStr.equalsIgnoreCase(arr[i]) == true) {
                    return i;
                }
            }

            return -1;
        }

        private boolean checkAttributes(Attributes attrList,HashMap<String, String> mapMandatory, HashMap<String, String> mapAllowed) {
            String temp;
            mandatAttrCount = 0;

            if (attrList == null) {
                return false;
            }

            for (int i = 0; i < attrList.getLength(); i++) {
                if (isMandatoryAttr(attrList.getQName(i)) != -1) {
                    temp = attrList.getQName(i).toUpperCase(Locale.ENGLISH);
                    mapMandatory.put(temp, attrList.getValue(i));

                    continue;
                }

                if (isAllowedAttr(attrList.getQName(i)) != -1) {
                    temp = attrList.getQName(i).toUpperCase(Locale.ENGLISH);
                    mapAllowed.put(temp, attrList.getValue(i));

                    continue;
                }
            }

            return isMandatOK();
        }
    }

    /** Class that can be used to parse XML document (Expects array of ElementHandler clasess).  Calls handler methods of ElementHandler clasess.
     */
    static class InnerParser extends DefaultHandler {
        private ElementHandler[] elmKeyService; // = {fileSystemElement(attrStack),folderElement(attrStack),fileElement(attrStack),attrElement(attrStack)};        
        private String tagInProcess = ""; // NOI18N
        private String publicId;
        private String publicURL;

        InnerParser(String publicId, String publicURL, ElementHandler[] elmKeyService) {
            this.elmKeyService = elmKeyService;
            this.publicId = publicId;
            this.publicURL = publicURL;
        }

        /** Starts parsing document, that can be localized by means of uri parameter
         * @param validate
         * @param uri adress of document, that will be parsed
         * @throws ParserConfigurationException
         * @throws IOException
         * @throws SAXException  */
        public void parseXML(String uri, boolean validate)
        throws IOException, SAXException, ParserConfigurationException, FactoryConfigurationError {
            XMLReader parser = getParser(validate);
            parser.parse(uri);
        }

        /** Starts parsing document - if you have document`s InputStream
         * @param validate
         * @param is document`s InputStream
         * @throws ParserConfigurationException
         * @throws IOException
         * @throws SAXException  */
        public void parseXML(InputStream is, boolean validate)
        throws IOException, SAXException, ParserConfigurationException, FactoryConfigurationError {
            InputSource iSource = new InputSource(is);
            XMLReader parser = getParser(validate);
            parser.parse(iSource);
        }

        private XMLReader getParser(boolean validate)
        throws SAXException, ParserConfigurationException, FactoryConfigurationError {
            XMLReader parser = XMLUtil.createXMLReader(validate);

            // create document handler and register it
            parser.setEntityResolver(this);
            parser.setContentHandler(this);
            parser.setErrorHandler(this);

            return parser;
        }

        @Override
         public void error(SAXParseException exception)
        throws SAXException {
            throw exception;
        }

        @Override
        public void warning(SAXParseException exception)
        throws SAXException {
            throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception)
        throws SAXException {
            throw exception;
        }

        @Override
        public void startElement(String uri, String lname, String name, Attributes attrs)
        throws SAXException {
            tagInProcess = name = name.trim();

            for (int i = 0; i < elmKeyService.length; i++) {
                if (elmKeyService[i].isMyTag(name) != -1) {
                    elmKeyService[i].startElement(name, attrs);

                    return;
                }
            }

            throw new SAXException(NbBundle.getMessage(DefaultAttributes.class, "XML_UnknownElement") + " " + name); // NOI18N
        }

        @Override
        public void endElement(String uri, String lname, String name) throws SAXException {
            for (int i = 0; i < elmKeyService.length; i++) {
                if (elmKeyService[i].isMyTag(name.trim()) != -1) {
                    elmKeyService[i].endElement(name.trim());

                    return;
                }
            }

            throw new SAXException(NbBundle.getMessage(DefaultAttributes.class, "XML_UnknownElement") + " " + name); // NOI18N
        }

        @Override
        public void characters(char[] ch, int start, int length)
        throws SAXException {
            for (int i = 0; i < elmKeyService.length; i++) {
                if (elmKeyService[i].isMyTag(tagInProcess) != -1) {
                    elmKeyService[i].characters(ch, start, length);

                    return;
                }
            }

            throw new SAXException(
                NbBundle.getMessage(DefaultAttributes.class, "XML_UnknownElement") + " " + tagInProcess
            ); // NOI18N
        }

        @Override
        public InputSource resolveEntity(java.lang.String pid, java.lang.String sid)
        throws SAXException {
            if ((pid != null) && pid.equals(publicId)) {
                return new InputSource(publicURL);
            }

            return new InputSource(sid);
        }
    }
}
