Monday, October 18, 2010

Display XML in JTree with DnD Support

This brief tutorial will explain how to use Java to display a tree view of XML using JTree with drag n drop support ,custom TreeCellRenderer to display different icons for different type of nodes and a popup.





Below is the xml that we will be using to display in the Jtree.
























CustomTreeNode

First lets implement our custom tree node by extending the DefaultMutableTreeNode. In this class we will define one property and will override two methods.

toString() -> to display a meaningful name in the tree node
isLeaf() -> to specify that this node is a leaf iff the tag name is file, non-leaf otherwise.

And we will define a new property 'loaded' to indicate whether the node has been loaded or not.


package org.gpl.xmltree;

import javax.swing.tree.DefaultMutableTreeNode;
import org.w3c.dom.Element;

/**
 *
 * @author Naveed Quadri
 */
public class XMLNode extends DefaultMutableTreeNode {

    private String name;
    private String iconName;
    private boolean isFile;
    private boolean loaded;

    XMLNode(Object nodeObject) {
        super(nodeObject);
        name = ((Element) userObject).getAttribute("printableName");
        iconName = ((Element) userObject).getAttribute("type");
        isFile = ((Element) userObject).getTagName().equalsIgnoreCase("file");
    }

    public boolean isLoaded() {
        return loaded;
    }

    public void setLoaded(boolean loaded) {
        this.loaded = loaded;
    }

    public String getIconName() {
        return iconName;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public boolean isLeaf() {
        return isFile;
    }
}

Custom TreeModel

All the tutorials i found on the internet suggested implementing the TreeModel interface for displaying XML data. But I found extending the DefaultTreeModel much better as all of the work is already done for us. For lazy loading of children, we'll implement the TreeWillExpandListener, so that the child nodes if any can be loaded when requested. Once the node has been loaded we will set the loaded property in the XMLNode class to true, so that the we dont have load the node again n again.


package org.gpl.xmltree;

import java.util.Vector;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import org.w3c.dom.*;

/**
 *
* @author Naveed Quadri
 */
public class XMLTreeModel extends DefaultTreeModel implements TreeWillExpandListener {

    /**
     *
     * @param parentNode 
     * @return
     * @see http://www.developer.com/xml/article.php/10929_3731356_2/Displaying-XML-in-a-Swing-JTree.htm
     */
    public static Vector getChildElements(Node parentNode) {
        NodeList list = parentNode.getChildNodes();
        Vector childNodes = new Vector();
        for (int i = 0; i < list.getLength(); i++) {
            if (list.item(i).getNodeType() == Node.ELEMENT_NODE) {
                childNodes.add((Element) list.item(i));// = new XMLNode((Element) list.item(i));
            }
        }
        return childNodes;
    }

    /**
     *
     * @param root
     */
    public XMLTreeModel(XMLNode root) {
        super(root);
        setChildren(root, XMLTreeModel.getChildElements((Node) root.getUserObject()));
    }

    /**
     *
     * @param parentNode
     * @param childElements
     */
    public void setChildren(XMLNode parentNode, Vector childElements) {
        if (childElements == null) {
            return;
        }
        //get the chld count
        int childCount = parentNode.getChildCount();
        //set the node as loaded
        parentNode.setLoaded(true);
        //remove all old nodes from the parent
        if (childCount > 0) {
            for (int i = 0; i < childCount; i++) {
                removeNodeFromParent((DefaultMutableTreeNode) parentNode.getChildAt(0));
            }
        }
        XMLNode node;
        //insert the nodes in the parent node
        for (int i = 0; i < childElements.size(); i++) {
            node = new XMLNode(childElements.get(i));
                insertNodeInto(node, parentNode, i);
        }
    }

    /**
     *
     * @param event
     * @throws ExpandVetoException
     */
    public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
        //get the lazy node
        XMLNode lazyNode = (XMLNode) event.getPath().getLastPathComponent();
        //node is already loaded, does'nt have to do it again. return.
        if (lazyNode.isLoaded()) {
            return;
        }
        //add the child nodes to the parent
        setChildren(lazyNode, XMLTreeModel.getChildElements((Node) lazyNode.getUserObject()));

    }

    public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
    }
}


Custom TreeCellRenderer

To display different icons for different type of nodes, we add HashTable as a client property to JTree. The Hashtable will map the iconType to the icon file .



