/*
 * 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.netbeans.core.startup;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PushbackInputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.DuplicateException;
import org.netbeans.Events;
import org.netbeans.InvalidException;
import org.netbeans.Module;
import org.netbeans.ModuleManager;
import org.netbeans.Stamps;
import org.netbeans.Util;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileSystem.AtomicAction;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Dependency;
import org.openide.modules.InstalledFileLocator;
import org.openide.modules.SpecificationVersion;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.RequestProcessor.Task;
import org.openide.util.Utilities;
import org.openide.util.WeakSet;
import org.openide.xml.EntityCatalog;
import org.openide.xml.XMLUtil;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
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;

/** Class responsible for maintaining the list of modules in the IDE persistently.
 * This class understands the "module status" XML format, and the list of modules
 * present in the Modules/ folder. And it can keep track of module histories.
 * Methods must be called from within appropriate mutex access.
 * @author Jesse Glick
 */
final class ModuleList implements Stamps.Updater {
    static final RequestProcessor RP = new RequestProcessor("Module List Updates"); // NOI18N
    /** The DTD for a module status. */
    public static final String PUBLIC_ID = "-//NetBeans//DTD Module Status 1.0//EN"; // NOI18N
    public static final String SYSTEM_ID = "http://www.netbeans.org/dtds/module-status-1_0.dtd"; // NOI18N
    
    /** Whether to validate module XML files.
     * Safer; only slows down startup in case quickie parse of XML statuses fails for some reason.
     */
    private static final boolean VALIDATE_XML = true;

    private static final Logger LOG = Logger.getLogger(ModuleList.class.getName());

    /** associated module manager */
    private final ModuleManager mgr;
    /** Modules/ folder containing XML data */
    private final FileObject folder;
    /** to fire events with */
    private final Events ev;
    /** map from code name (base)s to statuses of modules on disk */
    private final Map<String,DiskStatus> statuses = new HashMap<String,DiskStatus>(100);
    /** whether the initial round has been triggered or not */
    private boolean triggered = false;
    /** listener for changes in modules, etc.; see comment on class Listener */
    private final Listener listener = new Listener();
    private FileChangeListener weakListener;
    /** atomic actions I have used to change Modules/*.xml */
    private final Set<FileSystem.AtomicAction> myAtomicActions = Collections.<FileSystem.AtomicAction>synchronizedSet(new WeakSet<FileSystem.AtomicAction>(100));
    
    /** Create the list manager.
     * @param mgr the module manager which will actually control the modules at runtime
     * @param folder the Modules/ folder on the system file system to scan/write
     * @param ev the event logger
     */
    public ModuleList(ModuleManager mgr, FileObject folder, Events ev) {
        this.mgr = mgr;
        this.folder = folder;
        this.ev = ev;
        LOG.fine("ModuleList created, storage in " + folder);
    }
    
    /** Read an initial list of modules from disk according to their stored settings.
     * Just reads the XML files in the Modules/ directory, and adds those to
     * the manager's list of modules. Errors are handled internally.
     * Note that the modules encountered are not turned on at this point even if
     * the XML says they should be; but they are added to the list of modules to
     * enable as needed. All discovered modules are returned.
     * Write mutex only.
     */
    public Set<Module> readInitial() {
        ev.log(Events.START_READ);
        final Set<Module> read = new HashSet<Module>();
        try {
            folder.getFileSystem().runAtomicAction(new ReadInitial(read));
        } catch (IOException ioe) {
            LOG.log(Level.WARNING, null, ioe);
        }
        return read;
    }
    
    final Module createModule(
        File jarFile, ModuleHistory hist, boolean reloadable, boolean autoload, 
        boolean eager, Integer startLevel
    ) throws IOException {
        Module m;
        try {
            if (startLevel != null) {
                m = mgr.createBundle(jarFile, hist, reloadable, autoload, eager, startLevel);
            } else {
                m = mgr.create(jarFile, hist, reloadable, autoload, eager);
            }
        } catch (DuplicateException dupe) {
            // XXX should this be tolerated somehow? In case the original is
            // in fact scheduled for deletion anyway?
            throw new IOException(dupe);
        }
        return m;
    }
    
    /**
     * Try to find a module JAR by an XML-supplied name.
     * @param jar the JAR name (relative to an install dir, or a full path)
     * @param name code name base of the module JAR
     * @return an actual JAR file
     * @throws FileNotFoundException if no such JAR file could be found on disk
     * @throws IOException if something else was wrong
     */
    private File findJarByName(String jar, String name) throws IOException {
        File f = new File(jar);
        if (f.isAbsolute()) {
            if (!f.isFile()) throw new FileNotFoundException(f.getAbsolutePath());
            return f;
        } else {
            Set<File> jars = InstalledFileLocator.getDefault().locateAll(jar, name, false);
            if (jars.isEmpty()) {
                throw new FileNotFoundException(jar);
            } else if (jars.size() == 1 || Boolean.getBoolean("org.netbeans.core.startup.ModuleList.firstModuleJarWins")) {
                return jars.iterator().next();
            } else {
                // Pick the newest one available.
                int major = -1;
                SpecificationVersion spec = null;
                File newest = null;
                for (File candidate : jars) {
                    int candidateMajor = -1;
                    SpecificationVersion candidateSpec = null;
                    JarFile jf = new JarFile(candidate);
                    try {
                        java.util.jar.Attributes attr = jf.getManifest().getMainAttributes();
                        String codename = attr.getValue("OpenIDE-Module");
                        if (codename != null) {
                            int slash = codename.lastIndexOf('/');
                            if (slash != -1) {
                                candidateMajor = Integer.parseInt(codename.substring(slash + 1));
                            }
                        }
                        String sv = attr.getValue("OpenIDE-Module-Specification-Version");
                        if (sv != null) {
                            candidateSpec = new SpecificationVersion(sv);
                        }
                    } finally {
                        jf.close();
                    }
                    if (newest == null || candidateMajor > major || (spec != null && candidateSpec != null && candidateSpec.compareTo(spec) > 0)) {
                        newest = candidate;
                        major = candidateMajor;
                        spec = candidateSpec;
                    }
                }
                return newest;
            }
        }
    }
    
