/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.autoupdate.catalog;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dialog;
import org.netbeans.core.NbTopManager;
import org.netbeans.core.startup.*;
import org.netbeans.*;
import java.util.*;
import javax.swing.SwingUtilities;
import org.openide.modules.SpecificationVersion;
import java.io.File;
import java.beans.*;
import org.openide.*;
import java.io.IOException;
import org.openide.util.*;

/** Bean representing a module.
 * Mirrors its properties but provides safe access from the event thread.
 * Also permits delayed write access, again safe to initiate from the event thread.
 * These changes are batched and auto-validating.
 * @author Jesse Glick
 */
public final class ModuleBean implements Runnable, PropertyChangeListener {
    
    private static final ErrorManager err = ErrorManager.getDefault().getInstance("org.netbeans.modules.autoupdate.catalog.ModuleBean"); // NOI18N
    
    private final Module module;
    
    private String codeName;
    private String codeNameBase;
    private String specVers;
    private String implVers;
    private String buildVers;
    private String[] provides;
    private File jar;
    private boolean enabled;
    private boolean reloadable;
    private boolean autoload;
    private boolean eager;
    private boolean problematic;
    private String[] problemDescriptions;
    private String displayName;
    private String shortDescription;
    private String longDescription;
    private String category;
    private String classpath;
    
    /** Must be created within mutex. */
    private ModuleBean(Module m) {
        module = m;
        loadProps();
        module.addPropertyChangeListener(org.openide.util.WeakListeners.propertyChange (this, module));
    }
    
    /** If necessary, get the underlying module. */
    public Module getModule() {
        return module;
    }
    
