/*
 * 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.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorSupport;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.KeyStroke;
import javax.swing.AbstractAction;
import org.netbeans.core.IDESettings;
import org.netbeans.modules.autoupdate.AutoModuleNewType;
import org.openide.ErrorManager;
import org.openide.actions.NewAction;
import org.openide.actions.PropertiesAction;
import org.openide.actions.ToolsAction;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.PropertySupport;
import org.openide.nodes.Sheet;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.SharedClassObject;
import org.openide.util.WeakListeners;
import org.openide.util.actions.SystemAction;
import org.openide.util.datatransfer.NewType;
import org.openide.util.lookup.Lookups;
import org.openide.windows.WindowManager;

/** Node representing modules.
 * @author Jesse Glick, Jaroslav Tulach, Rostislav Levy (?), et al.
 */
public class ModuleNode extends AbstractNode {
    
    private static final ResourceBundle bundle = NbBundle.getBundle( ModuleNode.class );

    /** Icon bases */
    private static final String MODULE_ITEM_ICON_BASE = "org/netbeans/modules/autoupdate/resources/moduleItem.gif"; // NOI18N
    private static final String MODULE_ITEM_DISABLED_BASE = "org/netbeans/modules/autoupdate/resources/moduleItemDisabled.gif"; // NOI18N
    private static final String MODULE_ITEM_ERROR_BASE = "org/netbeans/modules/autoupdate/resources/moduleItemError.gif"; // NOI18N
    private static final String MODULE_TEST_ITEM_ICON_BASE = "org/netbeans/modules/autoupdate/resources/testModuleItem.gif"; // NOI18N
    private static final String MODULES_ICON_BASE = "org/netbeans/modules/autoupdate/resources/modules.gif"; // NOI18N

    /** Last directory used by the install new module file chooser. */
    private static File lastChosenDir = null;
    private static ModuleDeleter deleter;

    /** Creates a new ModulesNode */
    public ModuleNode() {
        this(false);
    }
    
    private boolean simple = false;
    public ModuleNode(boolean simple) {
        // #14553: try to keep code name consistent for node path compatibility
        super(new Modules (simple));
        setName("Modules"); // NOI18N
        setDisplayName(bundle.getString("CTL_Modules_name"));
        setShortDescription (bundle.getString ("CTL_Modules_hint"));
        setIconBaseWithExtension(MODULES_ICON_BASE);
        this.simple = simple;
    }

    public HelpCtx getHelpCtx () {
        return new HelpCtx (ModuleNode.class);
    }

    public Node cloneNode () {
        return new ModuleNode ();
    }

    public Action[] getActions(boolean context) {
        return new Action[] {
           SystemAction.get(NewAction.class),
           null,
           SystemAction.get (ModuleNodeActions.SortAction.class),
           null,
           SystemAction.get(ToolsAction.class),
           SystemAction.get(PropertiesAction.class),
       };
    }

    /** Add a sorting property. */
    protected Sheet createSheet () {
        Sheet.Set set = new Sheet.Set ();
        set.setName ("sorting"); // NOI18N
        set.setDisplayName (bundle.getString ("LBL_ModuleNode_sheet_sorting"));
        set.put (((Modules) getChildren ()).createSortingProperty ());
        Sheet sheet = new Sheet ();
        sheet.put (set);
        return sheet;
    }

    public NewType[] getNewTypes() {
        return new NewType[] {
            new NewType() {
                public String getName() {
                    return bundle.getString("CTL_NewModuleByFile");
                }
                public void create() throws IOException {
                    addFile(false);
                }
            },
            new NewType() {
                public String getName() {
                    return bundle.getString("CTL_NewTestModule");
                }
                public void create() throws IOException {
                    addFile(true);
                }
            },
            new AutoModuleNewType(),
        };
    }