    /** Actually go ahead and enable modules which were queued up by
     * reading methods. Should be done after as many modules
     * are collected as possible, in case they have odd mutual
     * dependencies. Also begins listening to further changes.
     * Pass in a list of boot modules which you would
     * like to also try to enable now.
     */
    public void trigger(Set<Module> boot) {
        ev.log(Events.PERF_START, "ModuleList.trigger"); // NOI18N
        if (triggered) throw new IllegalStateException("Duplicate call to trigger()"); // NOI18N
        Set<Module> maybeEnable = new HashSet<Module>(boot);
        for (DiskStatus status: statuses.values()) {
            if (status.pendingInstall) {
                // We are going to try to turn it on...
                status.pendingInstall = false;
                Module m = status.module;
                if (m.isEnabled() || m.isAutoload() || m.isEager()) throw new IllegalStateException();
                maybeEnable.add(m);
            }
        }
        ev.log(Events.PERF_TICK, "modules to enable prepared"); // NOI18N
	
        if (! maybeEnable.isEmpty()) {
            ev.log(Events.START_AUTO_RESTORE, maybeEnable);
            installNew(maybeEnable);
            ev.log(Events.FINISH_AUTO_RESTORE, maybeEnable);
        }
        LOG.fine("ModuleList.trigger: enabled new modules, flushing changes...");
        triggered = true;
        flushInitial();
        ev.log(Events.PERF_END, "ModuleList.trigger"); // NOI18N
    }
    // XXX is this method still needed? rethink...
    private void installNew(Set<Module> modules) {
        if (modules.isEmpty()) {
            return;
        }
        ev.log(Events.PERF_START, "ModuleList.installNew"); // NOI18N
        // First suppress all autoloads.
        Iterator<Module> it = modules.iterator();
        while (it.hasNext()) {
            Module m = it.next();
            if (m.isAutoload() || m.isEager()) {
                it.remove();
            } else if (m.isEnabled()) {
                // Can happen in obscure circumstances: old module A
                // now exists again but with dependency on new module B,
                // and a complete build was not done for A+B, so they have
                // no existing Modules/ *.xml. In such a case B will already
                // have been turned on when restoring A; harmless to remove
                // it from the list here.
                LOG.fine("#17295 fix active for " + m.getCodeNameBase());
                it.remove();
            } else if (!m.isValid()) {
                // Again can also happen if the user upgrades from one version
                // of a module to another. In this case ModuleList correctly removed
                // the old dead module from the manager's list, however it is still
                // in the set of modules to restore.
                LOG.fine("#17471 fix active for " + m.getCodeNameBase());
                it.remove();
            }
        }
        List<Module> toEnable = mgr.simulateEnable(modules);
	for (Module m: toEnable) {
            if (m.isAutoload() || m.isEager()) {
                continue;
            }
            // Quietly turn on others as well:
            if (! modules.contains(m)) {
                modules.add(m);
            }
        }
        Set<Module> missing = new HashSet<Module>(modules);
        missing.removeAll(toEnable);
        if (! missing.isEmpty()) {
            // Include also problematic autoloads and so on needed by these modules.
            Util.transitiveClosureModuleDependencies(mgr, missing);
            it = missing.iterator();
            while (it.hasNext()) {
                Module m = it.next();
                if (m.getProblems().isEmpty()) {
                    it.remove();
                }
            }
            ev.log(Events.FAILED_INSTALL_NEW, missing);
            modules.removeAll(missing);
        }
        try {
            mgr.enable(modules);
        } catch (InvalidException ie) {
            LOG.log(Level.INFO, null, ie);
            Module bad = ie.getModule();
            if (bad == null) throw new IllegalStateException();
            Set<Module> affectedModules = mgr.getModuleInterdependencies(bad, true, true, true);
            ev.log(Events.FAILED_INSTALL_NEW_UNEXPECTED, bad, affectedModules, ie);
            modules.removeAll (affectedModules);
            // Try again without it. Note that some other dependent modules might
            // then be in the missing list for the second round.
            installNew(modules);
        }
        ev.log(Events.PERF_END, "ModuleList.installNew"); // NOI18N
    }
    
    /** Read an XML file using an XMLReader and parse into a map of properties.
     * One distinguished property 'name' is the code name base
     * and is taken from the root element. Others are taken
     * from the param elements.
     * Properties are of type String, Boolean, Integer, or SpecificationVersion
     * according to the property name.
     * @param is the input stream
     * @param reader the XML reader to use to parse; may be null
     * @return a map of named properties to values of various types
     */
    private Map<String,Object> readStatus(InputSource is, XMLReader reader) throws IOException, SAXException {
        if (reader == null) {
            reader = XMLUtil.createXMLReader(VALIDATE_XML);
            reader.setEntityResolver(listener);
            reader.setErrorHandler(listener);
        }
        final Map<String,Object> m = new HashMap<String,Object>();

        DefaultHandler handler = new DefaultHandler() {
            private String modName;
            private String paramName;
            private StringBuffer data = new StringBuffer();
	    
            public @Override void startElement(String uri,
                                     String localname,
                                     String qname,
                                     Attributes attrs) throws SAXException {
                if ("module".equals(qname) ) { // NOI18N
                    modName = attrs.getValue("name"); // NOI18N
                    if( modName == null )
                        throw new SAXException("No module name"); // NOI18N
                    m.put("name", modName.intern()); // NOI18N
                }
                else if (modName != null && "param".equals(qname)) { // NOI18N
                    paramName = attrs.getValue("name");
                    if( paramName == null ) {
                        throw new SAXException("No param name"); // NOI18N
                    }
                    paramName = paramName.intern();
                    data.setLength(0);
                }
            }
	    
            public @Override void characters(char[] ch, int start, int len) {
                if(modName != null  && paramName != null)
                    data.append( ch, start, len );
            }
            
            public @Override void endElement (String uri, String localname, String qname)
                throws SAXException
            {
                if ("param".equals(qname)) { // NOI18N
                    if (modName != null && paramName != null) {
                        if (data.length() == 0)
                            throw new SAXException("No text contents in " + paramName + " of " + modName); // NOI18N
                        
                        try {
                            m.put(paramName, processStatusParam(paramName, data.toString()));
                        } catch (NumberFormatException nfe) {
                            // From either Integer or SpecificationVersion constructors.
                            throw (SAXException) new SAXException(nfe.toString()).initCause(nfe);
                        }

                        data.setLength(0);
                        paramName = null;
                    }
                }
                else if ("module".equals(qname)) { // NOI18N
                    modName = null;
                }
            }
        };
        
        reader.setContentHandler(handler);
        reader.parse(is);

        sanityCheckStatus(m);

        return m;
    }
    
    /** Parse a param value according to a natural type.
     * @param k the param name (must be interned!)
     * @param v the raw string value from XML
     * @return some parsed value suitable for the status map
     */
    private Object processStatusParam(String k, String v) throws NumberFormatException {
        if (k == "enabled" // NOI18N
                   || k == "autoload" // NOI18N
                   || k == "eager" // NOI18N
                   || k == "reloadable" // NOI18N
                   ) {
            return Boolean.valueOf(v);
        } else {
            if (k == "startlevel") { // NOI18N 
                return Integer.valueOf(v);
            }
            // Other properties are of type String.
            // Intern the smaller ones which are likely to be repeated somewhere.
            if (v.length() < 100) v = v.intern();
            return v;
        }
    }
    
    /** Just checks that all the right stuff is there.
     */
    private void sanityCheckStatus(Map<String,Object> m) throws IOException {
        String jar = (String) m.get("jar"); // NOI18N
        if (jar == null) {
            throw new IOException("Must define jar param"); // NOI18N
        }
        if (Boolean.TRUE.equals(m.get("autoload")) && m.get("enabled") != null) { // NOI18N
            throw new IOException("Autoload " + jar + " cannot specify enablement");
        }
        if (Boolean.TRUE.equals(m.get("eager")) && m.get("enabled") != null) { // NOI18N
            throw new IOException("Eager " + jar + " cannot specify enablement");
        }
    }