//
//....... 
// 
jTree.putClientProperty("icons", getIcons());
//
//........ 
//
private Hashtable getIcons() {
        Hashtable icons = new Hashtable();
        icons.put("unknown", IconFactory.getIcon("unknown", IconFactory.IconSize.SIZE_16X16));
        icons.put("doc", IconFactory.getIcon("word", IconFactory.IconSize.SIZE_16X16));
        icons.put("pdf", IconFactory.getIcon("adobe", IconFactory.IconSize.SIZE_16X16));
        icons.put("docx", IconFactory.getIcon("word", IconFactory.IconSize.SIZE_16X16));
        icons.put("dir", IconFactory.getIcon("dir", IconFactory.IconSize.SIZE_16X16));
        icons.put("txt", IconFactory.getIcon("text", IconFactory.IconSize.SIZE_16X16));
        icons.put("archive", IconFactory.getIcon("archive", IconFactory.IconSize.SIZE_16X16));
        icons.put("media", IconFactory.getIcon("media", IconFactory.IconSize.SIZE_16X16));
        icons.put("/", IconFactory.getIcon("usb", IconFactory.IconSize.SIZE_16X16));
        return icons;
    }

IconFactory is the class to get the icons from the resources dir and resize it to 16X16. The source of this class can be found in the project archive attached at the end of the page.
In the TreeCellRenderer we retrieve the icons hashtable set the icon depending on the type of node.

package org.gpl.xmltree;

import java.awt.Component;
import java.util.Hashtable;
import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeCellRenderer;

/**
 *
 * @author Naveed Quadri
 */
public class XMLTreeCellRenderer extends DefaultTreeCellRenderer{

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf,
                row, hasFocus);
        Icon icon = null;
        //Retrieve the 'icons' clientProperty
        Hashtable icons = (Hashtable) tree.getClientProperty("icons");
        //get the type of node
        String name = ((XMLNode) value).getIconName();
        if ((name != null)) {
            //get the icon for this type of node
            icon = (Icon) icons.get(name);
            if(icon == null)
                //if we could'nt find anything, get the 'unknown' icon
                icon = (Icon)icons.get("unknown");
            //set the icon
            setIcon(icon);
        }
        //set the tooltip
        setToolTipText(((XMLNode) value).toString());
        // return back this component
        return this;
    }
}

TransferableNode

To implement the drag and drop interface, XMLNode  has to wrapped in a transferable object. we do that by implenting the transferable interface.  The Transferable interface contains three methods that has to be implemented.

package org.gpl.xmltree;

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;

public class NodesTransferable implements Transferable {

    //specify the data flavor
    final public static DataFlavor INFO_FLAVOR =
            new DataFlavor(XMLNode[].class, "application/x-java-serialized-object");
    static DataFlavor flavors[] = {INFO_FLAVOR};
    XMLNode[] nodes;

    //an array of XMLNodes to support DnD for more than one node
    public NodesTransferable(XMLNode[] nodes) {
        this.nodes = nodes;
    }

    /**
     *
     * @param flavor
     * @return NodesTransferable object
     * @throws UnsupportedFlavorException
     */
    public Object getTransferData(DataFlavor flavor)
            throws UnsupportedFlavorException {
        if (!isDataFlavorSupported(flavor)) {
            throw new UnsupportedFlavorException(flavor);
        }
        return nodes;
    }

    //return the data flavors
    public DataFlavor[] getTransferDataFlavors() {
        return flavors;
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {
        return flavor.equals(INFO_FLAVOR);
    }
}



TransferHandler

Now that we have our transferable object, we need a transfer handler that will transfer the node object from one position to other.  we do that by extending the TransferHandler class.



package org.gpl.xmltree;

import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.TransferHandler;
import javax.swing.tree.TreePath;
import org.w3c.dom.Node;

/**
 *
 * @author Naveed Quadri
 * @see http://www.coderanch.com/t/346509/GUI/java/JTree-drag-drop-inside-one
 */
class TreeTransferHandler extends TransferHandler {

    private XMLNode[] nodesToRemove;

    public TreeTransferHandler() {
    }