    /** Allows to add new module by specifying its file.
    */
    void addFile (boolean reloadable) /*throws IOException*/ {
        // XXX should use File property editor, not JFileChooser directly
        final JFileChooser chooser = new JFileChooser ();
        chooser.setFileHidingEnabled(false);
        
        // XXX #18270. Enter doesn't work when expecting folder change,
        // Accessibility problem. We hack default behaviour here.
        
        // Only jdk1.3 there is not the action in action map, i.e. also no 
        // key binding. When running on jdk1.4 only remove this part
        // dealing with setting the key binding.
        InputMap im = chooser.getInputMap(
            JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        if(im != null) {
            KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);

            Object value = im.get(enter);
            if(value == null) {
                im.put(enter, "approveSelection"); // NOI18N
            }
        }

        // Add(jdk1.3) or replace(jdk1.4) to parent (UI) map the new action
        // doing the folder change.
        ActionMap map = chooser.getActionMap();
        if(map != null) {
            // Get parent map, which is map set by FileChooserUI,
            // containing the default approveSelection action.
            ActionMap parent = map.getParent();

            if(parent != null) {

                // Get original action from parent map, set by UI.
                final Action original = parent.get("approveSelection"); // NOI18N

                // Replace it by our action which adds the folder change,
                // if selected is a directory.
                parent.put("approveSelection", new AbstractAction() { // NOI18N
                    public void actionPerformed(ActionEvent evt) {
                        File file = chooser.getSelectedFile();

                        if(file != null && file.isDirectory()) {
                            try {
                                // Strip trailing ".."
                                file = file.getCanonicalFile();
                            } catch (java.io.IOException ioe) {
                                // Ok, use f as is
                            }

                            chooser.setCurrentDirectory(file);
                        } else {
                            if(original != null) {
                                original.actionPerformed(evt);
                            }
                        }
                    }
                });
            }
        } // End of hacking.
        
        if (lastChosenDir != null) chooser.setCurrentDirectory (lastChosenDir);
        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setApproveButtonText (bundle.getString ("CTL_ModuleSelect"));
        chooser.setApproveButtonToolTipText (bundle.getString ("CTL_ModuleSelectToolTip"));
        chooser.setFileFilter (new javax.swing.filechooser.FileFilter () {
            public String getDescription () {
                return bundle.getString ("CTL_ModuleSelectFilter");
            }
            public boolean accept (File f) {
                return f.isDirectory () || f.getName ().endsWith (".jar"); // NOI18N
            }
        });

        int result = chooser.showOpenDialog (WindowManager.getDefault ().getMainWindow ());
        lastChosenDir = chooser.getCurrentDirectory ();
        if (result == JFileChooser.APPROVE_OPTION) {
            // install the file
            File jar = chooser.getSelectedFile();
            getAllModules().create(jar, reloadable);
        }

    }

    /** Class representing node of one standard module
     */
    static class Item extends AbstractNode implements PropertyChangeListener {

        private ModuleBean item;
        /** true if usedBy prop has been displayed, which can affect all else */
        private boolean listeningToAllModules = false;
        private boolean simple = false;

        public Item (ModuleBean item) {
            super(Children.LEAF, Lookups.singleton(item));
            this.item = item;

            setName (item.getCodeNameBase());
            item.addPropertyChangeListener(WeakListeners.propertyChange(this, item));
            updateDisplayStuff();
        }
        
        public Item (ModuleBean item, boolean simple) {
            this(item);
            this.simple = simple;
        }
        
        ModuleBean getItem () {
            assert item != null : "Cannot be called untill item is null.";
            return item;
        }
        
        void uninstall () {
            ModuleDeleter deleter = getModuleDeleter ();
            assert deleter != null : "ModuleDeleter must be available.";
            boolean canUninstall = deleter.canDelete (item.getModule ());
            if (canUninstall) {
                try {
                    deleter.delete (item.getModule ());
                } catch (IOException ioe) {
                    ErrorManager.getDefault ().notify (ErrorManager.USER, ioe);                    
                }
            } else if (canDestroy ()) {
                item.delete();
            }
        }
        
        private void updateDisplayStuff() {
            setDisplayName (item.getDisplayName());
            setShortDescription(item.getShortDescription());
            setIconBaseWithExtension(item.isEnabled() ?
                (item.isReloadable() ? MODULE_TEST_ITEM_ICON_BASE : MODULE_ITEM_ICON_BASE) :
                (item.isProblematic() ? MODULE_ITEM_ERROR_BASE : MODULE_ITEM_DISABLED_BASE));
        }

        public void propertyChange(PropertyChangeEvent evt) {
            // Some aspects of the module may have changed. Redisplay everything.
            updateDisplayStuff();
            firePropertyChange(null, null, null);
        }
        
        public HelpCtx getHelpCtx () {
            return new HelpCtx (Item.class);
        }

        public Action[] getActions(boolean context) {
            
            return new Action[] {
                SystemAction.get (ModuleNodeActions.EnableDisableAction.class),
                SystemAction.get (ModuleNodeActions.EnableAllAction.class),
                null,
                SystemAction.get (ModuleNodeActions.UninstallAction.class),
                //SystemAction.get (DeleteAction.class),
                // XXX reload action too...
                null,
                SystemAction.get (ModuleNodeActions.SortAction.class),
                null,
                //SystemAction.get (ToolsAction.class),
                SystemAction.get (PropertiesAction.class),
            };
        }
        