    // Encoding irrelevant for these getBytes() calls: all are ASCII...
    // (unless someone has their system encoding set to UCS-16!)
    private static final byte[] MODULE_XML_INTRO = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"\n                        \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">\n<module name=\"".getBytes(); // NOI18N
//    private static final byte[] MODULE_XML_DIV1 = ">\n    <param name=\"".getBytes(); // NOI18N
    private static final byte[] MODULE_XML_INTRO_END = ">\n".getBytes(); // NOI18N
    private static final byte[] MODULE_XML_DIV2 = "   <param name=\"".getBytes(); // NOI18N
    private static final byte[] MODULE_XML_DIV3 = "/param>\n".getBytes(); // NOI18N
    private static final byte[] MODULE_XML_END = "/module>\n".getBytes(); // NOI18N
    /** Just like {@link #readStatus(InputSource,XMLReader)} but avoids using an XML parser.
     * If it does not manage to parse it this way, it returns null, in which case
     * you have to use a real parser.
     * @see "#26786"
     */
    private Map<String, Object> readStatus(InputStream is, boolean checkEOF) throws IOException {
        PushbackInputStream pbis = new PushbackInputStream(is, 1);
        Map<String,Object> m = new HashMap<String,Object>(15);
        if (!expect(pbis, MODULE_XML_INTRO)) {
            LOG.fine("Could not read intro");
            return null;
        }
        String name = readTo(pbis, '"');
        if (name == null) {
            LOG.fine("Could not read code name base");
            return null;
        }
        m.put("name", name.intern()); // NOI18N
        if (!expect(pbis, MODULE_XML_INTRO_END)) {
            LOG.fine("Could not read stuff after cnb");
            return null;
        }
        // Now we have <param>s some number of times, finally </module>.
    PARSE:
        while (true) {
            int c = pbis.read();
            switch (c) {
            case ' ':
                // <param>
                if (!expect(pbis, MODULE_XML_DIV2)) {
                    LOG.fine("Could not read up to param");
                    return null;
                }
                String k = readTo(pbis, '"');
                if (k == null) {
                    LOG.fine("Could not read param");
                    return null;
                }
                k = k.intern();
                if (pbis.read() != '>') {
                    LOG.fine("No > at end of <param> " + k);
                    return null;
                }
                String v = readTo(pbis, '<');
                if (v == null) {
                    LOG.fine("Could not read value of " + k);
                    return null;
                }
                if (!expect(pbis, MODULE_XML_DIV3)) {
                    LOG.fine("Could not read end of param " + k);
                    return null;
                }
                try {
                    m.put(k, processStatusParam(k, v));
                } catch (NumberFormatException nfe) {
                    LOG.fine("Number misparse: " + nfe);
                    return null;
                }
                break;
            case '<':
                // </module>
                if (!expect(pbis, MODULE_XML_END)) {
                    LOG.fine("Strange ending");
                    return null;
                }
                if (!checkEOF) {
                    break PARSE;
                }
                if (pbis.read() != -1) {
                    LOG.fine("Trailing garbage");
                    return null;
                }
                // Success!
                break PARSE;
            default:
                LOG.fine("Strange stuff after <param>s: " + c);
                return null;
            }
        }
        sanityCheckStatus(m);
        return m;
    }
    
    /** Read some stuff from a stream and skip over it.
     * Newline conventions are normalized to Unix \n.
     * @return true upon success, false if stream contained something else
     */
    private boolean expect(PushbackInputStream is, byte[] stuff) throws IOException {
        int len = stuff.length;
        boolean inNewline = false;
        for (int i = 0; i < len; ) {
            int c = is.read();
            if (c == 10 || c == 13) {
                // Normalize: s/[\r\n]+/\n/g
                if (inNewline) {
                    continue;
                } else {
                    inNewline = true;
                    c = 10;
                }
            } else {
                inNewline = false;
            }
            if (c != stuff[i++]) {
                return false;
            }
        }
        if (stuff[len - 1] == 10) {
            // Expecting something ending in a \n - so we have to
            // read any further \r or \n and discard.
            int c = is.read();
            if (c != -1 && c != 10 && c != 13) {
                // Got some non-newline character, push it back!
                is.unread(c);
            }
        }
        return true;
    }
    /** Read a maximal string until delim is encountered (which will be removed from stream).
     * This impl reads only ASCII, for speed.
     * Newline conventions are normalized to Unix \n.
     * @return the read string, or null if the delim is not encountered before EOF.
     */
    private String readTo(InputStream is, char delim) throws IOException {
        if (delim == 10) {
            // Not implemented - stream might have "foo\r\n" and we would
            // return "foo" and leave "\n" in the stream.
            throw new IOException("Not implemented"); // NOI18N
        }
        CharArrayWriter caw = new CharArrayWriter(100);
        boolean inNewline = false;
        while (true) {
            int c = is.read();
            if (c == -1) return null;
            if (c > 126) return null;
            if (c == 10 || c == 13) {
                // Normalize: s/[\r\n]+/\n/g
                if (inNewline) {
                    continue;
                } else {
                    inNewline = true;
                    c = 10;
                }
            } else if (c < 32 && c != 9) {
                // Random control character!
                return null;
            } else {
                inNewline = false;
            }
            if (c == delim) {
                return caw.toString();
            } else {
                caw.write(c);
            }
        }
    }

    final Map<String,Map<String,Object>> readCache() {
        InputStream is = Stamps.getModulesJARs().asStream("all-modules.dat"); // NOI18N
        if (is == null) {
            // schedule write for later
            writeCache();
            return null;
        }
        LOG.log(Level.FINEST, "Reading cache all-modules.dat");
        try {
            ObjectInputStream ois = new ObjectInputStream(is);

            Map<String,Map<String,Object>> ret = new HashMap<String, Map<String, Object>>(1333);
            while (is.available() > 0) {
                Map<String, Object> prop = readStatus(ois, false);
                if (prop == null) {
                    LOG.log(Level.CONFIG, "Cache is invalid all-modules.dat");
                    return null;
                }
                Set<?> deps;
                try {
                    deps = (Set<?>) ois.readObject();
                } catch (ClassNotFoundException ex) {
                    throw new IOException(ex);
                }
                prop.put("deps", deps);
                String cnb = (String)prop.get("name"); // NOI18N
                ret.put(cnb, prop);
            }


            is.close();
            return ret;
        } catch (IOException ex) {
            LOG.log(Level.INFO, "Cannot read cache", ex);
            writeCache();
            return null;
        }
    }

    final void writeCache() {
        Stamps.getModulesJARs().scheduleSave(this, "all-modules.dat", false);
    }
    
    @Override
    public void cacheReady() {
    }

    @Override
    public void flushCaches(DataOutputStream os) throws IOException {
        ObjectOutputStream oss = new ObjectOutputStream(os);
        for (Module m : mgr.getModules()) {
            if (m.isFixed()) {
                continue;
            }
            Map<String, Object> prop = computeProperties(m);
            writeStatus(prop, oss);
            oss.writeObject(m.getDependencies());
        }
    }
    
    /** Write a module's status to disk in the form of an XML file.
     * The map of parameters must contain one named 'name' with the code
     * name base of the module.
     */
    private void writeStatus(Map<String, Object> m, OutputStream os) throws IOException {
        String codeName = (String)m.get("name"); // NOI18N
        if (codeName == null)
            throw new IllegalArgumentException("no code name present"); // NOI18N

        Writer w = new OutputStreamWriter(os, "UTF-8"); // NOI18N
        w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); // NOI18N
        w.write("<!DOCTYPE module PUBLIC \""); // NOI18N
        w.write(PUBLIC_ID);
        w.write("\"\n                        \""); // NOI18N
        w.write(SYSTEM_ID);
        w.write("\">\n"); // NOI18N
        w.write("<module name=\""); // NOI18N
        w.write(XMLUtil.toAttributeValue(codeName)); // NOI18N
        w.write("\">\n");       // NOI18N

        // Use TreeMap to sort the keys by name; since the module status files might
        // be version-controlled we want to avoid gratuitous format changes.
        for (Map.Entry<String, Object> entry: new TreeMap<String, Object>(m).entrySet()) {
            String name = entry.getKey();
            if (
                name.equals("name") || // NOI18N
                name.equals("deps") // NOI18N
            ) {
                // Skip this one, it is a pseudo-param.
                continue;
            }
            
            Object val = entry.getValue();

            w.write("    <param name=\""); // NOI18N
            w.write(XMLUtil.toAttributeValue(name)); // NOI18N
            w.write("\">");     // NOI18N
            w.write(XMLUtil.toElementContent(val.toString()));
            w.write("</param>\n"); // NOI18N
        }