    @Override
    public boolean canImport(TransferHandler.TransferSupport support) {

        if (!support.isDrop()) {
            return false;
        }

        support.setShowDropLocation(true);

        if (!support.isDataFlavorSupported(NodesTransferable.INFO_FLAVOR)) {
            return false;
        }

        // Do not allow a drop on the drag source selections.
        JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
        TreePath dest = dl.getPath();
        XMLNode target = (XMLNode) dest.getLastPathComponent();
        JTree tree = (JTree) support.getComponent();
        int dropRow = tree.getRowForPath(dl.getPath());
        int[] selRows = tree.getSelectionRows();
        for (int i = 0; i < selRows.length; i++) {
            if (selRows[i] == dropRow) {
                return false;
            }
        }

        if (target.isLeaf()) {
            return false;
        }
        // Do not allow MOVE-action drops if a non-leaf node is
        // selected unless all of its children are also selected.
        int action = support.getDropAction();
        if (action == MOVE) {
            return haveCompleteNode(tree);
        }
        // Do not allow a non-leaf node to be copied to a level
        // which is less than its source level.


        TreePath path = tree.getPathForRow(selRows[0]);
        XMLNode firstNode =
                (XMLNode) path.getLastPathComponent();
        if (firstNode.getChildCount() > 0
                && target.getLevel() < firstNode.getLevel()) {

            return false;
        }
        return true;
    }

    private boolean haveCompleteNode(JTree tree) {
        int[] selRows = tree.getSelectionRows();
        TreePath path = tree.getPathForRow(selRows[0]);
        XMLNode first =
                (XMLNode) path.getLastPathComponent();
        int childCount = first.getChildCount();
        // first has children and no children are selected.
        if (childCount > 0 && selRows.length == 1) {
            return false;
        }
        // first may have children.
        for (int i = 1; i < selRows.length; i++) {
            path = tree.getPathForRow(selRows[i]);
            XMLNode next =
                    (XMLNode) path.getLastPathComponent();
            if (first.isNodeChild(next)) {
                // Found a child of first.
                if (childCount > selRows.length - 1) {
                    // Not all children of first are selected.
                    return false;
                }

            }
        }
        return true;
    }

    @Override
    protected Transferable createTransferable(JComponent c) {
        JTree tree = (JTree) c;
        TreePath[] paths = tree.getSelectionPaths();
        if (paths != null) {
            // Make up a node array of copies for transfer and
            // another for/of the nodes that will be removed in
            // exportDone after a successful drop.
            List copies =
                    new ArrayList();
            List toRemove =
                    new ArrayList();
            XMLNode node =
                    (XMLNode) paths[0].getLastPathComponent();
            if (!node.isLoaded()) {
                XMLTreeModel model = (XMLTreeModel) tree.getModel();
                model.setChildren(node, XMLTreeModel.getChildElements((Node) node.getUserObject()));
            }

            //XMLNode will loose its  loaded property after
            //making a copy of it.
            //so reading the property of the node before making
            //a copy and setting the property back after copying.

            boolean loaded = node.isLoaded();
            XMLNode copy = copy(node);
            copy.setLoaded(loaded);
            copies.add(copy);
            toRemove.add(node);
            for (int i = 1; i < paths.length; i++) {
                XMLNode next =
                        (XMLNode) paths[i].getLastPathComponent();
                // Do not allow higher level nodes to be added to list.
                if (next.getLevel() < node.getLevel()) {
                    break;
                } else if (next.getLevel() > node.getLevel()) { // child node
                    copy.add(copy(next));
                    // node already contains child
                } else { // sibling
                    copies.add(copy(next));
                    toRemove.add(next);
                }
            }
            XMLNode[] nodes =
                    copies.toArray(new XMLNode[copies.size()]);
            nodesToRemove =
                    toRemove.toArray(new XMLNode[toRemove.size()]);
            return new NodesTransferable(nodes);
        }
        return null;
    }

    /** Defensive copy used in createTransferable. */
    private XMLNode copy(XMLNode node) {
        return new XMLNode(node.getUserObject());
    }

    protected void exportDone(JComponent source, Transferable data, int action) {
        if ((action & MOVE) == MOVE) {
            JTree tree = (JTree) source;
            XMLTreeModel model = (XMLTreeModel) tree.getModel();
            // Remove nodes saved in nodesToRemove in createTransferable.
            for (int i = 0; i < nodesToRemove.length; i++) {
                model.removeNodeFromParent(nodesToRemove[i]);
            }
        }
    }

    public int getSourceActions(JComponent c) {
        return COPY_OR_MOVE;
    }