        public Action getPreferredAction() {
            return SystemAction.get(PropertiesAction.class);
        }

        public void destroy () {
            if (ModuleNodeUtils.confirmUninstall (new Node [] { this })) {
                uninstall ();
            }
        }

        public boolean canDestroy () {
            boolean canUninstall = getModuleDeleter ().canDelete (item.getModule ());
            if (! canUninstall) {
                // #65568: allow delete an standalone module
                return item.getJar () != null;
            }
            return true;
        }

        /** Creates properties.
        */
        protected Sheet createSheet () {
            Sheet s = Sheet.createDefault ();
            Sheet.Set ss = s.get (Sheet.PROPERTIES);

            Sheet.Set sse = Sheet.createExpertSet ();
            s.put(sse);
            
            try {
                Node.Property p;
                if (!simple) {
                    p = new PropertySupport.Reflection(item, String.class, "getDisplayName", null); // NOI18N
                    p.setName("displayName"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_name"));
                    p.setShortDescription(bundle.getString ("HINT_modules_name"));
                    ss.put(p);
                }
                p = new PropertySupport.Reflection(item, String.class, "getSpecificationVersion", null); // NOI18N
                p.setValue("suppressCustomEditor",Boolean.TRUE);
                p.setName("specificationVersion"); // NOI18N
                p.setDisplayName(bundle.getString ("PROP_modules_specversion"));
                p.setShortDescription(bundle.getString ("HINT_modules_specversion"));
                ss.put(p);
                if (!simple) {
                    p = new PropertySupport.Reflection(item, String.class, "getImplementationVersion", null); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setName("implementationVersion"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_implversion"));
                    p.setShortDescription(bundle.getString ("HINT_modules_implversion"));
                    sse.put(p);
                    p = new PropertySupport.Reflection(item, String.class, "getBuildVersion", null); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setName("buildVersion"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_buildversion"));
                    p.setShortDescription(bundle.getString ("HINT_modules_buildversion"));
                    sse.put(p);
                    p = new PropertySupport.Reflection(item, String.class, "getShortDescription", null); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setName("shortDescription"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_shortDescription"));
                    p.setShortDescription(bundle.getString ("HINT_modules_shortDescription"));
                    ss.put(p);
                    p = new PropertySupport.Reflection(item, String.class, "getLongDescription", null); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setName("longDescription"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_longDescription"));
                    p.setShortDescription(bundle.getString ("HINT_modules_longDescription"));
                    ss.put(p);
                    p = new PropertySupport.Reflection(item, String.class, "getCategory", null); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setName("category"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_category"));
                    p.setShortDescription(bundle.getString ("HINT_modules_category"));
                    sse.put(p);
                }
                class EnabledOrReloadableProp extends PropertySupport.Reflection {
                    public EnabledOrReloadableProp(String getter, String setter) throws NoSuchMethodException {
                        super(item, Boolean.TYPE, getter, setter);
                    }
                    public boolean canWrite() {
                        if (! super.canWrite()) return false; // not really necessary
                        if (this.getName().equals("enabled")) { // NOI18N
                            return /*! item.isFixed()*/ item.getJar() != null && !item.isProblematic() && !item.isAutoload() && !item.isEager();
                        } else if (this.getName().equals("reloadable")) { // NOI18N
                            // Need to be able to write to the containing directory to use this feature.
                            return item.getJar() != null &&
                            (item.isReloadable() || item.getJar().getParentFile().canWrite());
                        } else {
                            throw new IllegalStateException();
                        }
                    }
                }
                p = new EnabledOrReloadableProp("isEnabled", "setEnabled"); // NOI18N
                p.setName("enabled"); // NOI18N
                p.setDisplayName(bundle.getString("PROP_modules_enabled"));
                p.setShortDescription(bundle.getString("HINT_modules_enabled"));
                ss.put(p);
                if (!simple) {
                    p = new EnabledOrReloadableProp("isReloadable", "setReloadable"); // NOI18N
                    p.setName("reloadable"); // NOI18N
                    p.setDisplayName(bundle.getString("PROP_modules_reloadable"));
                    p.setShortDescription(bundle.getString("HINT_modules_reloadable"));
                    sse.put(p);
                    p = new PropertySupport.Reflection(item, String.class, "getCodeName", null); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setName("codeName"); // NOI18N
                    p.setDisplayName(bundle.getString ("PROP_modules_codename"));
                    p.setShortDescription(bundle.getString ("HINT_modules_codename"));
                    sse.put(p);
                    if (item.getJar() != null) {
                        p = new PropertySupport.Reflection(item, File.class, "getJar", null); // NOI18N
                        p.setName("jar"); // NOI18N
                        p.setDisplayName(bundle.getString ("PROP_modules_jar"));
                        p.setShortDescription(bundle.getString ("HINT_modules_jar"));
                        sse.put(p);
                    }
                    class ClasspathProperty extends PropertySupport.ReadOnly {
                        // See #22466
                        public ClasspathProperty() {
                            super("classpath", getNbClassPathOrStringClass(), // NOI18N
                                  bundle.getString("PROP_modules_classpath"),
                                  bundle.getString("HINT_modules_classpath"));
                        }
                        public Object getValue() throws InvocationTargetException {
                            String cp = item.getEffectiveClasspath();
                            if (getValueType() == String.class) {
                                return cp;
                            } else {
                                try {
                                    Constructor c = getValueType().getConstructor(new Class[] {String.class});
                                    return c.newInstance(new Object[] {cp});
                                } catch (Exception e) {
                                    throw new InvocationTargetException(e);
                                }
                            }
                        }
                    }
                    sse.put(new ClasspathProperty());
                    // Though normally in a separate category, still potentially
                    // useful to leave here (e.g. if sorting differently):
                    p = new PropertySupport.Reflection(item, Boolean.TYPE, "isAutoload", null); // NOI18N
                    p.setName("autoload"); // NOI18N
                    p.setDisplayName(bundle.getString("PROP_modules_autoload"));
                    p.setShortDescription(bundle.getString("HINT_modules_autoload"));
                    sse.put(p);
                    p = new PropertySupport.Reflection(item, Boolean.TYPE, "isEager", null); // NOI18N
                    p.setName("eager"); // NOI18N
                    p.setDisplayName(bundle.getString("PROP_modules_eager"));
                    p.setShortDescription(bundle.getString("HINT_modules_eager"));
                    sse.put(p);
                    p = new PropertySupport.Reflection(item, String[].class, "getProvides", null); // NOI18N
                    p.setName("provides"); // NOI18N
                    p.setValue("suppressCustomEditor",Boolean.TRUE);
                    p.setDisplayName(bundle.getString("PROP_modules_provides"));
                    p.setShortDescription(bundle.getString("HINT_modules_provides"));
                    sse.put(p);
                    // #16636:
                    p = new PropertySupport.Reflection(item, String[].class, "getProblemDescriptions", null); // NOI18N
                    p.setName("problemDescriptions"); // NOI18N
                    p.setDisplayName(bundle.getString("PROP_modules_problemDescriptions"));
                    p.setShortDescription(bundle.getString("HINT_modules_problemDescriptions"));
                    sse.put(p);
                }
            } catch (NoSuchMethodException nsme) {
                ErrorManager.getDefault().notify(nsme);
            }
            sse.put(new UsedByProp());

            return s;
        }

        private static Class getNbClassPathOrStringClass() {
            ClassLoader l = (ClassLoader)Lookup.getDefault().lookup(ClassLoader.class);
            try {
                return l.loadClass("org.openide.execution.NbClassPath"); // NOI18N
            } catch (ClassNotFoundException cnfe) {
                return String.class;
            }
        }

        /** For enabled modules, shows other modules which are currently
         * using it, i.e. depending on it to stay enabled.
         * @see #22504
         */
        private final class UsedByProp extends PropertySupport.ReadOnly implements ModuleBean.AllModulesBean.RelationCallback, PropertyChangeListener {
            private String[] value = null;
            public UsedByProp() {
                super("usedBy", String[].class, // NOI18N
                      bundle.getString("PROP_modules_usedby"),
                      bundle.getString("HINT_modules_usedby"));
            }
            public Object getValue() {
                if (value != null) {
                    return value;
                } else {
                    ModuleBean.AllModulesBean amb = ModuleBean.AllModulesBean.getDefault();
                    amb.getRelations(item, ModuleBean.AllModulesBean.RELATION_TRANSITIVELY_NEEDED_BY, this);
                    // All sorts of changes can now affect this!
                    if (!listeningToAllModules) {
                        listeningToAllModules = true;
                        amb.addPropertyChangeListener(WeakListeners.propertyChange(this, amb));
                        ModuleBean[] mods = amb.getModules();
                        for (int i = 0; i < mods.length; i++) {
                            if (mods[i] != item) {
                                mods[i].addPropertyChangeListener(WeakListeners.propertyChange(this, mods[i]));
                            }
                        }
                    }
                    return new String[] {bundle.getString("LBL_please_wait_modules_usedby")};
                }
            }
            public void result(Set modules) {
                List l = new ArrayList(Math.max(modules.size(), 1)); // List<String>
                // Only display enabled users, and only display anything if this is enabled:
                if (item.isEnabled()) {
                    Iterator it = modules.iterator();
                    while (it.hasNext()) {
                        ModuleBean mb = (ModuleBean)it.next();
                        if (mb.isEnabled()) {
                            l.add(mb.getDisplayName());
                        }
                    }
                    Collections.sort(l, Collator.getInstance());
                }
                String[] old = value;
                value = (String[])l.toArray(new String[l.size()]);
                Item.this.firePropertyChange("usedBy", old, value); // NOI18N
            }
            public void propertyChange(PropertyChangeEvent ev) {
                value = null;
                Item.this.firePropertyChange("usedBy", null, null); // NOI18N
            }
        }
            
    }
    
    /** Children that contains modules installed it has to
     * dissingushg between modules and test modules.
     * Keys are of type ModuleBean, LIBRARY_KEY, BRIDGE_KEY, or String category name.
     */
    private static class Modules extends Children.Keys implements PropertyChangeListener {
        
        /** Key for fixed/autoload modules, rather than a category per se. */
        private static final Object LIBRARY_KEY = new Object();
        /** Key for eager modules. */
        private static final Object BRIDGE_KEY = new Object();

        private IDESettings settings;
        private boolean simple = false;
        
        public Modules() {}
        public Modules(boolean simple) {
            this.simple = simple;
        }

        /** Refreshed list of nodes acc. to current sorting and contents. */
        private void refreshKeys() {
            if (settings == null) {
                addNotify();
            }
            ModuleBean[] items = getAllModules().getModules();
            if (settings.getModulesSortMode() == IDESettings.MODULES_SORT_CATEGORY) {
                final Comparator collator = Collator.getInstance();
                class CategoryComparator implements Comparator {
                    public int compare(Object o1, Object o2) {
                        // Libraries always put in the last place.
                        if (o1 == LIBRARY_KEY) {
                            if (o2 == LIBRARY_KEY) {
                                return 0;
                            } else {
                                return 1;
                            }
                        } else {
                            if (o2 == LIBRARY_KEY) {
                                return -1;
                            } else {
                                // Eager modules come between categories and libraries.
                                if (o1 == BRIDGE_KEY) {
                                    if (o2 == BRIDGE_KEY) {
                                        return 0;
                                    } else {
                                        return 1;
                                    }
                                } else if (o2 == BRIDGE_KEY) {
                                    return -1;
                                }
                                // #13078: sort modules and categories inline.
                                String name1;
                                if (o1 instanceof ModuleBean) {
                                    name1 = ((ModuleBean)o1).getDisplayName();
                                } else {
                                    name1 = (String)o1;
                                }
                                String name2;
                                if (o2 instanceof ModuleBean) {
                                    name2 = ((ModuleBean)o2).getDisplayName();
                                } else {
                                    name2 = (String)o2;
                                }
                                return collator.compare(name1, name2);
                            }
                        }
                    }
                }
                SortedSet categories = new TreeSet(new CategoryComparator());
                for (int i = 0; i < items.length; i++) {
                    if (items[i].getJar() == null || items[i].isAutoload()) {
                        categories.add(LIBRARY_KEY);
                    } else if (items[i].isEager()) {
                        categories.add(BRIDGE_KEY);
                    } else {
                        String category = items[i].getCategory();
                        if (category != null) {
                            categories.add(category);
                        } else {
                            categories.add(items[i]);
                        }
                    }
                }
                setKeys(categories);
            } else if (settings.getModulesSortMode() != IDESettings.MODULES_SORT_UNSORTED) {
                if (settings.getModulesSortMode() == IDESettings.MODULES_SORT_ENABLED) {
                    // Listen for changes in enabled status and resort.
                    for (int i = 0; i < items.length; i++) {
                        items[i].addPropertyChangeListener(this);
                    }
                }
                List itemsL = new ArrayList(Arrays.asList(items));
                Collections.sort(itemsL, new Comparator() {
                    public int compare(Object o1, Object o2) {
                        ModuleBean m1 = (ModuleBean)o1;
                        ModuleBean m2 = (ModuleBean)o2;
                        switch (settings.getModulesSortMode()) {
                            case IDESettings.MODULES_SORT_CODENAME:
                                return m1.getCodeName().compareTo(m2.getCodeName());
                            case IDESettings.MODULES_SORT_ENABLED:
                                // Order:
                                // 0. Reloadable enabled regular modules.
                                // 1. Reloadable disabled regular modules.
                                // 2. Reloadable enabled fixed/autoload/eager modules.
                                // 3. Reloadable disabled fixed/autoload/eager modules.
                                // 4. Enabled regular modules.
                                // 5. Disabled regular modules.
                                // 6. Enabled fixed/autoload/eager modules.
                                // 7. Disabled fixed/autoload/eager modules.
                                int k1 = (m1.isReloadable() ? 0 : 4) +
                                         ((m1.isAutoload() || m1.isEager() || m1.getJar() == null) ? 2 : 0) +
                                         (m1.isEnabled() ? 0 : 1);
                                int k2 = (m2.isReloadable() ? 0 : 4) +
                                         ((m2.isAutoload() || m2.isEager() || m2.getJar() == null) ? 2 : 0) +
                                         (m2.isEnabled() ? 0 : 1);
                                if (k1 != k2) return k1 - k2;
                                // fallthrough
                            case IDESettings.MODULES_SORT_DISPLAYNAME:
                                return Collator.getInstance().compare(m1.getDisplayName(), m2.getDisplayName());
                            case IDESettings.MODULES_SORT_URL:
                                File j1 = m1.getJar();
                                File j2 = m2.getJar();
                                if (j1 != null) {
                                    if (j2 != null) {
                                        return j1.compareTo(j2);
                                    } else {
                                        return 1;
                                    }
                                } else {
                                    if (j2 != null) {
                                        return -1;
                                    } else {
                                        return 0;
                                    }
                                }
                            default:
                                return 0;
                        }
                    }
                });
                setKeys(itemsL);
            } else {
                setKeys(items);
            }
        }

        Node.Property createSortingProperty () {
            settings = (IDESettings) SharedClassObject.findObject (IDESettings.class, true);
            return new PropertySupport.ReadWrite ("sorted", Integer.TYPE, // NOI18N
                                                  bundle.getString ("PROP_ModuleNode_sorted"),
                                                  bundle.getString ("HINT_ModuleNode_sorted")) {
                public Object getValue () {
                    return new Integer (settings.getModulesSortMode ());
                }
                public void setValue (Object o) {
                    settings.setModulesSortMode (((Integer) o).intValue ());
                }
                public boolean supportsDefaultValue () {
                    return true;
                }
                public void restoreDefaultValue () {
                    setValue (new Integer (IDESettings.MODULES_SORT_CATEGORY));
                }
                public PropertyEditor getPropertyEditor () {
                    return new SortingModeEditor();
                }
            };
        }
        
        private static final class SortingModeEditor extends PropertyEditorSupport {
            private final String[] tags = new String[] {
                bundle.getString ("LBL_ModuleNode_SORT_UNSORTED"),
                bundle.getString ("LBL_ModuleNode_SORT_DISPLAYNAME"),
                bundle.getString ("LBL_ModuleNode_SORT_CODENAME"),
                bundle.getString ("LBL_ModuleNode_SORT_ENABLED"),
                bundle.getString ("LBL_ModuleNode_SORT_URL"),
                bundle.getString ("LBL_ModuleNode_SORT_CATEGORY"),
            };
            public SortingModeEditor() {}
            public String[] getTags () {
                return tags;
            }
            public String getAsText () {
                return tags[((Integer) this.getValue ()).intValue ()];
            }
            public void setAsText (String text) {
                for (int i = 0; i < tags.length; i++) {
                    if (tags[i].equals (text)) {
                        this.setValue (new Integer (i));
                        return;
                    }
                }
                throw new IllegalArgumentException ();
            }
        }

        /** Initializes content */
        public void addNotify () {
            settings = (IDESettings) SharedClassObject.findObject (IDESettings.class, true);
            getAllModules().addPropertyChangeListener (this);
            settings.addPropertyChangeListener (this);
            refreshKeys ();
        }

        /** Releases listener. */
        public void removeNotify () {
            settings.removePropertyChangeListener (this);
            getAllModules().removePropertyChangeListener (this);
            ModuleBean[] items = getAllModules().getModules();
            for (int i = 0; i < items.length; i++) {
                items[i].removePropertyChangeListener(this);
            }
        }
        
        /** Make sure hierarchy lookups get the proper module list first. */
        public Node findChild(String name) {
            getAllModules().waitForModules().waitFinished();
            refreshKeys();
            return super.findChild(name);
        }

        /** Reacts to changes */
        public void propertyChange (PropertyChangeEvent ev) {
            // From IDESettings or AllModulesBean or ModuleBean:
            refreshKeys ();
        }

        /** Generates node for the ModuleBean key */
        protected Node[] createNodes (Object key) {
            if (key == LIBRARY_KEY) {
                return new Node[] {new CategoryNode(false)};
            } else if (key == BRIDGE_KEY) {
                return new Node[] {new CategoryNode(true)};
            } else if (key instanceof String) {
                return new Node[] { new CategoryNode ((String) key) };
            } else {
                return new Node[] { new Item ((ModuleBean)key, simple) };
            }
        }
    }

    private static final class CategoryNode extends AbstractNode {
        
        private final boolean libraries;
        private final boolean bridges;
        
        public CategoryNode(boolean bridgeNotLib) {
            this (bridgeNotLib, false);
        }
        
        private CategoryNode (boolean bridgeNotLib, boolean simple) {
            super(new CategoryChildren(bridgeNotLib, simple));
            setName(bridgeNotLib ? "bridges" : "libraries"); // NOI18N
            setDisplayName(bundle.getString(bridgeNotLib ? "LBL_ModuleNode_bridges" : "LBL_ModuleNode_libraries"));
            setIconBaseWithExtension(MODULES_ICON_BASE);
            libraries = !bridgeNotLib;
            bridges = bridgeNotLib;
        }

        public CategoryNode (String category) {
            this(category, false);
        }
        
        private CategoryNode (String category, boolean simple) {
            super (new CategoryChildren (category, simple));
            setName (category); // displayName should be the same
            setIconBaseWithExtension(MODULES_ICON_BASE);
            libraries = false;
            bridges = false;
        }

        public boolean canDestroy () {
            return ModuleNodeUtils.canUninstall (new Node [] { this });
        }
        
        public void destroy () {
            ModuleNodeUtils.doUninstall (new Node [] { this });
        }

        public boolean canRename () {
            return false;
        }

        public boolean canCut () {
            return false;
        }
        
        public HelpCtx getHelpCtx () {
            return new HelpCtx (CategoryNode.class);
        }

        public Action[] getActions(boolean context) {
            
            return new Action[] {
                SystemAction.get (ModuleNodeActions.EnableDisableAction.class),
                SystemAction.get (ModuleNodeActions.EnableAllAction.class),
                null,
                SystemAction.get (ModuleNodeActions.UninstallAction.class),
                //SystemAction.get (DeleteAction.class),
                // XXX reload action too...
                null,
                SystemAction.get (ModuleNodeActions.SortAction.class),
                null,
                //SystemAction.get (ToolsAction.class),
                SystemAction.get (PropertiesAction.class),
            };
        }
        
        /** Creates property.
        */
        protected Sheet createSheet () {
            Sheet s = Sheet.createDefault ();
            if (libraries || bridges) {
                // Do not want the enabled prop here.
                return s;
            }
            Sheet.Set ss = s.get (Sheet.PROPERTIES);
            
            class EnabledProp extends PropertySupport.ReadWrite implements PropertyChangeListener {
                /** modules in this category */
                private Set modules = new HashSet(); // Set<Modules>
                private final String category;
                public EnabledProp(String category) {
                    super("enabled", Boolean.TYPE, // NOI18N
                          bundle.getString ("PROP_modules_enabled"),
                          bundle.getString ("HINT_modules_enabled"));
                    this.category = category;
                    calculateModules();
                    getAllModules().addPropertyChangeListener(org.openide.util.WeakListeners.propertyChange(this, getAllModules()));
                }
                public Object getValue() {
                    Boolean val = null;
                    Iterator it = modules.iterator();
                    while (it.hasNext()) {
                        ModuleBean m = (ModuleBean)it.next();
                        if (m.isEnabled()) {
                            if (val == null) {
                                val = Boolean.TRUE;
                            } else if (! val.booleanValue()) {
                                // Mixed results.
                                return null;
                            }
                        } else {
                            if (val == null) {
                                val = Boolean.FALSE;
                            } else if (val.booleanValue()) {
                                // Mixed results.
                                return null;
                            }
                        }
                    }
                    return val;
                }
                public void setValue(Object val) {
                    boolean e = ((Boolean)val).booleanValue();
                    Iterator it = modules.iterator();
                    getAllModules ().pause ();
                    while (it.hasNext()) {
                        ModuleBean m = (ModuleBean)it.next();
                        m.setEnabled(e);
                    }
                    getAllModules ().resume ();
                }
                public boolean canWrite() {
                    Iterator it = modules.iterator();
                    while (it.hasNext()) {
                        ModuleBean m = (ModuleBean)it.next();
                        if (m.isAutoload() || m.getJar() == null || m.isEager()) throw new IllegalStateException();
                        if (m.isProblematic()) {
                            return false;
                        }
                    }
                    return true;
                }
                private void calculateModules() {
                    Set modules2 = new HashSet(); // Set<Module>
                    ModuleBean[] testing = getAllModules().getModules();
                    for (int i = 0; i < testing.length; i++) {
                        if (testing[i].getJar() != null && !testing[i].isAutoload() && !testing[i].isEager() &&
                                category.equals(testing[i].getCategory())) {
                            modules2.add(testing[i]);
                            if (! modules.contains(testing[i])) {
                                testing[i].addPropertyChangeListener(org.openide.util.WeakListeners.propertyChange(this, testing[i]));
                            }
                        }
                    }
                    modules = modules2;
                }
                public void propertyChange(PropertyChangeEvent evt) {
                    // Something changed--list of modules, enabled status of some, etc.
                    CategoryNode.this.firePropertyChange("enabled", null, null); // NOI18N
                    calculateModules();
                }
                private PropertyEditor editor=null;
                public PropertyEditor getPropertyEditor() {
                    //issue 38019, cache the property editor for TTV performance
                    if (editor == null) {
                        editor = super.getPropertyEditor();
                    }
                    return editor;
                }
            }
            
            ss.put(new EnabledProp(getName()));
            return s;
        }

        private static final class CategoryChildren extends Children.Keys implements PropertyChangeListener {

            private final String category;
            private final boolean bridgeNotLib;
            private boolean simple = false;
            /** Libraries pseudo-category. */
            public CategoryChildren(boolean bridgeNotLib) {
                this(bridgeNotLib, false);
            }

            public CategoryChildren (String category) {
                this(category, false);
            }
            
            public CategoryChildren(String category, boolean simple) {
                this.simple = simple;
                this.category = category;
                bridgeNotLib = false; // arbitrary
            }
            
            public CategoryChildren (boolean bridgeNotLib, boolean simple) {
                this.simple = simple;
                category = null;
                this.bridgeNotLib = bridgeNotLib;
            }

            protected void addNotify () {
                refreshKeys ();
                getAllModules().addPropertyChangeListener (this);
            }

            protected void removeNotify () {
                getAllModules().removePropertyChangeListener (this);
                setKeys (Collections.EMPTY_SET);
            }

            /** Make sure hierarchy lookups get the proper module list first. */
            public Node findChild(String name) {
                getAllModules().waitForModules().waitFinished();
                refreshKeys();
                return super.findChild(name);
            }

            private void refreshKeys () {
                List keys = new ArrayList ();
                ModuleBean[] modules = getAllModules().getModules();
                for (int i = 0; i < modules.length; i++) {
                    boolean lib = modules[i].getJar() == null || modules[i].isAutoload();
                    boolean bridge = modules[i].isEager();
                    if (category != null && !lib && !bridge && category.equals (modules[i].getCategory())) {
                        keys.add (modules[i]);
                    } else if (category == null && !bridgeNotLib && lib) {
                        keys.add(modules[i]);
                    } else if (category == null && bridgeNotLib && bridge) {
                        keys.add(modules[i]);
                    }
                }
                final Comparator collator = Collator.getInstance();
                Collections.sort (keys, new Comparator () {
                        public int compare (Object o1, Object o2) {
                            ModuleBean m1 = (ModuleBean) o1;
                            ModuleBean m2 = (ModuleBean) o2;
                            return collator.compare(m1.getDisplayName(), m2.getDisplayName());
                        }
                    });
                setKeys (keys);
            }

            protected Node[] createNodes (Object key) {
                return new Node[] { new Item ((ModuleBean) key, simple) };
            }

            public void propertyChange (PropertyChangeEvent ev) {
                refreshKeys ();
            }

        }

    }

    /** Getter for all modules.
     */
    private static ModuleBean.AllModulesBean getAllModules () {
        return ModuleBean.AllModulesBean.getDefault();
    }

    private static ModuleDeleter getModuleDeleter () {
        if (deleter == null) {
            deleter = new ModuleDeleterImpl ();
        }
        return deleter;
    }
    
}
