Saturday Apr 12, 2008

Expandable File Nodes Displaying Parsed File Content

The only thing that should have been, potentially, interesting about yesterday's screenshot of a config file editor was that explorer view that showed an expanded node representing a file, with subnodes matching the content of the file. So, here I'll show how to create that effect, in this case for project.xml files:

I believe that knowing how to do this is going to be extremely useful for a lot of developers, especially those providing some kind of support for XML files, such as if you're creating web framework support, for example, or anything else involving XML files. Here are the steps, with all the code:

  1. Create a module project.

  2. Use the New File wizard, specifying the XML file's MIME type and namespace. When you finish the wizard, look in the MIME resolver. In my case, i.e., for project.xml files, I needed exactly this content in the MIME resolver, note especially the section in bold:

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
    To change this template, choose Tools | Templates
    and open the template in the editor.
    -->
    <!DOCTYPE MIME-resolver PUBLIC "-//NetBeans//DTD MIME Resolver 1.0//EN" 
                                   "http://www.netbeans.org/dtds/mime-resolver-1_0.dtd">
    <MIME-resolver>
        <file>
            <ext name="xml"/>
            <resolver mime="text/x-project+xml">
                <xml-rule>
                    <element ns="http://www.netbeans.org/ns/project/1"/>
                </xml-rule>
            </resolver>
        </file>
    </MIME-resolver>
    

  3. Next, let's use the DataObject class to parse the file and expose the result. Add the following to the generated DataObject class:

    private Map<String, List<String>> entries;
    
    //We will call this method from our DataNode.
    //When we do so, we parse the project.xml file
    //and return org.w3c.dom.Node names to the DataNode:
    synchronized Map<String, List<String>> getEntries() {
        if (entries == null) {
            parse();
        }
        return entries;
    }
    
    private void parse() {
        try {
            entries = new LinkedHashMap<String, List<String>>();
            List sectionEntries = null;
            BufferedReader br = null;
            //Use the FileObject retrieved from the DataObject,
            //via DataObject.getPrimaryFile(), to get the input stream:
            br = new BufferedReader(new InputStreamReader(getPrimaryFile().getInputStream()));
            InputSource source = new InputSource(br);
            //You could use any kind of parser, depending on your file type,
            //though for XML files you can use the NetBeans IDE org.openide.xml.XMLUtil class
            //to convert your input source to a org.w3c.dom.Document object:
            org.w3c.dom.Document doc = XMLUtil.parse(source, false, false, null, null);
            org.w3c.dom.NodeList list = doc.getElementsByTagName("\*");
            int length = list.getLength();
            for (int i = 0; i < length; i++) {
                org.w3c.dom.Node mainNode = list.item(i);
                String value = mainNode.getNodeName();
                //For purposes of this example, we simply put
                //the name of the node in our linked hashmap:
                entries.put(value, sectionEntries);
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        } catch (SAXException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

  4. Next, in the generated DataNode, lets create a controller for triggering the parsing and for creating a child node per node name:

    static class SectionChildren extends Children.Keys {
    
        private ProjectDataObject obj;
        private Lookup lookup;
    
        private SectionChildren(ProjectDataObject obj, Lookup lookup) {
            this.obj = obj;
            this.lookup = lookup;
        }
    
        //Called the first time that a list of nodes is needed.
        //An example of this is when the node is expanded.
        @Override
        protected void addNotify() {
            setKeys(obj.getEntries().keySet());
        }
    
        //Called when the user collapses a node and starts working on 
        //something else. The NetBeans Platform will notice that the list 
        //of nodes is no longer needed, and it will free up the memory that 
        //is no longer being used. 
        @Override
        protected void removeNotify() {
            setKeys(Collections.emptyList());
        }
     
        //Called whenever a child node needs to be constructed.
        @Override
        protected Node[] createNodes(String key) {
            return new Node[]{new SectionNode(key, obj.getEntries().get(key), lookup)};
        }
    }
    
    static class SectionNode extends AbstractNode {
    
        SectionNode(String name, java.util.List entries, Lookup lookup) {
            super(Children.LEAF);
            setName(name);
            setIconBaseWithExtension(ENTRY_IMAGE_ICON_BASE);
        }
    
    }

    For further info on the above code, see NetBeans System Properties Module Tutorial.

  5. Finally, you might be wondering how to trigger the above code. In other words, when/how should you determine that new child nodes should be created? Answer: rewrite the DataNode constructors:

    public ProjectDataNode(ProjectDataObject obj) {
        super(obj, new SectionChildren(obj, null));
        setIconBaseWithExtension(SECTION_IMAGE_ICON_BASE);
    }
    
    ProjectDataNode(ProjectDataObject obj, Lookup lookup) {
        super(obj, new SectionChildren(obj, lookup), lookup);
        setIconBaseWithExtension(SECTION_IMAGE_ICON_BASE);
    }

    The bits in bold above are what I added to trigger the creating of the child nodes.

And that's all. Now you can install your module and expand the node of the file type that you're interested in. Using the parse method above, you can parse the content in any way you like, so that when the DataNode creates new section nodes, the data that you've parsed will be used to set the display name (or something else) on the child nodes. Here, for example, I've parsed the project.xml file in such a way that all the module dependencies appear as the display names of the child nodes:

You can also add child nodes below the existing child nodes, and so on.

Configuration File Editor

About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2008 »
SunMonTueWedThuFriSat
  
2
4
5
13
18
19
23
30
   
       
Today