    public boolean importData(TransferHandler.TransferSupport support) {
        if (!canImport(support)) {
            return false;
        }
        // Extract transfer data.
        XMLNode[] nodes = null;
        try {
            Transferable t = support.getTransferable();
            nodes = (XMLNode[]) t.getTransferData(NodesTransferable.INFO_FLAVOR);
        } catch (UnsupportedFlavorException ufe) {
            System.out.println("UnsupportedFlavor: " + ufe.getMessage());
        } catch (java.io.IOException ioe) {
            System.out.println("I/O error: " + ioe.getMessage());
        }
        // Get drop location info.
        JTree.DropLocation dl =
                (JTree.DropLocation) support.getDropLocation();
        int childIndex = dl.getChildIndex();
        TreePath dest = dl.getPath();
        XMLNode parent =
                (XMLNode) dest.getLastPathComponent();
        JTree tree = (JTree) support.getComponent();
        XMLTreeModel model = (XMLTreeModel) tree.getModel();
        if (!parent.isLoaded()) {
            model.setChildren(parent, XMLTreeModel.getChildElements((Node) parent.getUserObject()));
        }
        // Configure for drop mode.
        int index = childIndex; // DropMode.INSERT
        if (childIndex == -1) { // DropMode.ON
            index = parent.getChildCount();
        }
        // Add data to model.
        for (int i = 0; i < nodes.length; i++) {
            model.insertNodeInto(nodes[i], parent, index++);
        }
        return true;
    }

    public String toString() {
        return getClass().getName();
    }
}
 


XMLTree

Create a XMLTree class by extending the JTree. Get the path to the xml file in the constructor . Get the instance of the DocumentBuilderFactory to parse the xml file and get the root node of the xml. Instantiate and set the TreeModel and TreeCellRenderer. Add TreeWillExpandListener and put the client property for the icons.

package org.gpl.xmltree;

import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DropMode;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

/**
 *
 * @author Naveed Quadri
 */
public class XMLTree extends JTree {

    private XMLTreeModel xmlTreeModel;
    private XMLNode root;
    private XMLTreeCellRenderer xmlTreeCellRenderer;
    private Document xmlDoc;
    private AbstractAction expandCollapseAction;
    private JPopupMenu popupMenu;
    private TreePath clickedPath;

    public XMLTree(String xmlFile) {
        try {
            root = getRoot(xmlFile);
        } catch (ParserConfigurationException ex) {
        } catch (SAXException ex) {
        } catch (IOException ex) {
        } catch (NullPointerException ex) {
        }

        xmlTreeModel = new XMLTreeModel(root);
        xmlTreeCellRenderer = new XMLTreeCellRenderer();
        //set the treemodel
        setModel(xmlTreeModel);
        setShowsRootHandles(true);
        //add Tree Will Expand Listener
        addTreeWillExpandListener(xmlTreeModel);
        //enable drag n drop
        setDragEnabled(true);
        setDropMode(DropMode.ON_OR_INSERT);
        //
        setTransferHandler(new TreeTransferHandler());
        getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        putClientProperty("JTree.lineStyle", "Angled");
        // put Client Property for icons
        putClientProperty("icons", getIcons());
        // set the tree cell renderer
        setCellRenderer(xmlTreeCellRenderer);
        //attach the popup
        popupMenu = getJPopupForExplorerTree();
        add(popupMenu);

        // add the mouse listener for showing the popup
        addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    //get the co-ord of the mouse pointer
                    int x = e.getX();
                    int y = e.getY();
                    clickedPath = getPathForLocation(x, y);
                    if (clickedPath != null) {
                        XMLNode node = (XMLNode) clickedPath.getLastPathComponent();
                        //org.w3c.dom.Element el = node.getElement();
                        if (!node.isLeaf()) {
                            if (isExpanded(clickedPath)) {
                                expandCollapseAction.putValue(Action.NAME, "Collapse");
                            } else {
                                expandCollapseAction.putValue(Action.NAME, "Expand");
                            }
                        } else {
                            expandCollapseAction.putValue(Action.NAME, "Open");
                        }
                        popupMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
                        setSelectionPath(clickedPath);

                    }
                }
            }
        });
    }

    private JPopupMenu getJPopupForExplorerTree() {

        AbstractAction copyAction;
        AbstractAction cutAction;
        AbstractAction pasteAction;
        AbstractAction deleteAction;
        JPopupMenu popup = new JPopupMenu();
        expandCollapseAction = new AbstractAction() {

            public void actionPerformed(ActionEvent e) {
                if (clickedPath == null) {
                    return;
                }
                if (isExpanded(clickedPath)) {
                    collapsePath(clickedPath);
                } else {
                    expandPath(clickedPath);
                }
            }
        };
        copyAction = new AbstractAction("Copy", IconFactory.getIcon("copy", IconFactory.IconSize.SIZE_16X16)) {

            public void actionPerformed(ActionEvent e) {
                System.out.println("Copy");
            }
        };
        cutAction = new AbstractAction("Cut", IconFactory.getIcon("cut", IconFactory.IconSize.SIZE_16X16)) {

            public void actionPerformed(ActionEvent e) {
                System.out.println("Cut");
            }
        };
        pasteAction = new AbstractAction("Paste", IconFactory.getIcon("paste", IconFactory.IconSize.SIZE_16X16)) {

            public void actionPerformed(ActionEvent e) {
                System.out.println("Paste");
            }
        };
        deleteAction = new AbstractAction("Delete", IconFactory.getIcon("delete", IconFactory.IconSize.SIZE_16X16)) {

            public void actionPerformed(ActionEvent e) {
                System.out.println("Delete");
            }
        };
        popup.add(expandCollapseAction);
        popup.addSeparator();
        popup.add(copyAction);
        popup.add(cutAction);
        popup.add(pasteAction);
        popup.add(deleteAction);
        return popup;
    }

    private Hashtable getIcons() {
      //
      // see Custom TreeCellRenderer 
      // 
    }

    private XMLNode getRoot(String xmlFile) throws ParserConfigurationException, SAXException, IOException {
        Vector elements;
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = dbFactory.newDocumentBuilder();
        xmlDoc = builder.parse(new File(xmlFile));
        xmlDoc.normalize();
        elements = XMLTreeModel.getChildElements(xmlDoc);
        if (elements.size() > 0) {
            return new XMLNode(elements.get(0));

        }
        return null;
    }
}
Now the XMLTree can be instantiated by
XMLTree xmlTree = new XMLTree("path\\to\\xml\\file.xml");