        w.write("</module>\n"); // NOI18N
        w.flush();
    }
    
    /** Write information about a module out to disk.
     * If the old status is given as null, this is a newly
     * added module; create an appropriate status and return it.
     * Else update the existing status and return it (it is
     * assumed properties are already updated).
     * Should write the XML and create/rewrite/delete the serialized
     * installer file as needed.
     */
    private DiskStatus writeOut(Module m, DiskStatus old) throws IOException {
        final DiskStatus nue;
        if (old == null) {
            nue = new DiskStatus();
            nue.module = m;
            nue.setDiskProps(computeProperties(m));
        } else {
            nue = old;
        }
        FileSystem.AtomicAction aa = new FileSystem.AtomicAction() {
            public void run() throws IOException {
                if (nue.file == null) {
                    nue.file = FileUtil.createData(folder, ((String)nue.diskProps.get("name")).replace('.', '-') + ".xml"); // NOI18N
                } else {
                    // Just verify that no one else touched it since we last did.
                    if (/*nue.lastApprovedChange != nue.file.lastModified().getTime()*/nue.dirty) {
                        // Oops, something is wrong. #156764 - log at lower level.
                        LOG.log(Level.INFO, null, new IOException("Will not clobber external changes in " + nue.file));
                        return;
                    }
                }
                LOG.fine("ModuleList: (re)writing " + nue.file);
                FileLock lock = nue.file.lock();
                try {
                    OutputStream os = nue.file.getOutputStream(lock);
                    try {
                        writeStatus(nue.diskProps, os);
                    } finally {
                        os.close();
                    }
                } finally {
                    lock.releaseLock();
                }
                //nue.lastApprovedChange = nue.file.lastModified().getTime();
            }
        };
        myAtomicActions.add(aa);
        folder.getFileSystem().runAtomicAction(aa);
        return nue;
    }
    
    /** Delete a module from disk.
     */
    private void deleteFromDisk(final Module m, final DiskStatus status) throws IOException {
        final String nameDashes = m.getCodeNameBase().replace('.', '-'); // NOI18N
        //final long expectedTime = status.lastApprovedChange;
        FileSystem.AtomicAction aa = new FileSystem.AtomicAction() {
            public void run() throws IOException {
                FileObject xml = folder.getFileObject(nameDashes, "xml"); // NOI18N
                if (xml == null) {
                    // Could be that the XML was already deleted externally, etc.
                    LOG.fine("ModuleList: " + m + "'s XML already gone from disk");
                    return;
                }
                //if (xml == null) throw new IOException("No such XML file: " + nameDashes + ".xml"); // NOI18N
                if (status.dirty) {
                    // Someone wrote to the file since we did. Don't delete it blindly!
                    // XXX should this throw an exception, or just warn??
                    throw new IOException("Unapproved external change to " + xml); // NOI18N
                }
                LOG.fine("ModuleList: deleting " + xml);
                /*
                if (xml.lastModified().getTime() != expectedTime) {
                    // Someone wrote to the file since we did. Don't delete it blindly!
                    throw new IOException("Unapproved external change to " + xml); // NOI18N
                }
                 */
                xml.delete();
                FileObject ser = folder.getFileObject(nameDashes, "ser"); // NOI18N
                if (ser != null) {
                    LOG.fine("(and also " + ser + ")");
                    ser.delete();
                }
            }
        };
        myAtomicActions.add(aa);
        folder.getFileSystem().runAtomicAction(aa);
    }
    
    /** Flush the initial state of the module installer after startup to disk.
     * This means:
     * 1. Find all modules in the manager.
     * 2. Anything for which we have no status, write out its XML now
     *    and create a status object for it.
     * 3. Anything for which we have a status, compare the status we
     *    have to its current state (don't forget the installer
     *    serialization state--if this is nonnull, that counts as an
     *    automatic change because it means the module was loaded and
     *    needed to store something).
     * 4. For any changes found in 3., write out new XML (and if
     *    there is any installer state, a new installer ser).
     * 5. Attach listeners to the manager and all modules to catch further
     *    changes in the system so they may be flushed.
     * We could in principle start listening right after readInitial()
     * but it should be more efficient to wait and see what has really
     * changed. Also, some XML may say that a module is enabled, and in
     * fact trigger() was not able to turn it on. In that case, this will
     * show up as a change in step 3. and we will rewrite it as disabled.
     * Called within write mutex by trigger().
     */
    private void flushInitial() {
        LOG.fine("Flushing initial module list...");
        // Find all modules for which we have status already. Treat
        // them as possibly changed, and attach listeners.
        for (Module m : mgr.getModules()) {
            DiskStatus status = statuses.get(m.getCodeNameBase());
            if (status != null) {
                 moduleChanged(m, status);
                m.addPropertyChangeListener(listener);
            }
        }
        // Now find all new and deleted modules.
        moduleListChanged();
        // And listener for new or deleted modules.
        mgr.addPropertyChangeListener(listener);
    }
    
    /** Does the real work when the list of modules changes.
     * Finds newly added modules, creates XML and status for
     * them and begins listening for changes; finds deleted
     * modules, removes their listener, XML, and status.
     * May be called within read or write mutex; since it
     * could be in the read mutex, synchronize (on statuses).
     */
    final void moduleListChanged() {
        synchronized (statuses) {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("ModuleList: moduleListChanged; statuses=" + statuses);
            }
            // Newly added modules first.
            for (Module m : mgr.getModules()) {
                if (m.isFixed() || m.getJarFile() == null) {
                    // No way, we don't manage these.
                    continue;
                }
                final String name = m.getCodeNameBase();
                if (statuses.get(name) == null) {
                    // Yup, it's new. Write it out.
                    LOG.fine("moduleListChanged: added: " + m);
                    try {
                        statuses.put(name, writeOut(m, null));
                        m.addPropertyChangeListener(listener);
                    } catch (IOException ioe) {
                        LOG.log(Level.WARNING, null, ioe);
                        // XXX Now what? Keep it in our list or what??
                    }
                }
            }
            // Now deleted & recreated modules.
            Iterator<DiskStatus> it = statuses.values().iterator();
            while (it.hasNext()) {
                DiskStatus status = it.next();
                if (! status.module.isValid()) {
                    status.module.removePropertyChangeListener(listener);
                    Module nue = mgr.get(status.module.getCodeNameBase());
                    if (nue != null) {
                        // Deleted, but a new module with the same code name base
                        // was created (#5922 e.g.). So change the module reference
                        // in the status and write out any changes to disk.
                        LOG.fine("moduleListChanged: recreated: " + nue);
                        nue.addPropertyChangeListener(listener);
                        status.module = nue;
                        moduleChanged(nue, status);
                    } else {
                        // Newly deleted.
                        LOG.fine("moduleListChanged: deleted: " + status.module);
                        it.remove();
                        try {
                            deleteFromDisk(status.module, status);
                        } catch (IOException ioe) {
                            LOG.log(Level.WARNING, null, ioe);
                        }
                    }
                }
            }
        }
    }
    
    /** Does the real work when one module changes.
     * Compares old and new state and writes XML
     * (and perhaps serialized installer state) as needed.
     * May be called within read or write mutex; since it
     * could be in the read mutex, synchronize (on status).
     */
    private void moduleChanged(Module m, DiskStatus status) {
        synchronized (status) {
            LOG.log(Level.FINE, "moduleChanged: {0}", m);
            Map<String,Object> newProps = computeProperties(m);
            int cnt = 0;
            for (Map.Entry<String, Object> entry : status.diskProps.entrySet()) {
                if (entry.getKey().equals("deps")) { // NOI18N
                    continue;
                }
                Object snd = newProps.get(entry.getKey());
                if (!entry.getValue().equals(snd)) {
                    cnt = -1;
                    break;
                }
                cnt++;
            }
            if (cnt != newProps.size()) {
                if (LOG.isLoggable(Level.FINE)) {
                    Set<Map.Entry<String,Object>> changes = new HashSet<Map.Entry<String,Object>>(newProps.entrySet());
                    changes.removeAll(status.diskProps.entrySet());
                    LOG.fine("ModuleList: changes are " + changes);
                }
                // We need to write changes.
                status.setDiskProps(newProps);
                try {
                    writeOut(m, status);
                } catch (IOException ioe) {
                    LOG.log(Level.WARNING, null, ioe);
                    // XXX now what? continue to manage it anyway?
                }
                writeCache();
            }
        }
    }
    
    /** Compute what properties we would want to store in XML
     * for this module. I.e. 'name', 'reloadable', etc.
     */
    private Map<String,Object> computeProperties(Module m) {
        if (m.isFixed() || ! m.isValid()) throw new IllegalArgumentException("fixed or invalid: " + m); // NOI18N
        Map<String,Object> p = new HashMap<String,Object>();
        p.put("name", m.getCodeNameBase()); // NOI18N
        if (!m.isAutoload() && !m.isEager()) {
            p.put("enabled", m.isEnabled()); // NOI18N
        }
        p.put("autoload", m.isAutoload()); // NOI18N
        p.put("eager", m.isEager()); // NOI18N
        p.put("reloadable", m.isReloadable()); // NOI18N
        if (m.getStartLevel() > 0) {
            p.put("startlevel", m.getStartLevel()); // NOI18N
        }
        if (m.getHistory() instanceof ModuleHistory) {
            ModuleHistory hist = (ModuleHistory) m.getHistory();
            p.put("jar", hist.getJar()); // NOI18N
        }
        return p;
    }
    
    final void init() {
        weakListener = FileUtil.weakFileChangeListener(listener, folder);
        folder.getChildren();
        folder.addFileChangeListener(weakListener);
    }

    final void shutDown() {
        folder.removeFileChangeListener(weakListener);
    }
    /** Listener for changes in set of modules and various properties of individual modules.
     * Also serves as a strict error handler for XML parsing.
     * Also listens to changes in the Modules/ folder and processes them in req proc.
     */
    private final class Listener implements PropertyChangeListener, ErrorHandler, EntityResolver, FileChangeListener, Runnable {
        private final RequestProcessor.Task task;
        
        Listener() {
            task = RP.create(this);
        }
        
        // Property change coming from ModuleManager or some known Module.
        
        private boolean listening = true;
        public void propertyChange(PropertyChangeEvent evt) {
            if (! triggered) throw new IllegalStateException("Property change before trigger()"); // NOI18N
            // REMEMBER this is inside *read* mutex, it is forbidden to even attempt
            // to get write access synchronously here!
            String prop = evt.getPropertyName();
            Object src = evt.getSource();
            if (!listening) {
                // #27106: do not react to our own changes while we are making them
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("ModuleList: ignoring own change " + prop + " from " + src);
                }
                return;
            }
            if (ModuleManager.PROP_CLASS_LOADER.equals(prop) ||
                    ModuleManager.PROP_ENABLED_MODULES.equals(prop) ||
                    Module.PROP_CLASS_LOADER.equals(prop) ||
                    Module.PROP_PROBLEMS.equals(prop) ||
                    Module.PROP_VALID.equals(prop)) {
                // Properties we are not directly interested in, ignore.
                // Note that rather than paying attention to PROP_VALID
                // we simply deal with deletions when PROP_MODULES is fired.
                return;
            } else if (ModuleManager.PROP_MODULES.equals(prop)) {
                moduleListChanged();
            } else if (src instanceof Module) {
                // enabled, manifest, reloadable, possibly other stuff in the future
                Module m = (Module)src;
                if (! m.isValid()) {
                    // Skip it. We will get PROP_MODULES sometime anyway.
                    return;
                }
                DiskStatus status = statuses.get(m.getCodeNameBase());
                if (status == null) {
                    throw new IllegalStateException("Unknown module " + m + "; statuses=" + statuses); // NOI18N
                }
                if (status.pendingInstall && Module.PROP_ENABLED.equals(prop)) {
                    throw new IllegalStateException("Got PROP_ENABLED on " + m + " before trigger()"); // NOI18N
                }
                moduleChanged(m, status);
            } else {
                LOG.fine("Unexpected property change: " + evt + " prop=" + prop + " src=" + src);
            }
        }
        
        // SAX stuff.
        
        public void warning(SAXParseException e) throws SAXException {
            LOG.log(Level.WARNING, null, e);
        }
        public void error(SAXParseException e) throws SAXException {
            throw e;
        }
        public void fatalError(SAXParseException e) throws SAXException {
            throw e;
        }
        public InputSource resolveEntity(String pubid, String sysid) throws SAXException, IOException {
            if (pubid.equals(PUBLIC_ID)) {
                if (VALIDATE_XML) {
                    // We certainly know where to get this from.
                    return new InputSource(ModuleList.class.getResource("module-status-1_0.dtd").toExternalForm()); // NOI18N
                } else {
                    // Not validating, don't load any DTD! Significantly faster.
                    return new InputSource(new ByteArrayInputStream(new byte[0]));
                }
            } else {
                // Otherwise try the standard places.
                return EntityCatalog.getDefault().resolveEntity(pubid, sysid);
            }
        }
        
        // Changes in Modules/ folder.
        
        public void fileDeleted(FileEvent ev) {
            if (isOurs(ev)) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("ModuleList: got expected deletion " + ev);
                }
                return;
            }
            FileObject fo = ev.getFile();
            fileDeleted0(fo.getName(), fo.getExt()/*, ev.getTime()*/);
        }
        
        public void fileDataCreated(FileEvent ev) {
            if (isOurs(ev)) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("ModuleList: got expected creation " + ev);
                }
                return;
            }
            FileObject fo = ev.getFile();
            fileCreated0(fo.getName(), fo.getExt()/*, ev.getTime()*/);
        }
        
        public void fileRenamed(FileRenameEvent ev) {
            if (isOurs(ev)) {
                throw new IllegalStateException("I don't rename anything! " + ev); // NOI18N
            }
            FileObject fo = ev.getFile();
            fileDeleted0(ev.getName(), ev.getExt()/*, ev.getTime()*/);
            fileCreated0(fo.getName(), fo.getExt()/*, ev.getTime()*/);
        }

        private void fileCreated0(String name, String ext/*, long time*/) {
            if ("xml".equals(ext)) { // NOI18N
                String codenamebase = name.replace('-', '.');
                DiskStatus status = statuses.get(codenamebase);
                LOG.fine("ModuleList: outside file creation event for " + codenamebase);
                if (status != null) {
                    // XXX should this really happen??
                    status.dirty = true;
                }
                runme();
            } else if ("ser".equals(ext)) { // NOI18N
                // XXX handle newly added installers?? or not
            } // else ignore
        }
        
        private void fileDeleted0(String name, String ext/*, long time*/) {
            if ("xml".equals(ext)) { // NOI18N
                // Removed module.
                String codenamebase = name.replace('-', '.');
                DiskStatus status = statuses.get(codenamebase);
                LOG.fine("ModuleList: outside file deletion event for " + codenamebase);
                if (status != null) {
                    // XXX should this ever happen?
                    status.dirty = true;
                }
                runme();
            } else if ("ser".equals(ext)) { // NOI18N
                // XXX handle newly deleted installers?? or not
            } // else ignore
        }
        
        public void fileChanged(FileEvent ev) {
            if (isOurs(ev)) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("ModuleList: got expected modification " + ev);
                }
                return;
            }
            FileObject fo = ev.getFile();
            String name = fo.getName();
            String ext = fo.getExt();
            if ("xml".equals(ext)) { // NOI18N
                // Changed module.
                String codenamebase = name.replace('-', '.');
                DiskStatus status = statuses.get(codenamebase);
                LOG.fine("ModuleList: outside file modification event for " + codenamebase + ": " + ev);
                if (status != null) {
                    status.dirty = true;
                } else {
                    // XXX should this ever happen?
                }
                runme();
            } else if ("ser".equals(ext)) { // NOI18N
                // XXX handle changes of installers?? or not
            } // else ignore
        }
        
        public void fileFolderCreated(FileEvent ev) {
            // ignore
        }
        public void fileAttributeChanged(FileAttributeEvent ev) {
            // ignore
        }
        
        /** Check if a given file event in the Modules/ folder was a result
         * of our own manipulations, as opposed to some other code (or polled
         * refresh) manipulating one of these XML files. See #15573.
         */
        private boolean isOurs(FileEvent ev) {
            for (FileSystem.AtomicAction action : myAtomicActions) {
                if (ev.firedFrom(action)) {
                    return true;
                }
            }
            return false;
        }
        
        // Dealing with changes in Modules/ folder and processing them.
        
        private void runme() {
            task.schedule(100);
        }
        @Override
        public void run() {
            LOG.fine("ModuleList: will process outstanding external XML changes");
            mgr.mutexPrivileged().enterWriteAccess();
            try {
                folder.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
                    public void run() throws IOException {
                        // 1. For any dirty XML for which status exists but reloadable differs from XML: change.
                        // 2. For any XML for which we have no status: create & create status, as disabled.
                        // 3. For all dirty XML which says enabled but status says disabled: batch-enable as possible.
                        //    (Where not possible, mark disabled in XML??)
                        // 4. For all dirty XML which says disabled but status says enabled: batch-disable plus others.
                        // 5. For all status for which no XML exists: batch-disable plus others, then delete.
                        // 6. For any dirty XML for which jar/autoload/eager/release/specversion differs from
                        //    actual state of module: warn but do nothing.
                        // 7. For now, ignore any changes in *.ser.
                        // 8. For any dirty XML for which status now exists: replace diskProps with contents of XML.
                        // 9. Mark all statuses clean.
                        // Code name to module XMLs found on disk:
                        Map<String,FileObject> xmlfiles = prepareXMLFiles();
                        // Code name to properties for dirty XML or XML sans status only.
                        Map<String,Map<String,Object>> dirtyprops = prepareDirtyProps(xmlfiles);
                        // #27106: do not listen to changes we ourselves produce.
                        // It only matters if statuses has not been updated before
                        // the changes are fired.
                        listening = false;
                        try {
                            stepCheckReloadable(dirtyprops);
                            stepCreate(xmlfiles, dirtyprops);
                            stepEnable(dirtyprops);
                            stepDisable(dirtyprops);
                            stepDelete(xmlfiles);
                            stepCheckMisc(dirtyprops);
                            stepCheckSer(xmlfiles, dirtyprops);
                        } finally {
                            listening = true;
                            stepUpdateProps(dirtyprops);
                            stepMarkClean();
                        }
                    }
                });
                LOG.fine("ModuleList: finished processing outstanding external XML changes");
            } catch (IOException ioe) {
                LOG.log(Level.WARNING, null, ioe);
            } finally {
                mgr.mutexPrivileged().exitWriteAccess();
            }
        }
        // All the steps called from the run() method to handle disk changes:
        private Map<String,FileObject> prepareXMLFiles() {
            LOG.fine("ModuleList: prepareXMLFiles");
            Map<String,FileObject> xmlfiles = new HashMap<String,FileObject>(100);
            FileObject[] kids = folder.getChildren();
            for (int i = 0; i < kids.length; i++) {
                if (kids[i].hasExt("xml")) { // NOI18N
                    xmlfiles.put(kids[i].getName().replace('-', '.'), kids[i]);
                }
            }
            return xmlfiles;
        }
        private Map<String,Map<String,Object>> prepareDirtyProps(Map<String,FileObject> xmlfiles) throws IOException {
            LOG.fine("ModuleList: prepareDirtyProps");
            Map<String,Map<String,Object>> dirtyprops = new HashMap<String,Map<String,Object>>(100);
            for (Map.Entry<String,FileObject> entry : xmlfiles.entrySet()) {
                String cnb = entry.getKey();
                DiskStatus status = statuses.get(cnb);
                if (status == null || status.dirty) {
                    FileObject xmlfile = entry.getValue();
                    if (xmlfile == null || ! xmlfile.canRead ()) {
                        continue;
                    }
                    Random eth = null;
                    for (int repeats = 0; ; repeats++) {
                        InputStream is = xmlfile.getInputStream();
                        try {
                            InputSource src = new InputSource(is);
                            src.setSystemId(xmlfile.toURL().toString());
                            try {
                                dirtyprops.put(cnb, readStatus(src, null));
                            } catch (SAXException saxe) {
                                final String msg = "Parse error:\n---%<--- " + xmlfile.getPath() + "\n" +
                                    xmlfile.asText("UTF-8") + "\n---%<---\ngot: " + saxe;
                                if (repeats < 10) {
                                    int wait;
                                    if (eth == null) {
                                        eth = new Random();
                                    }
                                    wait = eth.nextInt(90) + 10;
                                    LOG.warning(msg);
                                    LOG.log(Level.INFO, "Retry: {0} after waiting {1}", new Object[] { repeats, wait });
                                    try {
                                        Thread.sleep(wait);
                                    } catch (InterruptedException ignore) {
                                        // not that important
                                    }
                                    continue;
                                }
                                throw new IOException(msg, saxe);
                            }
                        } finally {
                            is.close();
                        }
                        break;
                    }
                }
            }
            return dirtyprops;
        }
        private void stepCheckReloadable(Map<String,Map<String,Object>> dirtyprops) {
            LOG.fine("ModuleList: stepCheckReloadable");
            for (Map.Entry<String,Map<String,Object>> entry : dirtyprops.entrySet()) {
                String cnb = entry.getKey();
                DiskStatus status = statuses.get(cnb);
                if (status != null) {
                    Map<String,Object> props = entry.getValue();
                    Boolean diskReloadableB = (Boolean)props.get("reloadable"); // NOI18N
                    boolean diskReloadable = (diskReloadableB != null ? diskReloadableB.booleanValue() : false);
                    boolean memReloadable = status.module.isReloadable();
                    if (memReloadable != diskReloadable) {
                        LOG.fine("Disk change in reloadable for " + cnb + " from " + memReloadable + " to " + diskReloadable);
                        status.module.setReloadable(diskReloadable);
                    }
                }
            }
        }
        private void stepCreate(Map<String,FileObject> xmlfiles, Map<String,Map<String,Object>> dirtyprops) throws IOException {
            LOG.fine("ModuleList: stepCreate");
            for (Map.Entry<String,FileObject> entry : xmlfiles.entrySet()) {
                String cnb = entry.getKey();
                if (! statuses.containsKey(cnb)) {
                    FileObject xmlfile = entry.getValue();
                    Map<String, Object> props = dirtyprops.get(cnb);
                    if (! cnb.equals(props.get("name"))) throw new IOException("Code name mismatch"); // NOI18N
                    String jar = (String)props.get("jar"); // NOI18N
                    File jarFile;
                    try {
                        jarFile = findJarByName(jar, cnb);
                    } catch (FileNotFoundException fnfe) {
                        final File file = new File(fnfe.getMessage());
                        ev.log(Events.MISSING_JAR_FILE, file, true);
                        final File p = file.getParentFile();
                        File[] arr = p.listFiles();
                        LOG.log(Level.FINE, "Content of {0} is:", p); // NOI18N
                        int cnt = 0;
                        if (arr != null) {
                            for (File f : arr) {
                                LOG.log(Level.FINE, "{0}. = {1}", new Object[] { ++cnt, f }); // NOI18N
                            }
                            LOG.log(Level.FINE, "There was {0} files", cnt); // NOI18N
                        } else {
                            LOG.fine("Directory does not exist"); // NOI18N
                        }
                        dirtyprops.remove(cnb); // #159001
                        continue;
                    }
                    Boolean reloadableB = (Boolean)props.get("reloadable"); // NOI18N
                    boolean reloadable = (reloadableB != null ? reloadableB.booleanValue() : false);
                    Boolean autoloadB = (Boolean)props.get("autoload"); // NOI18N
                    boolean autoload = (autoloadB != null ? autoloadB.booleanValue() : false);
                    Boolean eagerB = (Boolean)props.get("eager"); // NOI18N
                    boolean eager = (eagerB != null ? eagerB.booleanValue() : false);
                    Integer startLevel = (Integer)props.get("startlevel"); // NOI18N
                    ModuleHistory hist = new ModuleHistory(jar, "created from " + xmlfile);
                    Module m = createModule(jarFile, hist, reloadable, autoload, eager, startLevel);
                    m.addPropertyChangeListener(this);
                    // Mark the status as disabled for the moment, so in step 3 it will be turned on
                    // if in dirtyprops it was marked enabled.
                    Map<String, Object> statusProps;
                    if (props.get("enabled") != null && ((Boolean)props.get("enabled")).booleanValue()) { // NOI18N
                        statusProps = new HashMap<String, Object>(props);
                        statusProps.put("enabled", Boolean.FALSE); // NOI18N
                    } else {
                        statusProps = props;
                    }
                    DiskStatus status = new DiskStatus();
                    status.module = m;
                    status.file = xmlfile;
                    status.setDiskProps(statusProps);
                    statuses.put(cnb, status);
                }
            }
        }
        private void stepEnable(Map<String,Map<String,Object>> dirtyprops) throws IOException {
            LOG.fine("ModuleList: stepEnable");
            if (LOG.isLoggable(Level.FINEST)) {
                for (Entry<String, Map<String, Object>> e : dirtyprops.entrySet()) {
                    LOG.log(Level.FINEST, "{0} = {1}", new Object[]{e.getKey(), e.getValue()}); // NOI18N
                }
            }
            Set<Module> toenable = new HashSet<Module>();
            for (Map.Entry<String,Map<String,Object>> entry : dirtyprops.entrySet()) {
                String cnb = entry.getKey();
                Map<String, Object> props = entry.getValue();
                if (props.get("enabled") != null && ((Boolean)props.get("enabled")).booleanValue()) { // NOI18N
                    DiskStatus status = statuses.get(cnb);
                    assert status != null : cnb; // #159001
                    if (status.diskProps.get("enabled") == null || ! ((Boolean)status.diskProps.get("enabled")).booleanValue()) { // NOI18N
                        if (status.module.isEnabled()) throw new IllegalStateException("Already enabled: " + status.module); // NOI18N
                        toenable.add(status.module);
                    }
                }
            }
            if (LOG.isLoggable(Level.FINEST)) {
                for (Module m : toenable) {
                    LOG.log(Level.FINEST, "About to enable {0}", m); // NOI18N
                }
            }
            installNew(toenable);
        }
        private void stepDisable(Map<String,Map<String,Object>> dirtyprops) throws IOException {
            LOG.fine("ModuleList: stepDisable");
            Set<Module> todisable = new HashSet<Module>();
            for (Map.Entry<String,Map<String,Object>> entry: dirtyprops.entrySet()) {
                String cnb = entry.getKey();
                Map<String, Object> props = entry.getValue();
                if (props.get("enabled") == null || ! ((Boolean)props.get("enabled")).booleanValue()) { // NOI18N
                    DiskStatus status = statuses.get(cnb);
                    assert status != null : cnb; // #159001
                    if (Boolean.TRUE.equals(status.diskProps.get("enabled"))) { // NOI18N
                        if (! status.module.isEnabled()) throw new IllegalStateException("Already disabled: " + status.module); // NOI18N
                        todisable.add(status.module);
                    }
                }
            }
            if (todisable.isEmpty()) {
                return;
            }
            List<Module> reallydisable = mgr.simulateDisable(todisable);
	    for (Module m: reallydisable) {
                if (!m.isAutoload() && !m.isEager() && !todisable.contains(m)) {
                    todisable.add(m);
                }
            }
            mgr.disable(todisable);
        }
        private void stepDelete(Map<String,FileObject> xmlfiles) throws IOException {
            LOG.fine("ModuleList: stepDelete");
            Set<Module> todelete = new HashSet<Module>();
            Iterator<Map.Entry<String,DiskStatus>> it = statuses.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String,DiskStatus> entry = it.next();
                String cnb = entry.getKey();
                DiskStatus status = entry.getValue();
                if (! xmlfiles.containsKey(cnb)) {
                    Module m = status.module;
                    todelete.add(m);
                    it.remove();
                }
            }
            if (todelete.isEmpty()) {
                return;
            }
            Set<Module> todisable = new HashSet<Module>();
	    for (Module m: todelete) {
                if (m.isEnabled() && !m.isAutoload() && !m.isEager()) {
                    todisable.add(m);
                }
            }
            List<Module> reallydisable = mgr.simulateDisable(todisable);
	    for (Module m: reallydisable) {
                if (!m.isAutoload() && !m.isEager() && !todisable.contains(m)) {
                    todisable.add(m);
                }
            }
            mgr.disable(todisable);
            // In case someone tried to delete an enabled autoload/eager module:
            Iterator<Module> delIt = todelete.iterator();
            while (delIt.hasNext()) {
                Module m = delIt.next();
                if (m.isEnabled()) {
                    if (!m.isAutoload() && !m.isEager()) throw new IllegalStateException("Module " + m + " scheduled for deletion could not be disabled yet was not an autoload nor eager"); // NOI18N
                    // XXX is it better to find all regular module using it and turn all of those off?
                    ev.log(Events.CANT_DELETE_ENABLED_AUTOLOAD, m);
                    delIt.remove();
                } else {
                    mgr.delete(m);
                }
            }
        }
        private void stepCheckMisc(Map<String,Map<String,Object>> dirtyprops) {
            LOG.fine("ModuleList: stepCheckMisc");
            String[] toCheck = {"jar", "autoload", "eager"}; // NOI18N
            for (Map.Entry<String,Map<String,Object>> entry : dirtyprops.entrySet()) {
                String cnb = entry.getKey();
                Map<String,Object> props = entry.getValue();
                DiskStatus status = statuses.get(cnb);
                assert status != null : cnb; // #159001
                Map<String,Object> diskProps = status.diskProps;
                for (int i = 0; i < toCheck.length; i++) {
                    String prop = toCheck[i];
                    Object onDisk = props.get(prop);
                    Object inMem = diskProps.get(prop);
                    if (! Utilities.compareObjects(onDisk, inMem)) {
                        ev.log(Events.MISC_PROP_MISMATCH, status.module, prop, onDisk, inMem);
                    }
                }
            }
        }
        private void stepCheckSer(Map/*<String,FileObject>*/ xmlfiles, Map/*<String,Map<String,Object>>*/ dirtyprops) {
            // There is NO step 7!
        }
        private void stepUpdateProps(Map<String,Map<String,Object>> dirtyprops) {
            LOG.fine("ModuleList: stepUpdateProps");
            for (Map.Entry<String,Map<String,Object>> entry: dirtyprops.entrySet()) {
                String cnb = entry.getKey();
                DiskStatus status = statuses.get(cnb);
                if (status != null) {
                    Map<String,Object> props = entry.getValue();
                    status.setDiskProps(props);
                }
            }
        }
        private void stepMarkClean() {
            LOG.fine("ModuleList: stepMarkClean");
            for (DiskStatus status : statuses.values()) {
                status.dirty = false;
            }
        }
        
    }
    
    /** Representation of the status of a module on disk and so on. */
    private static final class DiskStatus {
        /** Initialize as a struct, i.e. member by member: */
        public DiskStatus() {}
        /** actual module object */
        public Module module;
        /** XML file holding its status */
        public FileObject file;
        /** timestamp of last modification to XML file that this class did */
        //public long lastApprovedChange;
        /** if true, this module was scanned and should be enabled but we are waiting for trigger */
        public boolean pendingInstall = false;
        /** properties of the module on disk */
        public Map<String,Object /*String|Integer|Boolean|SpecificationVersion*/> diskProps;
        void setDiskProps(Map<String,Object> diskProps) {
            Parameters.notNull("diskProps", diskProps);
            this.diskProps = diskProps;
        }
        /** if true, the XML was changed on disk by someone else */
        public boolean dirty = false;
        /** for debugging: */
        public @Override String toString() {
            return "DiskStatus[module=" + module + // NOI18N
                ",valid=" + module.isValid() + // NOI18N
                ",file=" + file + /*",lastApprovedChange=" + new Date(lastApprovedChange) +*/ // NOI18N
                ",dirty=" + dirty + // NOI18N
                ",pendingInstall=" + pendingInstall + // NOI18N
                ",diskProps=" + diskProps + "]"; // NOI18N
        }
    }

    private class ReadInitial implements AtomicAction, Runnable {
        private final Set<Module> read;
        private volatile Task task;

        public ReadInitial(Set<Module> read) {
            this.read = read;
        }

        @Override
        public void run() {
            if (task != null) {
                init();
                return;
            }
            task = RP.create(this);
            task.schedule(0);
            
            Map<String, Map<String, Object>> cache = readCache();
            String[] names;
            if (cache != null) {
                names = cache.keySet().toArray(new String[cache.size()]);
            } else {
                FileObject[] children = folder.getChildren();
                List<String> arr = new ArrayList<String>(children.length);
                for (FileObject f : children) {
                    if (f.hasExt("ser")) { // NOI18N
                        // Fine, skip over.
                    } else if (f.hasExt("xml")) {
                        // NOI18N
                        // Assume this is one of ours. Note fixed naming scheme.
                        String nameDashes = f.getName(); // NOI18N
                        char[] badChars = {'.', '/', '>', '='};
                        for (int j = 0; j < 4; j++) {
                            if (nameDashes.indexOf(badChars[j]) != -1) {
                                throw new IllegalArgumentException("Bad name: " + nameDashes); // NOI18N
                            }
                        }
                        String name = nameDashes.replace('-', '.').intern(); // NOI18N
                        arr.add(name);
                    } else {
                        LOG.fine("Strange file encountered in modules folder: " + f);
                    }
                }
                names = arr.toArray(new String[arr.size()]);
            }
            ev.log(Events.MODULES_FILE_SCANNED, names.length);
            XMLReader reader = null;
            for (int i = 0; i < names.length; i++) {
                String name = names[i];
                FileObject f = null;
                try {
                    // OK, read it from disk.
                    Map<String, Object> props = cache == null ? null : cache.get(name);
                    if (props == null) {
                        // Now name is the code name base of the module we expect to find.
                        // Check its format (throws IllegalArgumentException if bad):
                        Dependency.create(Dependency.TYPE_MODULE, name);
                        LOG.log(Level.FINEST, "no cache for {0}", name);
                        f = folder.getFileObject(name.replace('.', '-') + ".xml");
                        InputStream is = f.getInputStream();
                        try {
                            props = readStatus(new BufferedInputStream(is), true);
                            if (props == null) {
                                LOG.warning("Note - failed to parse " + f + " the quick way, falling back on XMLReader");
                                is.close();
                                is = f.getInputStream();
                                InputSource src = new InputSource(is);
                                // Make sure any includes etc. are handled properly:
                                src.setSystemId(f.toURL().toExternalForm());
                                if (reader == null) {
                                    try {
                                        reader = XMLUtil.createXMLReader();
                                    } catch (SAXException e) {
                                        throw (IllegalStateException) new IllegalStateException(e.toString()).initCause(e);
                                    }
                                    reader.setEntityResolver(listener);
                                    reader.setErrorHandler(listener);
                                }
                                props = readStatus(src, reader);
                            }
                        } finally {
                            is.close();
                        }
                    }
                    if (!name.equals(props.get("name"))) {
                        throw new IOException("Code name mismatch: " + name + " vs. " + props.get("name")); // NOI18N
                    }
                    Boolean enabledB = (Boolean) props.get("enabled"); // NOI18N
                    String jar = (String) props.get("jar"); // NOI18N
                    File jarFile;
                    try {
                        jarFile = findJarByName(jar, name);
                    } catch (FileNotFoundException fnfe) {
                        //LOG.fine("Cannot find: " + fnfe.getMessage());
                        ev.log(Events.MISSING_JAR_FILE, new File(fnfe.getMessage()), enabledB);
                        if (f != null && !Boolean.FALSE.equals(enabledB)) {
                            try {
                                f.delete();
                            } catch (IOException ioe) {
                                LOG.log(Level.WARNING, null, ioe);
                            }
                        }
                        continue;
                    }
                    ModuleHistory history = new ModuleHistory(jar, "loaded from " + f); // NOI18N
                    Boolean reloadableB = (Boolean) props.get("reloadable"); // NOI18N
                    boolean reloadable = reloadableB != null ? reloadableB.booleanValue() : false;
                    boolean enabled = enabledB != null ? enabledB.booleanValue() : false;
                    Boolean autoloadB = (Boolean) props.get("autoload"); // NOI18N
                    boolean autoload = autoloadB != null ? autoloadB.booleanValue() : false;
                    Boolean eagerB = (Boolean) props.get("eager"); // NOI18N
                    boolean eager = eagerB != null ? eagerB.booleanValue() : false;
                    NbInstaller.register(name, props.get("deps")); // NOI18N
                    Integer startLevel = (Integer)props.get("startlevel"); // NOI18N
                    Module m = createModule(jarFile, history, reloadable, autoload, eager, startLevel);
                    NbInstaller.register(null, null);
                    read.add(m);
                    DiskStatus status = new DiskStatus();
                    status.module = m;
                    status.file = f;
                    //status.lastApprovedChange = children[i].lastModified().getTime();
                    status.pendingInstall = enabled;
                    // Will only really be flushed if mgr props != disk props, i.e
                    // if version changed or could not be enabled.
                    //status.pendingFlush = true;
                    status.setDiskProps(props);
                    statuses.put(name, status);
                } catch (Exception e) {
                    LOG.log(Level.WARNING, "Error encountered while reading " + name, e);
                }
                ev.log(Events.MODULES_FILE_PROCESSED, name);
            }
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("read initial XML files: statuses=" + statuses);
            }
            ev.log(Events.FINISH_READ, read);
            // Handle changes in the Modules/ folder on disk by parsing & applying them.
            task.waitFinished();
        }
    }
    
}