    private void loadProps() {
        if (SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
        err.log("loadProps: module=" + module);
        if (! module.isValid()) {
            err.log("invalid, forget it...");
            return;
        }
        // Set fields. Called inside read mutex.
        codeName = module.getCodeName();
        codeNameBase = module.getCodeNameBase();
        SpecificationVersion sv = module.getSpecificationVersion();
        specVers = (sv == null ? null : sv.toString());
        implVers = module.getImplementationVersion ();
        buildVers = module.getBuildVersion ();
        provides = module.getProvides();
        jar = module.getJarFile();
        enabled = module.isEnabled();
        reloadable = module.isReloadable();
        autoload = module.isAutoload();
        eager = module.isEager();
        Set problems = module.getProblems();
        problematic = !problems.isEmpty();
        if (problematic) {
            problemDescriptions = new String[problems.size()];
            Iterator it = problems.iterator();
            int i = 0;
            while (it.hasNext()) {
                problemDescriptions[i++] = NbProblemDisplayer.messageForProblem(module, it.next());
            }
        } else {
            problemDescriptions = null;
        }
        displayName = module.getDisplayName();
        shortDescription = (String)module.getLocalizedAttribute("OpenIDE-Module-Short-Description"); // NOI18N
        longDescription = (String)module.getLocalizedAttribute("OpenIDE-Module-Long-Description"); // NOI18N
        category = (String)module.getLocalizedAttribute("OpenIDE-Module-Display-Category"); // NOI18N
        classpath = NbTopManager.getUninitialized().getModuleSystem().getEffectiveClasspath(module);
    }
    
    /** Get the code name. */
    public String getCodeName() {
        return codeName;
    }
    
    /** Get the code name base. */
    public String getCodeNameBase() {
        return codeNameBase;
    }
    
    /** Get the specification version, or null. */
    public String getSpecificationVersion() {
        return specVers;
    }
    
    /** Get the implementation version, or null. */
    public String getImplementationVersion() {
        return implVers;
    }
    
    /** Get the build version, or null. */
    public String getBuildVersion() {
        return buildVers;
    }
    
    /** Get a list of provided tokens (never null, maybe empty). */
    public String[] getProvides() {
        return provides;
    }
    
    /** Get the module JAR file, or null. */
    public File getJar() {
        return jar;
    }
    
    /** Test whether the module is enabled. */
    public boolean isEnabled() {
        return enabled;
    }
    
    /** Enable/disable the module. */
    public void setEnabled(boolean e) {
        if (enabled == e) return;
        if (jar == null || autoload || eager || problematic) throw new IllegalStateException();
        err.log("setEnabled: module=" + module + " enabled=" + e);
        enabled = e; // optimistic change
        supp.firePropertyChange("enabled", null, null); // NOI18N
        Update u = new Update(e ? "enable" : "disable", module); // NOI18N
        AllModulesBean.getDefault().update(u);
    }
    
    /** Test whether the module is a library module. */
    public boolean isAutoload() {
        return autoload;
    }
    
    /** Test whether the module is a bridge module. */
    public boolean isEager() {
        return eager;
    }
    
    /** Test whether the module is reloadable. */
    public boolean isReloadable() {
        return reloadable;
    }
    
    /** Set whether the module is reloadable. */
    public void setReloadable(boolean r) {
        // XXX sanity-check
        if (reloadable == r) return;
        err.log("setReloadable: module=" + module + " reloadable=" + r);
        reloadable = r; // optimistic change
        supp.firePropertyChange("reloadable", null, null); // NOI18N
        Update u = new Update(r ? "makeReloadable" : "makeUnreloadable", module); // NOI18N
        AllModulesBean.getDefault().update(u);
    }
    
    /** Delete the module. */
    public void delete() {
        if (jar == null) throw new IllegalStateException();
        err.log("delete: module=" + module);
        Update u = new Update("delete", module); // NOI18N
        AllModulesBean.getDefault().update(u);
    }
    
    /** Test whether the module has problems with installation. */
    public boolean isProblematic() {
        return problematic;
    }
    
    /**
     * Get a list of descriptions of each problem in the module (if it has any).
     * Each item will be a localized phrase.
     * @return a nonempty array of explanations if {@link #isProblematic}, null otherwise
     * @see NbProblemDisplayer#messageForProblem
     * @see "#16636"
     */
    public String[] getProblemDescriptions() {
        return problemDescriptions;
    }
    
    /** Get the display name. */
    public String getDisplayName() {
        return displayName;
    }
    
    /** Get the short description, or null. */
    public String getShortDescription() {
        return shortDescription;
    }
    
    /** Get the long description, or null. */
    public String getLongDescription() {
        return longDescription;
    }
    
    /** Get the display category, or null. */
    public String getCategory() {
        return category;
    }
    
    /** Get the effective classpath for this module.
     * May be the empty string for a disabled module.
     * @see ModuleSystem#getEffectiveClasspath
     * @see "#22466"
     */
    public String getEffectiveClasspath() {
        return classpath;
    }
    
    private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
    
    /** Listen to changes in bean properties. */
    public void addPropertyChangeListener(PropertyChangeListener l) {
        supp.removePropertyChangeListener(l);
        supp.addPropertyChangeListener(l);
    }
    
    /** Stop listening to changes in bean properties. */
    public void removePropertyChangeListener(PropertyChangeListener l) {
        supp.removePropertyChangeListener(l);
    }
    
    public void propertyChange(PropertyChangeEvent evt) {
        if (SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
        // Something on the module changed. Inside read mutex.
        err.log("got changes: module=" + module + " evt=" + evt);
        if (/* #13834 */ evt != null && Module.PROP_CLASS_LOADER.equals(evt.getPropertyName())) {
            err.log("ignoring PROP_CLASS_LOADER");
            // Speed optimization.
            return;
        }
        loadProps();
        SwingUtilities.invokeLater(this);
    }
    
    public void run() {
        if (! SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
        // Inside event thread after a change.
        err.log("firing changes: module=" + module);
        supp.firePropertyChange(null, null, null);
        ModuleSelectionPanel.getGUI (false).setWaitingState (false, false);
    }
    
    // ModuleNode uses these as keys, so make sure even if recreated after change
    // in list of modules that the node selection is retained. Cf. #23757 however:
    
    public boolean equals(Object o) {
        return (o instanceof ModuleBean) &&
            codeNameBase.equals(((ModuleBean)o).codeNameBase);
    }
    
    public int hashCode() {
        return 35632846 ^ codeNameBase.hashCode();
    }
    
    public String toString() {
        return "ModuleBean[" + codeNameBase + "]"; // NOI8N
    }
    
    public static final class AllModulesBean implements Runnable, PropertyChangeListener, Comparator {
        
        private static AllModulesBean deflt = null;
        /** Get the bean representing all modules. */
        public static synchronized AllModulesBean getDefault() {
            if (deflt == null) deflt = new AllModulesBean();
            return deflt;
        }
        private AllModulesBean() {}
        
        private final ModuleManager mgr = NbTopManager.getUninitialized().getModuleSystem().getManager();
        private final Events ev = NbTopManager.getUninitialized().getModuleSystem().getEvents();
        
        private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
        
        /** Listen to changes in the list of modules or whether there are pending updates. */
        public void addPropertyChangeListener(PropertyChangeListener l) {
            supp.removePropertyChangeListener(l);
            supp.addPropertyChangeListener(l);
        }
        
        /** Stop listening to changes in the list of modules and whether there are pending updates. */
        public void removePropertyChangeListener(PropertyChangeListener l) {
            supp.removePropertyChangeListener(l);
        }
        
        private ModuleBean[] modules = null;
        
        private Task recalcTask = null;
        
        /** Get the list of all modules. */
        public synchronized ModuleBean[] getModules() {
            err.log("getModules: modules count=" + (modules == null ? "null" : String.valueOf(modules.length)));
            if (modules == null) {
                recalcTask = RequestProcessor.getDefault().post(new Reader());
                modules = new ModuleBean[0];
                return modules;
            } else {
                return modules/*.clone()*/;
            }
        }
        
        /** Get a task representing the need to get all modules.
         * When it is finished (if it is not already), they will be ready.
         * It is <em>not</em> guaranteed that changes will have been fired to
         * listeners by the time this task finishes.
         */
        public synchronized Task waitForModules() {
            getModules();
            if (recalcTask != null) {
                return recalcTask;
            } else {
                return Task.EMPTY;
            }
        }
        
        /** Create a new module from JAR file, perhaps reloadable. */
        public void create(File jar, boolean reloadable) {
            err.log("create: jar=" + jar);
            Update u = new Update(reloadable ? "createReloadable" : "create", jar); // NOI18N
            update(u);
        }
        
        private class Reader implements Runnable {
            private boolean theother = false;
            Reader() {}
            /** Called first in request processor, then pushed to read mutex,
             * to read list of modules.
             */
            public void run() {
                if (SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
                if (! theother) {
                    err.log("will load modules in read mutex...");
                    Reader r = new Reader();
                    r.theother = true;
                    mgr.mutex().readAccess(r);
                    return;
                }
                err.log("first time, finding module list");
                // First time. We are in read mutex and need to find out what is here.
                Set modulesSet = mgr.getModules();
                ModuleBean[] _modules = new ModuleBean[modulesSet.size()];
                Iterator it = modulesSet.iterator();
                int i = 0;
                while (it.hasNext()) {
                    Module m = (Module)it.next();
                    _modules[i++] = new ModuleBean(m);
                }
                synchronized (AllModulesBean.this) {
                    modules = _modules;
                    recalcTask = null;
                }
                // Listen for further changes.
                mgr.addPropertyChangeListener(org.openide.util.WeakListeners.propertyChange(AllModulesBean.this, mgr));
                // Relative to the initial list of zero modules, something 'has changed'.
                SwingUtilities.invokeLater(AllModulesBean.this);
            }
        }
        
        public void run() {
            if (! SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
            err.log("in event thread, will fire changes");
            // Something changed and now we are in the event thread.
            // (Either list of modules or pending changes or both.)
            supp.firePropertyChange(null, null, null);
        }
        
        public void propertyChange(PropertyChangeEvent evt) {
            if (SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
            err.log("got changes: evt=" + evt);
            ModuleSelectionPanel.getGUI (false).setWaitingState (true, true);
            if (ModuleManager.PROP_MODULES.equals(evt.getPropertyName())) {
                // Later on. Something changed. Again in read mutex.
                Map modules2Beans = new HashMap(modules.length * 4 / 3 + 1);
                for (int i = 0; i < modules.length; i++) {
                    modules2Beans.put(modules[i].getModule(), modules[i]);
                }
                Set modulesSet = mgr.getModules();
                ModuleBean[] themodules = new ModuleBean[modulesSet.size()];
                Iterator it = modulesSet.iterator();
                int i = 0;
                while (it.hasNext()) {
                    Module m = (Module)it.next();
                    ModuleBean existing = (ModuleBean)modules2Beans.get(m);
                    if (existing == null) existing = new ModuleBean(m);
                    themodules[i++] = existing;
                }
                synchronized (this) {
                    modules = themodules;
                }
                // Fire changes later.
                SwingUtilities.invokeLater(this);
            }
        }
        
        private final List updates = new LinkedList(); // List<Update>
        
        private boolean paused = false;
        
        private Runnable updater = new Updater();
        
        private RequestProcessor.Task updaterTask = new RequestProcessor ("module-bean-updater").create (updater);
        
        /** Pause any pending updates for later. */
        public void pause() {
            err.log("pause");
            paused = true;
        }
        
        /** Resume any previously paused updates. */
        public void resume() {
            err.log("resume");
            paused = false;
            updaterTask.schedule (0);
        }
        
        /** Cancel any previously posted updates (whether paused or not). */
        public void cancel() {
            err.log("cancel");
            synchronized (updates) {
                updates.clear();
            }
            paused = false;
            supp.firePropertyChange("pending", null, null); // NOI18N
        }
        
        /** Test whether there are any pending updates (whether paused or not). */
        public boolean isPending() {
            synchronized (updates) {
                return ! updates.isEmpty();
            }
        }
        
        /** Called from event thread.
         * Access only from within this class or from ModuleBean.
         */
        void update(Update u) {
            synchronized (updates) {
                // If nonempty, we are already waiting for it...
                boolean runme = updates.isEmpty();
                updates.add(u);
                err.log("pending updates: " + updates);
                if (runme) {
                    updaterTask.schedule (0);
                }
            }
            supp.firePropertyChange("pending", null, null); // NOI18N
        }
        
        private class Updater implements Runnable {
            Updater() {}
            /** Called from request processor to actually perform updates.
             * Or from the write mutex if isWriteAccess().
             */
            public void run() {
                if (SwingUtilities.isEventDispatchThread()) throw new IllegalStateException();
                if (! mgr.mutex ().isWriteAccess ()) {
                    err.log("saving all documents...");
                    org.openide.LifecycleManager.getDefault ().saveAll ();
                    err.log("will run updates in write mutex...");
                    mgr.mutex().writeAccess(this);
                    return;
                }
                try {
                    if (paused) {
                        err.log("run updates, but paused");
                        return;
                    }
                    ModuleSelectionPanel.getGUI (false).setWaitingState (true, true);
                    Set toEnable = new HashSet(); // Set<Module>
                    Set toDisable = new HashSet(); // Set<Module>
                    Set toMakeReloadable = new HashSet(); // Set<Module>
                    Set toMakeUnreloadable = new HashSet(); // Set<Module>
                    Set toDelete = new HashSet(); // Set<Module>
                    Set toCreate = new HashSet(); // Set<File>
                    Set toCreateReloable = new HashSet(); // Set<File>
                    Iterator it;
                    synchronized (updates) {
                        if (updates.isEmpty()) {
                            err.log("run updates, but empty");
                            return;
                        }
                        err.log("run updates: " + updates);
                        it = new LinkedList(updates).iterator();
                        updates.clear();
                    }
                    while (it.hasNext()) {
                        Update u = (Update)it.next();
                        if (u.command.equals("enable")) { // NOI18N
                            if (toDelete.contains(u.arg)) throw new IllegalStateException();
                            toDisable.remove(u.arg);
                            toEnable.add(u.arg);
                        } else if (u.command.equals("disable")) { // NOI18N
                            if (toDelete.contains(u.arg)) throw new IllegalStateException();
                            toEnable.remove(u.arg);
                            toDisable.add(u.arg);
                        } else if (u.command.equals("makeReloadable")) { // NOI18N
                            if (toDelete.contains(u.arg)) throw new IllegalStateException();
                            toMakeUnreloadable.remove(u.arg);
                            toMakeReloadable.add(u.arg);
                        } else if (u.command.equals("makeUnreloadable")) { // NOI18N
                            if (toDelete.contains(u.arg)) throw new IllegalStateException();
                            toMakeReloadable.remove(u.arg);
                            toMakeUnreloadable.add(u.arg);
                        } else if (u.command.equals("delete")) { // NOI18N
                            toEnable.remove(u.arg);
                            toDisable.remove(u.arg); // will always be disabled anyway
                            toMakeReloadable.remove(u.arg);
                            toMakeUnreloadable.remove(u.arg);
                            toDelete.add(u.arg);
                        } else if (u.command.equals("create")) { // NOI18N
                            toCreateReloable.remove(u.arg);
                            toCreate.add(u.arg);
                        } else if (u.command.equals("createReloadable")) { // NOI18N
                            toCreate.remove(u.arg);
                            toCreateReloable.add(u.arg);
                        } else {
                            throw new IllegalStateException();
                        }
                    }
                    doDelete(toDelete);
                    doDisable(toDisable, true);
                    it = toMakeReloadable.iterator();
                    while (it.hasNext()) {
                        Module m = (Module)it.next();
                        m.setReloadable(true);
                    }
                    it = toMakeUnreloadable.iterator();
                    while (it.hasNext()) {
                        Module m = (Module)it.next();
                        m.setReloadable(false);
                    }
                    doEnable(toEnable);
                    doCreate(toCreate, false);
                    doCreate(toCreateReloable, true);
                } catch (RuntimeException re) {
                    err.notify(re);
                    // Never know. Revert everything to real state just in case.
                    // #17873: if not inited, no need...
                    ModuleBean[] _modules = modules;
                    if (_modules != null) {
                        for (int i = 0; i < _modules.length; i++) {
                            _modules[i].propertyChange(null);
                        }
                    }
                } finally {
                    ModuleSelectionPanel.getGUI (false).setWaitingState (false, true);
                }
                // Fire a change in pending property.
                SwingUtilities.invokeLater(AllModulesBean.this);
            }
            
        }
        
        // Actual command to do certain kinds of updates, called within
        // write mutex and should take care of their own UI. Note that it
        // is OK to block on the event thread here indirectly, by means of
        // calling TopManager.notify and waiting for the result.
        
        private void doDelete(Set modules) {
            if (modules.isEmpty()) return;
            err.log("doDelete: " + modules);
            // Have to be turned off first:
            doDisable(modules, false);
            Iterator it = modules.iterator();
            while (it.hasNext()) {
                Module m = (Module)it.next();
                if (m.isFixed()) {
                    // Hmm, ignore.
                    continue;
                }
                mgr.delete(m);
            }
        }
        
        public int compare(Object o1, Object o2) {
            Module m1 = (Module)o1;
            Module m2 = (Module)o2;
            int i = m1.getDisplayName().compareTo(m2.getDisplayName());
            if (i != 0) {
                return i;
            } else {
                return m1.getCodeNameBase().compareTo(m2.getCodeNameBase());
            }
        }

        private void doDisable(Set modules, boolean cancelable) {
            if (modules.isEmpty()) return;
            err.log("doDisable: " + modules);
            SortedSet realModules = new TreeSet(this); // SortedSet<Module>
            realModules.addAll(modules);
            Iterator it = realModules.iterator();
            while (it.hasNext()) {
                Module m = (Module)it.next();
                if (!m.isEnabled() || m.isAutoload() || m.isEager() || m.isFixed()) {
                    // In here by mistake, ignore.
                    it.remove();
                }
            }
            List toDisable = mgr.simulateDisable(realModules);
            // Check if there are any non-autoloads/eagers added.
            it = toDisable.iterator();
            SortedSet others = new TreeSet(this); // SortedSet<Module>
            while (it.hasNext()) {
                Module m = (Module)it.next();
                if (!m.isAutoload() && !m.isEager() && !realModules.contains(m)) {
                    others.add(m);
                }
            }
            if (! others.isEmpty()) {
                Component c = new ModuleEnableDisablePanel(false, realModules, others);
                // component's accessibility will be used for Dialog accessibility
                c.getAccessibleContext().setAccessibleName(NbBundle.getMessage(ModuleBean.class, "ACSN_TITLE_disabling"));
                c.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(ModuleBean.class, "ACSD_TITLE_disabling"));
                NotifyDescriptor d = new NotifyDescriptor.Confirmation(c,
                    NbBundle.getMessage(ModuleBean.class, "MB_TITLE_disabling"),
                    cancelable ? NotifyDescriptor.YES_NO_OPTION : NotifyDescriptor.DEFAULT_OPTION);
                if (org.openide.DialogDisplayer.getDefault().notify(d) != NotifyDescriptor.YES_OPTION) {
                    // User refused.
                    // Fire changes again since modules are now wrong & need recalc.
                    ModuleBean[] _modules = this.modules;
                    if (_modules != null) {
                        for (int i = 0; i < _modules.length; i++) {
                            if (realModules.contains(_modules[i].module)) {
                                _modules[i].propertyChange(null);
                            }
                        }
                    }
                    return;
                }
                realModules.addAll(others);
            }
            // Ready to go.
            mgr.disable(realModules);
        }
        
        private void doEnable(Set modules) {
            if (modules.isEmpty()) return;
            err.log("doEnable: " + modules);
            SortedSet realModules = new TreeSet(this); // SortedSet<Module>
            realModules.addAll(modules);
            Iterator it = realModules.iterator();
            while (it.hasNext()) {
                Module m = (Module)it.next();
                if (m.isEnabled() || m.isAutoload() || m.isEager() || m.isFixed() || ! m.getProblems().isEmpty()) {
                    // In here by mistake, ignore.
                    it.remove();
                }
            }
            List toEnable = mgr.simulateEnable(realModules);
            // Check if there are any non-autoloads/eagers added.
            it = toEnable.iterator();
            SortedSet others = new TreeSet(this); // SortedSet<Module>
            while (it.hasNext()) {
                Module m = (Module)it.next();
                if (!m.isAutoload() && !m.isEager() && !realModules.contains(m)) {
                    others.add(m);
                }
            }
            if (! others.isEmpty()) {
                Component c = new ModuleEnableDisablePanel(true, realModules, others);
                // component's accessibility will be used for Dialog accessibility
                c.getAccessibleContext().setAccessibleName(NbBundle.getMessage(ModuleBean.class, "ACSN_TITLE_disabling"));
                c.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(ModuleBean.class, "ACSD_TITLE_disabling"));
                NotifyDescriptor d = new NotifyDescriptor.Confirmation(c,
                    NbBundle.getMessage(ModuleBean.class, "MB_TITLE_enabling"),
                    NotifyDescriptor.YES_NO_OPTION);
                if (org.openide.DialogDisplayer.getDefault().notify(d) != NotifyDescriptor.YES_OPTION) {
                    // User refused.
                    // Fire changes again since modules are now wrong & need recalc.
                    ModuleBean[] _modules = this.modules;
                    if (_modules != null) {
                        for (int i = 0; i < _modules.length; i++) {
                            if (realModules.contains(_modules[i].module)) {
                                _modules[i].propertyChange(null);
                            }
                        }
                    }
                    return;
                }
                realModules.addAll(others);
            }
            // Ready to go. First reload any test modules.
            it = toEnable.iterator();
            while (it.hasNext()) {
                Module m = (Module)it.next();
                if (m.isReloadable()) {
                    try {
                        mgr.reload(m);
                    } catch (IOException ioe) {
                        ErrorManager.getDefault().notify(ioe);
                        // Refresh all.
                        ModuleBean[] _modules = this.modules;
                        if (_modules != null) {
                            for (int i = 0; i < _modules.length; i++) {
                                if (realModules.contains(_modules[i].module)) {
                                    _modules[i].propertyChange(null);
                                }
                            }
                        }
                        return;
                    }
                }
            }
            try {
                mgr.enable(realModules);
            } catch (InvalidException ie) {
                err.notify(ErrorManager.INFORMATIONAL, ie);
                Module bad = ie.getModule();
                if (bad == null) throw new IllegalStateException();
                ev.log(Events.FAILED_INSTALL_NEW_UNEXPECTED, bad, ie);
                // Refresh it.
                ModuleBean[] _modules = this.modules;
                if (_modules != null) {
                    for (int i = 0; i < _modules.length; i++) {
                        if (_modules[i].module == bad) {
                            _modules[i].propertyChange(null);
                        }
                    }
                }
                // Try to enable a subset this time.
                realModules.remove(ie.getModule());
                doEnable(realModules);
            }
        }
        
        private void doCreate(Set files, boolean reloadable) {
            if (files.isEmpty()) return;
            err.log("doCreate: " + files + " reloadable=" + reloadable);
            Iterator it = files.iterator();
            while (it.hasNext()) {
                File jar = (File)it.next();
                Module nue;
                try {
                    nue = mgr.create(jar, new ModuleHistory(jar.getAbsolutePath()), reloadable, false, false);
                } catch (IOException ioe) {
                    ErrorManager.getDefault().notify(ErrorManager.USER, ioe);
                    continue;
                } catch (DuplicateException dupe) {
                    // Replace the old one.
                    Module old = dupe.getOldModule();
                    if (old.isFixed()) {
                        // Cannot replace it.
                        continue;
                    }
                    if (old.isEnabled()) {
                        // Need to turn it off first.
                        if (old.isAutoload() || old.isEager()) {
                            // Hmm, too complicated to deal with now. Skip it.
                            continue;
                        }
                        doDisable(Collections.singleton(old), true);
                        if (old.isEnabled()) {
                            // Did not work for some reason, skip it.
                            continue;
                        }
                    }
                    mgr.delete(old);
                    try {
                        nue = mgr.create(jar, new ModuleHistory(jar.getAbsolutePath()), reloadable, false, false);
                    } catch (IOException ioe) {
                        ErrorManager.getDefault().notify(ErrorManager.USER, ioe);
                        continue;
                    } catch (DuplicateException dupe2) {
                        // Huh??
                        ErrorManager.getDefault().notify(dupe2);
                        continue;
                    }
                }
                // Now turn it on.
                if (nue.getProblems().isEmpty()) {
                    doEnable(Collections.singleton(nue));
                    // If it did not work, just leave it there disabled, user
                    // can delete if desired.
                } else {
                    // Cannot install, make sure user knows about it!
                    ev.log(Events.FAILED_INSTALL_NEW, Collections.singleton(nue));
                }
            }
        }
        
        // Inter-module dependencies; see #22504.
        // Cumbersome to do with the property + change model used for simple
        // properties of modules, because these properties may be expensive
        // to compute and might never be needed. It is also harder to know for
        // sure when they might have changed.

        /**
         * Modules directly needed by this module.
         */
        public static final int RELATION_DIRECTLY_NEEDS = 0;
        /**
         * All modules needed by this module.
         */
        public static final int RELATION_TRANSITIVELY_NEEDS = 1;
        /**
         * Modules which directly need this module.
         */
        public static final int RELATION_DIRECTLY_NEEDED_BY = 2;
        /**
         * All modules which need this module.
         */
        public static final int RELATION_TRANSITIVELY_NEEDED_BY = 3;
        
        /** A callback used to supply the result of a relationship computation
         * asynchronously.
         */
        public interface RelationCallback {
            /** Called when a computation is done.
             * @param modules a set of {@link ModuleBean} instances
             */
            void result(Set modules);
        }
        
        private static final RequestProcessor RELATION_COMPUTER_RP = new RequestProcessor("RelationComputer"); // NOI18N
        /**
         * Compute the relations of other modules to this one.
         * <p>Provide-require dependencies are treated just like direct dependencies,
         * where there is in fact a provider for a requirement. Note that this could
         * lead to slightly misleading results in some cases: for example, consider
         * if A (enabled) requires X, and B (enabled) and C (enabled) both provide X.
         * A's reported dependency list will include both B and C, despite the fact
         * that it is possible to disable either one by itself without disabling A.
         * <p>This computation is done asynchronously. Call this method
         * from the event thread; the callback will be called later
         * from the event thread.
         * @param mb the module to start with
         * @param type one of the RELATION_* constants
         * @param callback will be called when the computation is done
         */
        public void getRelations(ModuleBean mb, int type, RelationCallback callback) {
            if (type != RELATION_DIRECTLY_NEEDS && type != RELATION_TRANSITIVELY_NEEDS &&
                    type != RELATION_DIRECTLY_NEEDED_BY && type != RELATION_TRANSITIVELY_NEEDED_BY) {
                throw new IllegalArgumentException("bad type: " + type); // NOI18N
            }
            RELATION_COMPUTER_RP.post(new RelationComputer(mb.module, type, callback));
        }
        
        private class RelationComputer implements Runnable {
            private int stage;
            private final Module m;
            private final int type;
            private final RelationCallback callback;
            private Set result; // Set<Module>
            public RelationComputer(Module m, int type, RelationCallback callback) {
                this.stage = 0;
                this.m = m;
                this.type = type;
                this.callback = callback;
            }
            public void run() {
                switch (stage) {
                    case 0:
                        stage = 1;
                        mgr.mutex().readAccess(this);
                        break;
                    case 1:
                        compute();
                        stage = 2;
                        SwingUtilities.invokeLater(this);
                        break;
                    default:
                        // Convert Module -> ModuleBean and return it.
                        Set mbresult = new HashSet(result.size() * 2 + 1); // Set<ModuleBean>
                        ModuleBean[] _modules = getModules();
                        for (int i = 0; i < _modules.length; i++) {
                            if (result.contains(_modules[i].module)) {
                                mbresult.add(_modules[i]);
                            }
                        }
                        callback.result(mbresult);
                        break;
                }
            }
            /** Called from within module system read mutex.
             * Should do the calculations and store them.
             */
            private void compute() {
                switch (type) {
                case RELATION_DIRECTLY_NEEDS:
                    result = mgr.getModuleInterdependencies(m, false, false);
                    break;
                case RELATION_DIRECTLY_NEEDED_BY:
                    result = mgr.getModuleInterdependencies(m, true, false);
                    break;
                case RELATION_TRANSITIVELY_NEEDS:
                    result = mgr.getModuleInterdependencies(m, false, true);
                    break;
                case RELATION_TRANSITIVELY_NEEDED_BY:
                    result = mgr.getModuleInterdependencies(m, true, true);
                    break;
                default:
                    assert false : type;
                }
            }
        }
        
    }

    /** One update to run. */
    private static final class Update {
        public final String command;
        public final Object arg;
        public Update(String command, Object arg) {
            this.command = command;
            this.arg = arg;
        }
        public boolean equals(Object o) {
            if (! (o instanceof Update)) return false;
            Update u = (Update)o;
            return command.equals(u.command) && arg.equals(u.arg);
        }
        public int hashCode() {
            return command.hashCode() ^ arg.hashCode();
        }
        public String toString() {
            return "Update[" + command + "," + arg + "]"; // NOI18N
        }
    }
    
}