A complete NetBeans project can be downloaded from here

9 comments:

  1. Create Post thank you very much!

    ReplyDelete
  2. Hello I got question about your project.

    is your project contain the way to save your

    change in your xml file?

    I used org.w3g.dom thing to find a way to save

    into the document object, but not yet succeed.

    so I looking for a way. your's seems like using

    SAX. Hopefully can you explain to me how you

    save your change to xml? that will be very

    helpful to my project. Thank you

    ReplyDelete
  3. @jangho
    No I have not implemented saving xml files in this project.
    You may already have visited this link, if not check this out java2s

    if it still doesnt work let me know, i can try it for you.

    ReplyDelete
  4. @jangho also i am using dom in this project, SAX is read only

    ReplyDelete
  5. @Naveed Thanks for your reply.

    I'm implementing the way to save into dom file in

    the importdata, in the transferhandler. and

    making another constructor

    to get the Dom file in the transferhandler

    currently I can't find how can I change the data

    in the dom file when I DnD the data.

    It shows good on the JTree but doesn't fix the

    xml, when I reload it.

    I wish I can get little help to figuring out

    Thanks

    ReplyDelete
  6. the object I tried to manipulate is the
    org.w3g.dom.document object all I need to implementing is about change document when I change tree.

    ReplyDelete
  7. jangho, right now i am out of station so i dont have my laptop handy so i cant check for myself..

    but a few pointers..
    implement TreeModelListener so when ever your model changes you are notified about it. and whenever model changes serialize the model itself. I think this shud give you an xml which you can deserialize later and construct your treemodel back. Or you can also use TransformerFactory.

    Document doc = you are getting this in your constructor
    TransformerFactory tf = TransformerFactory.newInstance();
    Transformer output = tf.newTransformer();
    output.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes");
    output.transform(new DOMSource(doc),new StreamResult(file));

    if your using an IDE it will fix your imports otherwise you can get them from javadocs, coz even i am not sure of the imports.

    tell me how this goes. if you found an alternate or better solution, i will be glad if you cud update me.

    ReplyDelete
  8. I am currently trying to implements DropTargetListener and DropSourcelistener and DropGestureListener, and try to mix it with my TransferHandler.

    However I have no gurantee of success yet.

    I'll try your first suggestion later.

    and I can't try the second one cause I cannot

    directly get a access to the xml file in the server.

    all I get is the document object from server

    and I need to manipulate it

    Anyway I'm trying to capture the object at the oment of drop with those listeners.

    If you have any suggestion or hint in that way, then let me know please

    I'm eager to get any suggestion or hint.

    Thank you for your kindly reply.

    ReplyDelete
  9. Hi Naveed,
    I'm trying to implements the rename of the XMLNode (printablename attribute).
    Can you tell me which is the best way for to do this?
    I have implemented the extended TreeModelListener class, I have the UserObject name
    changed but I don't see how to set the new attribute name in XMLNode.

    Many thanks
    Luigi

    ReplyDelete