public static class

XTree.ComparableDefaultMutableTreeNode

extends DefaultMutableTreeNode
implements Comparable<T>
/*
 * Copyright (c) 2004, 2006, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.tools.jconsole.inspector;

import java.awt.EventQueue;
import java.util.*;
import javax.management.*;
import javax.swing.*;
import javax.swing.tree.*;
import sun.tools.jconsole.JConsole;
import sun.tools.jconsole.MBeansTab;
import sun.tools.jconsole.Resources;
import sun.tools.jconsole.inspector.XNodeInfo;
import sun.tools.jconsole.inspector.XNodeInfo.Type;

@SuppressWarnings("serial")
public class XTree extends JTree {

    private static final List<String> orderedKeyPropertyList =
            new ArrayList<String>();
    static {
        String keyPropertyList =
                System.getProperty("com.sun.tools.jconsole.mbeans.keyPropertyList");
        if (keyPropertyList == null) {
            orderedKeyPropertyList.add("type");
            orderedKeyPropertyList.add("j2eeType");
        } else {
            StringTokenizer st = new StringTokenizer(keyPropertyList, ",");
            while (st.hasMoreTokens()) {
                orderedKeyPropertyList.add(st.nextToken());
            }
        }
    }

    private MBeansTab mbeansTab;

    private Map<String, DefaultMutableTreeNode> nodes =
            new HashMap<String, DefaultMutableTreeNode>();

    public XTree(MBeansTab mbeansTab) {
        this(new DefaultMutableTreeNode("MBeanTreeRootNode"), mbeansTab);
    }

    public XTree(TreeNode root, MBeansTab mbeansTab) {
        super(root);
        this.mbeansTab = mbeansTab;
        setRootVisible(false);
        setShowsRootHandles(true);
        ToolTipManager.sharedInstance().registerComponent(this);
    }

    /**
     * This method removes the node from its parent
     */
    // Call on EDT
    private synchronized void removeChildNode(DefaultMutableTreeNode child) {
        DefaultTreeModel model = (DefaultTreeModel) getModel();
        model.removeNodeFromParent(child);
    }

    /**
     * This method adds the child to the specified parent node
     * at specific index.
     */
    // Call on EDT
    private synchronized void addChildNode(
            DefaultMutableTreeNode parent,
            DefaultMutableTreeNode child,
            int index) {
        // Tree does not show up when there is only the root node
        //
        DefaultTreeModel model = (DefaultTreeModel) getModel();
        DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
        boolean rootLeaf = root.isLeaf();
        model.insertNodeInto(child, parent, index);
        if (rootLeaf) {
            model.nodeStructureChanged(root);
        }
    }

    /**
     * This method adds the child to the specified parent node.
     * The index where the child is to be added depends on the
     * child node being Comparable or not. If the child node is
     * not Comparable then it is added at the end, i.e. right
     * after the current parent's children.
     */
    // Call on EDT
    private synchronized void addChildNode(
            DefaultMutableTreeNode parent, DefaultMutableTreeNode child) {
        int childCount = parent.getChildCount();
        if (childCount == 0) {
            addChildNode(parent, child, 0);
        } else if (child instanceof ComparableDefaultMutableTreeNode) {
            ComparableDefaultMutableTreeNode comparableChild =
                (ComparableDefaultMutableTreeNode)child;
            int i = 0;
            for (; i < childCount; i++) {
                DefaultMutableTreeNode brother =
                        (DefaultMutableTreeNode) parent.getChildAt(i);
                //child < brother
                if (comparableChild.compareTo(brother) < 0) {
                    addChildNode(parent, child, i);
                    break;
                }
                //child = brother
                else if (comparableChild.compareTo(brother) == 0) {
                    addChildNode(parent, child, i);
                    break;
                }
            }
            //child < all brothers
            if (i == childCount) {
                addChildNode(parent, child, childCount);
            }
        } else {
            //not comparable, add at the end
            addChildNode(parent, child, childCount);
        }
    }

    /**
     * This method removes all the displayed nodes from the tree,
     * but does not affect actual MBeanServer contents.
     */
    // Call on EDT
    public synchronized void removeAll() {
        DefaultTreeModel model = (DefaultTreeModel) getModel();
        DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
        root.removeAllChildren();
        model.nodeStructureChanged(root);
        nodes.clear();
    }

    public void delMBeanFromView(final ObjectName mbean) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                // We assume here that MBeans are removed one by one (on MBean
                // unregistered notification). Deletes the tree node associated
                // with the given MBean and recursively all the node parents
                // which are leaves and non XMBean.
                //
                synchronized (XTree.this) {
                    DefaultMutableTreeNode node = null;
                    Dn dn = buildDn(mbean);
                    if (dn.size() > 0) {
                        DefaultTreeModel model = (DefaultTreeModel) getModel();
                        Token token = dn.getToken(0);
                        String hashKey = dn.getHashKey(token);
                        node = nodes.get(hashKey);
                        if ((node != null) && (!node.isRoot())) {
                            if (hasMBeanChildren(node)) {
                                removeNonMBeanChildren(node);
                                String label = token.getValue().toString();
                                XNodeInfo userObject = new XNodeInfo(
                                        Type.NONMBEAN, label,
                                        label, token.toString());
                                changeNodeValue(node, userObject);
                            } else {
                                DefaultMutableTreeNode parent =
                                        (DefaultMutableTreeNode) node.getParent();
                                model.removeNodeFromParent(node);
                                nodes.remove(hashKey);
                                delParentFromView(dn, 1, parent);
                            }
                        }
                    }
                }
            }
        });
    }

    /**
     * Returns true if any of the children nodes is an MBean.
     */
    private boolean hasMBeanChildren(DefaultMutableTreeNode node) {
        for (Enumeration e = node.children(); e.hasMoreElements(); ) {
            DefaultMutableTreeNode n = (DefaultMutableTreeNode) e.nextElement();
            if (((XNodeInfo) n.getUserObject()).getType().equals(Type.MBEAN)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Remove all the children nodes which are not MBean.
     */
    private void removeNonMBeanChildren(DefaultMutableTreeNode node) {
        Set<DefaultMutableTreeNode> metadataNodes =
                new HashSet<DefaultMutableTreeNode>();
        DefaultTreeModel model = (DefaultTreeModel) getModel();
        for (Enumeration e = node.children(); e.hasMoreElements(); ) {
            DefaultMutableTreeNode n = (DefaultMutableTreeNode) e.nextElement();
            if (!((XNodeInfo) n.getUserObject()).getType().equals(Type.MBEAN)) {
                metadataNodes.add(n);
            }
        }
        for (DefaultMutableTreeNode n : metadataNodes) {
            model.removeNodeFromParent(n);
        }
    }

    /**
     * Removes only the parent nodes which are non MBean and leaf.
     * This method assumes the child nodes have been removed before.
     */
    private DefaultMutableTreeNode delParentFromView(
            Dn dn, int index, DefaultMutableTreeNode node) {
        if ((!node.isRoot()) && node.isLeaf() &&
                (!(((XNodeInfo) node.getUserObject()).getType().equals(Type.MBEAN)))) {
            DefaultMutableTreeNode parent =
                    (DefaultMutableTreeNode) node.getParent();
            removeChildNode(node);
            String hashKey = dn.getHashKey(dn.getToken(index));
            nodes.remove(hashKey);
            delParentFromView(dn, index + 1, parent);
        }
        return node;
    }

    public synchronized void addMBeanToView(final ObjectName mbean) {
        final XMBean xmbean;
        try {
            xmbean = new XMBean(mbean, mbeansTab);
            if (xmbean == null) {
                return;
            }
        } catch (Exception e) {
            // Got exception while trying to retrieve the
            // given MBean from the underlying MBeanServer
            //
            if (JConsole.isDebug()) {
                e.printStackTrace();
            }
            return;
        }
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                synchronized (XTree.this) {
                    // Add the new nodes to the MBean tree from leaf to root

                    Dn dn = buildDn(mbean);
                    if (dn.size() == 0) return;
                    Token token = dn.getToken(0);
                    DefaultMutableTreeNode node = null;
                    boolean nodeCreated = true;

                    //
                    // Add the node or replace its user object if already added
                    //

                    String hashKey = dn.getHashKey(token);
                    if (nodes.containsKey(hashKey)) {
                        //already in the tree, means it has been created previously
                        //when adding another node
                        node = nodes.get(hashKey);
                        //sets the user object
                        final Object data = createNodeValue(xmbean, token);
                        final String label = data.toString();
                        final XNodeInfo userObject =
                                new XNodeInfo(Type.MBEAN, data, label, mbean.toString());
                        changeNodeValue(node, userObject);
                        nodeCreated = false;
                    } else {
                        //create a new node
                        node = createDnNode(dn, token, xmbean);
                        if (node != null) {
                            nodes.put(hashKey, node);
                            nodeCreated = true;
                        } else {
                            return;
                        }
                    }

                    //
                    // Add (virtual) nodes without user object if necessary
                    //

                    for (int i = 1; i < dn.size(); i++) {
                        DefaultMutableTreeNode currentNode = null;
                        token = dn.getToken(i);
                        hashKey = dn.getHashKey(token);
                        if (nodes.containsKey(hashKey)) {
                            //node already present
                            if (nodeCreated) {
                                //previous node created, link to do
                                currentNode = nodes.get(hashKey);
                                addChildNode(currentNode, node);
                                return;
                            } else {
                                //both nodes already present
                                return;
                            }
                        } else {
                            //creates the node that can be a virtual one
                            if (token.getKeyDn().equals("domain")) {
                                //better match on keyDn that on Dn
                                currentNode = createDomainNode(dn, token);
                                if (currentNode != null) {
                                    final DefaultMutableTreeNode root =
                                            (DefaultMutableTreeNode) getModel().getRoot();
                                    addChildNode(root, currentNode);
                                }
                            } else {
                                currentNode = createSubDnNode(dn, token);
                                if (currentNode == null) {
                                    //skip
                                    continue;
                                }
                            }
                            nodes.put(hashKey, currentNode);
                            addChildNode(currentNode, node);
                            nodeCreated = true;
                        }
                        node = currentNode;
                    }
                }
            }
        });
    }

    // Call on EDT
    private synchronized void changeNodeValue(
            final DefaultMutableTreeNode node, XNodeInfo nodeValue) {
        if (node instanceof ComparableDefaultMutableTreeNode) {
            // should it stay at the same place?
            DefaultMutableTreeNode clone =
                    (DefaultMutableTreeNode) node.clone();
            clone.setUserObject(nodeValue);
            if (((ComparableDefaultMutableTreeNode) node).compareTo(clone) == 0) {
                // the order in the tree didn't change
                node.setUserObject(nodeValue);
                DefaultTreeModel model = (DefaultTreeModel) getModel();
                model.nodeChanged(node);
            } else {
                // delete the node and re-order it in case the
                // node value modifies the order in the tree
                DefaultMutableTreeNode parent =
                        (DefaultMutableTreeNode) node.getParent();
                removeChildNode(node);
                node.setUserObject(nodeValue);
                addChildNode(parent, node);
            }
        } else {
            // not comparable stays at the same place
            node.setUserObject(nodeValue);
            DefaultTreeModel model = (DefaultTreeModel) getModel();
            model.nodeChanged(node);
        }
        // Load the MBean metadata if type is MBEAN
        if (nodeValue.getType().equals(Type.MBEAN)) {
            XMBeanInfo.loadInfo(node);
            DefaultTreeModel model = (DefaultTreeModel) getModel();
            model.nodeStructureChanged(node);
        }
        // Clear the current selection and set it
        // again so valueChanged() gets called
        if (node == getLastSelectedPathComponent()) {
            TreePath selectionPath = getSelectionPath();
            clearSelection();
            setSelectionPath(selectionPath);
        }
    }

    //creates the domain node, called on a domain token
    private DefaultMutableTreeNode createDomainNode(Dn dn, Token token) {
        DefaultMutableTreeNode node = new ComparableDefaultMutableTreeNode();
        String label = dn.getDomain();
        XNodeInfo userObject =
                new XNodeInfo(Type.NONMBEAN, label, label, label);
        node.setUserObject(userObject);
        return node;
    }

    //creates the node corresponding to the whole Dn
    private DefaultMutableTreeNode createDnNode(
            Dn dn, Token token, XMBean xmbean) {
        DefaultMutableTreeNode node = new ComparableDefaultMutableTreeNode();
        Object data = createNodeValue(xmbean, token);
        String label = data.toString();
        XNodeInfo userObject = new XNodeInfo(Type.MBEAN, data, label,
                xmbean.getObjectName().toString());
        node.setUserObject(userObject);
        XMBeanInfo.loadInfo(node);
        return node;
    }

    //creates a node with the token value, call for each non domain sub
    //dn token
    private DefaultMutableTreeNode createSubDnNode(Dn dn, Token token) {
        DefaultMutableTreeNode node = new ComparableDefaultMutableTreeNode();
        String label = isKeyValueView() ? token.toString() :
            token.getValue().toString();
        XNodeInfo userObject =
                new XNodeInfo(Type.NONMBEAN, label, label, token.toString());
        node.setUserObject(userObject);
        return node;
    }

    private Object createNodeValue(XMBean xmbean, Token token) {
        String label = isKeyValueView() ? token.toString() :
            token.getValue().toString();
        xmbean.setText(label);
        return xmbean;
    }

    /**
     * Parses MBean ObjectName comma-separated properties string and put the
     * individual key/value pairs into the map. Key order in the properties
     * string is preserved by the map.
     */
    private Map<String,String> extractKeyValuePairs(
            String properties, ObjectName mbean) {
        String props = properties;
        Map<String,String> map = new LinkedHashMap<String,String>();
        int eq = props.indexOf("=");
        while (eq != -1) {
            String key = props.substring(0, eq);
            String value = mbean.getKeyProperty(key);
            map.put(key, value);
            props = props.substring(key.length() + 1 + value.length());
            if (props.startsWith(",")) {
                props = props.substring(1);
            }
            eq = props.indexOf("=");
        }
        return map;
    }

    /**
     * Returns the ordered key property list that will be used to build the
     * MBean tree. If the "com.sun.tools.jconsole.mbeans.keyPropertyList" system
     * property is not specified, then the ordered key property list used
     * to build the MBean tree will be the one returned by the method
     * ObjectName.getKeyPropertyListString() with "type" as first key,
     * and "j2eeType" as second key, if present. If any of the keys specified
     * in the comma-separated key property list does not apply to the given
     * MBean then it will be discarded.
     */
    private String getKeyPropertyListString(ObjectName mbean) {
        String props = mbean.getKeyPropertyListString();
        Map<String,String> map = extractKeyValuePairs(props, mbean);
        StringBuilder sb = new StringBuilder();
        // Add the key/value pairs to the buffer following the
        // key order defined by the "orderedKeyPropertyList"
        for (String key : orderedKeyPropertyList) {
            if (map.containsKey(key)) {
                sb.append(key + "=" + map.get(key) + ",");
                map.remove(key);
            }
        }
        // Add the remaining key/value pairs to the buffer
        for (Map.Entry<String,String> entry : map.entrySet()) {
            sb.append(entry.getKey() + "=" + entry.getValue() + ",");
        }
        String orderedKeyPropertyListString = sb.toString();
        orderedKeyPropertyListString = orderedKeyPropertyListString.substring(
                0, orderedKeyPropertyListString.length() - 1);
        return orderedKeyPropertyListString;
    }

    /**
     * Builds the Dn for the given MBean.
     */
    private Dn buildDn(ObjectName mbean) {

        String domain = mbean.getDomain();
        String globalDn = getKeyPropertyListString(mbean);

        Dn dn = buildDn(domain, globalDn, mbean);

        //update the Dn tokens to add the domain
        dn.updateDn();

        //reverse the Dn (from leaf to root)
        dn.reverseOrder();

        //compute the hashDn
        dn.computeHashDn();

        return dn;
    }

    /**
     * Builds the Dn for the given MBean.
     */
    private Dn buildDn(String domain, String globalDn, ObjectName mbean) {
        Dn dn = new Dn(domain, globalDn);
        String keyDn = "no_key";
        if (isTreeView()) {
            String props = globalDn;
            Map<String,String> map = extractKeyValuePairs(props, mbean);
            for (Map.Entry<String,String> entry : map.entrySet()) {
                dn.addToken(new Token(keyDn,
                        entry.getKey() + "=" + entry.getValue()));
            }
        } else {
            //flat view
            dn.addToken(new Token(keyDn, "properties=" + globalDn));
        }
        return dn;
    }

    //
    //utility objects
    //

    public static class ComparableDefaultMutableTreeNode
            extends DefaultMutableTreeNode
            implements Comparable<DefaultMutableTreeNode> {
        public int compareTo(DefaultMutableTreeNode node) {
            return (this.toString().compareTo(node.toString()));
        }
    }

    //
    //tree preferences
    //

    private boolean treeView;
    private boolean treeViewInit = false;
    public boolean isTreeView() {
        if (!treeViewInit) {
            treeView = getTreeViewValue();
            treeViewInit = true;
        }
        return treeView;
    }

    private boolean getTreeViewValue() {
        String treeView = System.getProperty("treeView");
        return ((treeView == null) ? true : !(treeView.equals("false")));
    }

    //
    //MBean key-value preferences
    //

    private boolean keyValueView = Boolean.getBoolean("keyValueView");
    public boolean isKeyValueView() {
        return keyValueView;
    }

    //
    //utility classes
    //

    public static class Dn {

        private String domain;
        private String dn;
        private String hashDn;
        private ArrayList<Token> tokens = new ArrayList<Token>();

        public Dn(String domain, String dn) {
            this.domain = domain;
            this.dn = dn;
        }

        public void clearTokens() {
            tokens.clear();
        }

        public void addToken(Token token) {
            tokens.add(token);
        }

        public void addToken(int index, Token token) {
            tokens.add(index, token);
        }

        public void setToken(int index, Token token) {
            tokens.set(index, token);
        }

        public void removeToken(int index) {
            tokens.remove(index);
        }

        public Token getToken(int index) {
            return tokens.get(index);
        }

        public void reverseOrder() {
            ArrayList<Token> newOrder = new ArrayList<Token>(tokens.size());
            for (int i = tokens.size() - 1; i >= 0; i--) {
                newOrder.add(tokens.get(i));
            }
            tokens = newOrder;
        }

        public int size() {
            return tokens.size();
        }

        public String getDomain() {
            return domain;
        }

        public String getDn() {
            return dn;
        }

        public String getHashDn() {
            return hashDn;
        }

        public String getHashKey(Token token) {
            final int begin = getHashDn().indexOf(token.getHashToken());
            return  getHashDn().substring(begin, getHashDn().length());
        }

        public void computeHashDn() {
            final StringBuilder hashDn = new StringBuilder();
            final int tokensSize = tokens.size();
            for (int i = 0; i < tokensSize; i++) {
                Token token = tokens.get(i);
                String hashToken = token.getHashToken();
                if (hashToken == null) {
                    hashToken = token.getToken() + (tokensSize - i);
                    token.setHashToken(hashToken);
                }
                hashDn.append(hashToken);
                hashDn.append(",");
            }
            if (tokensSize > 0) {
                this.hashDn = hashDn.substring(0, hashDn.length() - 1);
            } else {
                this.hashDn = "";
            }
        }

        /**
         * Adds the domain as the first token in the Dn.
         */
        public void updateDn() {
            addToken(0, new Token("domain", "domain=" + getDomain()));
        }

        public String toString() {
            return tokens.toString();
        }
    }

    public static class Token {

        private String keyDn;
        private String token;
        private String hashToken;
        private String key;
        private String value;

        public Token(String keyDn, String token) {
            this.keyDn = keyDn;
            this.token = token;
            buildKeyValue();
        }

        public Token(String keyDn, String token, String hashToken) {
            this.keyDn = keyDn;
            this.token = token;
            this.hashToken = hashToken;
            buildKeyValue();
        }

        public String getKeyDn() {
            return keyDn;
        }

        public String getToken() {
            return token;
        }

        public void setValue(String value) {
            this.value = value;
            this.token = key + "=" + value;
        }

        public void setKey(String key) {
            this.key = key;
            this.token = key + "=" + value;
        }

        public void setKeyDn(String keyDn) {
            this.keyDn = keyDn;
        }

        public  void setHashToken(String hashToken) {
            this.hashToken = hashToken;
        }

        public String getHashToken() {
            return hashToken;
        }

        public String getKey() {
            return key;
        }

        public String getValue() {
            return value;
        }

        public String toString(){
            return getToken();
        }

        public boolean equals(Object object) {
            if (object instanceof Token) {
                return token.equals(((Token) object));
            } else {
                return false;
            }
        }

        private void buildKeyValue() {
            int index = token.indexOf("=");
            if (index < 0) {
                key = token;
                value = token;
            } else {
                key = token.substring(0, index);
                value = token.substring(index + 1, token.length());
            }
        }
    }
}