BookNBPlatformCookbookCH06

Contents

Nodes and Explorer

Introduction

Often when your application gets bigger or you have the need to visually present a hierarchy of objects in an easy and intuitive way you'll end up using Nodes and Explorers. Nodes are a great way of working with different instances of objects in your application and trigger other functionality like properties, menus, context sensitive wizards among others. Probably the API's used the most in the IDE to display the project and all it's related files and as main user interface for most of the IDE's functionality. A tree is probably the most intuitive, common and user friendly user interface available to display data in a structure way to the user. This group of API's rely a lot in the Lookup API to provide your application with a global knowledge of what's the user's focus at any given moment triggering context sensitive behaviour in your application. For example you can have your application behave differently based on what's the user's focus at a certain time. So it's a great way to handle a variety of data in your application while sharing the same platform. A great way to mix applications that otherwise would end up being stand alone applications. Will focus on other ways to further use those API and even combine them with other API's like Visual API. For this we'll start some preparation work related to the topics for chapter MISC. In that chapter we'll work with servers and clients using the NetBeans platform. We'll get to those details later. Here we'll build the basic aspects of the application. Here are the basic design elements of the application relevant to this topic:

  1. Each Application consists of a group of Zones.
  2. Each Zone consists of various Entities.
  3. Each Entity might contain other Entities.
  4. All the above have a set of properties.
  5. Entities might have a visual representation.

If you are interested on learning more about Marauroa you can learn more here: https://sourceforge.net/apps/mediawiki/jwrestling/index.php?title=Developer_Learning_Trail We'll get on the technical details of the elements above for now we have the following:

  1. There are group of objects with certain hierarchy among them. If you have been coding Java or most current programming languages that have user interfaces probably one thing came into your mind: Tree. We'll use the Nodes and Explorer to accomplish this.
  2. There are objects that might have a visual representation. There are many ways of attacking this with plain Java but we'll use the Visual API here.
  3. Objects with properties. We'll use the properties support of the IDE to handle this.

In addition we'll use the Lookup API to join everything together. The real power of the Lookup API is that you can focus on designing individual components without the hassle of making them work together.

Creating a Tree with Nodes and Explorer API

This basically what we are used to know as a Jtree but enhanced withing the NetBeans platform. Here's what we'll do in this topic:

  • Create the object all our components will be reacting with.
  • Create the Top Component to hold our nodes.
  • Link all together.

Preparation

There's plenty of documentation on the selection aspect within the platform so we'll focus on what's not covered in the following tutorials:

How to

We'll make use of the CentralLookup class (http://blogs.sun.com/geertjan/entry/central_lookup) and modify it to suit our needs. As for any big problem let's break it in small pieces. First we need a source of the objects that will be displayed on the tree. We'll use two sources for this: an embedded database and the file system. See the Files and Data chapter for more details on this.

Database Module

Let's start with the Database Module. Here's the diagram of the database:

File:5863-06-1.png

To start let's create the JPA layer to connect to this database. Refer to the Files and Data chapter on steps on how to generate it. That will be the contents of the database layer. In addition to the JPA classes let's create some classes that can be accessed from the Lookup. In a different package create a class named DataBaseManager. See the sample code for full source. Don't worry about the details of the code. I'll explain the relevant code for this chapter in the next step. In the same package create a new class named DatabaseTool with the following code:

package simple.marauroa.application.core.db.manager;

import java.util.HashMap;
import java.util.List;
import org.openide.util.Exceptions;
import simple.marauroa.application.api.IDataBase;
import org.openide.util.lookup.ServiceProvider;
import simple.marauroa.application.api.IMarauroaApplication;
import simple.marauroa.application.core.db.Application;
import simple.marauroa.application.core.db.controller.exceptions.NonexistentEntityException;

/**
 *
 * @author Javier A. Ortiz Bultron <javier.ortiz.78@gmail.com>
 */
@ServiceProvider(service = IDataBase.class)
public class DatabaseTool implements IDataBase {

    private HashMap<String, String> parameters = new HashMap<String, String>();

    @Override
    public boolean applicationExists(String name) {
        return DataBaseManager.applicationExists(name);
    }

    @Override
    public boolean deleteApplication(String name) {
        parameters.clear();
        parameters.put("name", name);
        List apps = DataBaseManager.namedQuery("Application.findByName", parameters);
        try {
            DataBaseManager.deleteApplication((Application) apps.get(0));
        } catch (NonexistentEntityException ex) {
            Exceptions.printStackTrace(ex);
            return false;
        }
        return true;
    }

    @Override
    public void addApplication(IMarauroaApplication app) {
        if (!applicationExists(app.getName())) {
            DataBaseManager.addApplication(app);
        }
    }
}
This class implements a service with the line :
@ServiceProvider(service = IDataBase.class)
This registers the class in the Lookup and we can retrieve it as follows: 
Lookup.getDefault().lookup(IDataBase.class)
See the Lookup chapter for more details on how Lookup works.
Of course IDataBase needs to be defined in a package that is common between the res of the modules. Create a Module named API and create an interface  in it named IDataBase with the following code:
package simple.marauroa.application.api;

/**
 *
 * @author Javier A. Ortiz Bultron <javier.ortiz.78@gmail.com>
 */
public interface IDataBase {
    /**
     * Check if application exists
     * @param name Application name
     * @return true if it exists, false otherwise.
     */
    public boolean applicationExists(String name);
    
    /**
     * Delete application from database
     * @param name Application name
     * @return true if it was removed, false otherwise.
     */
    public boolean deleteApplication(String name);

    /**
     * Adds IMarauroaApplication to database
     * @param newInstance 
     */
    public void addApplication(IMarauroaApplication newInstance);
}

Make sure that the DB Layer module has the API module as a library. In this way we reached decoupling of modules while providing all the interaction we need. Now let's go back to DataBaseManager and its loadApplications() method. This method is really important as it initializes the Lookup with the objects that will be consumed by the other modules. It does two things: Looks for folders in a specific folder to recreate objects from the file system. Looks in the Database for objects defined that are not in the File system DataBaseManager.getApplications() retrieves all applications defined in the database and the expected path in the file system they should have. If the path doesn't exist it prompts the user using the Dialog API for the action to take. The user is presented with two options:

  1. Recreate the object from the database
  2. Delete the object from the database

Deletion is straight forward. The recreation option calls a dialog reached by using the Lookup and finding the service provider:

IAddApplicationDialogProvider dialogProvider = Lookup.getDefault().lookup(IAddApplicationDialogProvider.class);
JDialog dialog = dialogProvider.getDialog();

Like we did with the database, we have an interface named IAddApplicationDialogProvider:

package simple.marauroa.application.api;

import javax.swing.JDialog;

/**
 *
 * @author Javier A. Ortiz Bultrón <javier.ortiz.78@gmail.com>
 */
public interface IAddApplicationDialogProvider {

    /*
     * Get the dialog actually shown to the user
     */

    public JDialog getDialog();

    /*
     * Pre-set the application name
     */
    public void setApplicationName(String name);

    /*
     * Set the field editable or not
     */
    public void setEditableApplicationName(boolean enabled);

    /*
     * Ignore folder creation. Used when trying to recover from file system.
     */
    public void setIgnoreFolderCreation(boolean ignore);
    
    /*
     * The value previously set. Should be false by default.
     */
    public boolean isFolderCreationIgnored();
}

And an implementation we'll see in the GUI module next.

GUI Module

Let's show the implementation of the IAddApplicationDialogProvider from the last section.

package simple.marauroa.application.gui.dialog;

import javax.swing.JDialog;
import javax.swing.JFrame;
import org.openide.util.lookup.ServiceProvider;
import simple.marauroa.application.api.IAddApplicationDialogProvider;

/**
 *
 * @author Javier A. Ortiz Bultrón <javier.ortiz.78@gmail.com>
 */
@ServiceProvider(service = IAddApplicationDialogProvider.class)
public class AddApplicationDialogProvider implements IAddApplicationDialogProvider {

    private AddApplicationDialog dialog = null;
    boolean ignoreFolderCreation = false;

    @Override
    public void setApplicationName(String name) {
        if (dialog != null) {
            dialog.appName.setText(name);
        }
    }

    @Override
    public void setEditableApplicationName(boolean enabled) {
        if (dialog != null) {
            dialog.appName.setEditable(enabled);
        }
    }

    @Override
    public JDialog getDialog() {
        if (dialog == null) {
            dialog = new AddApplicationDialog(new JFrame());
        }
        return dialog;
    }

    @Override
    public void setIgnoreFolderCreation(boolean ignore) {
        ignoreFolderCreation = ignore;
    }

    @Override
    public boolean isFolderCreationIgnored() {
        return ignoreFolderCreation;
    }
}

Nothing fancy, just complying with the interface we defined earlier. AddApplicationDialog is a plain JDialog created in the IDE. Again nothing fancy, just a JList, a JText, two buttons and some labels. The interesting part is how the dialog gets the contents of the list. We initialize the dialog overriding its setVisible method like this:

@Override
    public void setVisible(boolean visible) {
        //Refresh each time we are shown
        if (visible) {
            final ArrayList<String> applicationTypes = new ArrayList<String>();
            for (IMarauroaApplicationProvider app : Lookup.getDefault().lookupAll(IMarauroaApplicationProvider.class)) {
                //Add them to the options
                if (!applicationTypes.contains(app.getTemplate().toStringForDisplay())) {
                    applicationTypes.add(app.getTemplate().toStringForDisplay());
                    applications.add(app.getTemplate());
                    Logger.getLogger(AddApplicationDialog.class.getSimpleName()).log(Level.FINE,
                            "Adding: {0} to the list of available "
                            + "applications", app.getTemplate().toStringForDisplay());
                }
            }
            options.setModel(new javax.swing.AbstractListModel() {

                final Object[] objects = MarauroaApplicationRepository.getIMarauroaApplications().toArray();

                @Override
                public int getSize() {
                    return applicationTypes.size();
                }

                @Override
                public Object getElementAt(int i) {
                    return applicationTypes.get(i);
                }
            });
        } else {
            //Clear any potential changes done via the IAddApplicationDialogProvider interface
            appName.setEditable(true);
            appName.setText("");
            Lookup.getDefault().lookup(IAddApplicationDialogProvider.class).setIgnoreFolderCreation(false);
        }
        super.setVisible(visible);
    }

We look in the Lookup for any classes implementing ImarauroaApplicationProvider.class and adding it to the list's model for display. As you might now by now this was defined in the API module with an implementation. Here's the code for the interface:

package simple.marauroa.application.api;

/**
 *
 * @author Javier A. Ortiz Bultrón <javier.ortiz.78@gmail.com>
 */
public interface IMarauroaApplicationProvider {

    public void setTemplate(IMarauroaApplication template);

    public IMarauroaApplication getTemplate();
}
Just provides a way to retrieve the IMarauroaApplication object defined by the provider.   Here's the code of the default provider:
package simple.marauroa.application.core;

import org.openide.util.lookup.ServiceProvider;
import simple.marauroa.application.api.IMarauroaApplication;
import simple.marauroa.application.api.IMarauroaApplicationProvider;

/**
 *
 * @author Javier A. Ortiz Bultrón <javier.ortiz.78@gmail.com>
 */
@ServiceProvider(service=IMarauroaApplicationProvider.class)
public class DefaultMarauroaProvider implements IMarauroaApplicationProvider {

    protected IMarauroaApplication template = new DefaultMarauroaApplication();

    @Override
    public IMarauroaApplication getTemplate() {
        return template;
    }

    @Override
    public void setTemplate(IMarauroaApplication template) {
        this.template = template;
    }
}

And here the default IMarauroaApplication it defines:

package simple.marauroa.application.core;

import java.awt.Image;

/**
 *
 * @author Javier A. Ortiz Bultrón <javier.ortiz.78@gmail.com>
 */
public class DefaultMarauroaApplication extends MarauroaApplication {

    public DefaultMarauroaApplication() {
    }

    public DefaultMarauroaApplication(String name) {
        super(name);
        setVersion("1.0");
    }

    @Override
    public Image getIcon(int type) {
        //Use default icon
        return null;
    }
}

As you can see this class extends MarauroaApplication, an abstract class containing lots of methods inherited by others. Is almost 1000 lines of code at the time I'm writing this chapter. Take a look at the source code for more details. Next lets build the Explorer that will display our applications. From the IDE create a Top Component following the Wizard. Make sure to place it on the “explorer” location. Switch to the code and modify the code to so the class implements ExplorerManager.Provider and EventBusListener as follows:

public final class ApplicationExplorerTopComponent extends TopComponent
        implements ExplorerManager.Provider, EventBusListener<IMarauroaApplication> {

You might be missing EventBusListener and other classes that we'll use in this part of the chapter. CentralLookup (http://blogs.sun.com/geertjan/entry/central_lookup) and the EventBus (http://netbeans.dzone.com/news/publish-subscribe-netbeans-pla) are used with some modifications in the code. Look at the sources within the Core module.

Core module

Define a global variable:

private final ExplorerManager explorerManager = new ExplorerManager();

And a method to comply with ExplorerManager.Provider interface:,pre> @Override

   public ExplorerManager getExplorerManager() {
       return explorerManager;
   }

</pre> Switch to the design view and make sure to set the Layout to BorderLayout. Switch back to the source afterward. Back in the constructor of the component add the following after the initComponents call:

add (new BeanTreeView(), BorderLayout.CENTER);
        putClientProperty(TopComponent.PROP_CLOSING_DISABLED, Boolean.TRUE);
        associateLookup(ExplorerUtils.createLookup(getExplorerManager(), getActionMap()));
        getExplorerManager().setRootContext(new RootNode(new MarauroaAppChildFactory()));
        getExplorerManager().getRootContext().setDisplayName("Registered Applications");

After that there will be two undefined classes: RootNode and MarauroaAppChildFactory. Let's discuss those. Like any tree, the explorer is composed of nodes. In NetBeans those are objects of the type AbstractNode or any class that extends those. If you need more information check the Getting ready section of this chapter. RootNode is a class that extends AbstractNode. It's only purpose is to represent the tree's root node. Adding actions to a node is really easy. Just override the node's getActions method returning an array of AbstractActions. For example:

@Override
    public Action[] getActions(boolean popup) {
        return new Action[]{new RootNodeAction()};
    }

Unlike Trees where you can add children to a node directly, the nodes in NetBeans need to b able to generate its children on their own. There's where the ChildFactory comes into play. ChildFactory is an abstract class that can generate its own children. MarauroaAppChildFactory is a ChildFactory for MarauroaApplications. Here's the code for it:

package simple.marauroa.application.gui;

import java.beans.IntrospectionException;
import java.util.List;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import simple.marauroa.application.api.IMarauroaApplication;
import simple.marauroa.application.core.MarauroaApplicationRepository;

/**
 *
 * @author Javier A. Ortiz Bultrón <javier.ortiz.78@gmail.com>
 */
public class MarauroaAppChildFactory extends ChildFactory<IMarauroaApplication> {

    @Override
    protected boolean createKeys(List<IMarauroaApplication> toPopulate) {
        toPopulate.addAll(MarauroaApplicationRepository.getIMarauroaApplications());
        return true;
    }

    @Override
    protected Node createNodeForKey(IMarauroaApplication app) {
        try {
            return new MarauroaApplicationNode(app);
        } catch (IntrospectionException ex) {
            Exceptions.printStackTrace(ex);
            return null;
        }
    }

    @Override
    protected Node[] createNodesForKey(IMarauroaApplication key) {
        return new Node[]{createNodeForKey(key)};
    }

    public void refresh() {
        refresh(true);
    }
}

It only has four methods:

  1. createKeys: Create a list of keys which can be individually passed to createNodes() to create child Nodes.
  2. createNodeForKey: Create a Node for a given key that was put into the list passed into createKeys().
  3. createNodesForKey: Create Nodes for a given key object (one from the List passed to createKeys(List ))
  4. refresh: Call this method when the list of objects being modeled by the has changed and the child Nodes of this Node should be updated.

See: http://bits.netbeans.org/dev/javadoc/org-openide-nodes/org/openide/nodes/ChildFactory.html for more details. Now let's take a look at MarauroaApplicationNode:

public class MarauroaApplicationNode extends BeanNode

Our class extends BeanNode. From the javadoc: “Represents a JavaBeans component as a node. You may use this node type for an already-existing JavaBean (possibly using BeanContext) in order for its JavaBean properties to be reflected as corresponding node properties. Thus, it serves as a compatibility wrapper. The bean passed in the constructor will be available in the node's lookup, though not directly. Instead, the node's Lookup will contain an InstanceCookie from which you can retrieve the bean instance.” http://bits.netbeans.org/dev/javadoc/org-openide-nodes/org/openide/nodes/BeanNode.html Normal nodes need to explicitly expose the node's properties so they can be viewed or edited in a Properties window. BeanNode allows to do that automatically if a Bean is provided. The IDE provide wizards for doing that. That will be covered on the Miscellaneous chapter. Let's analyze its constructor next:

public MarauroaApplicationNode(IMarauroaApplication application) throws IntrospectionException {
        super(application, Children.create(new RPZoneChildFactory(application.getName()), true), 
                Lookups.singleton(application));
        setDisplayName(application.toStringForDisplay());
    }

RPZoneChildFactory is the ChildFactory for the Nodes representation of the Zones within each server. SetDisplayName sets the label displayed for the node in the explorer.

@Override
    public Image getIcon(int type) {
        Image icon = getMarauroaApplication().getIcon(type);
        if (icon == null) {
            icon = ImageUtilities.loadImage(
                    "simple/marauroa/application/gui/resource/app.png");
        }
        return icon;
    }

The method above allows us to provide an icon to represent the node. 16 x 16 is the correct size for those. For simplicity we're returning the same icon always but you can vary the icon depending on the type specified, which can have the values specified by the BeanInfo (http://download.oracle.com/javase/6/docs/api/java/beans/BeanInfo.html?is-external=true)

@Override
    public Image getOpenedIcon(int i) {
        return getIcon(i);
    }

The method above allows to specify the icon to be displayed when the node is opened . Again, we're returning the same icon for making things easier. Basically the same approach was taken for the other nodes: RPZoneNode and RPObjectNode. See the code for reference. Run the project and right click on the RootNode. It'll show the option to add an application. Select the only option and type in a name. Press ok. After a while you should see something like this: File:5863-06-3.png

Now for the final piece lets display this using the Visual Library API. Let's create another TopComponent. This time select the editor location for it. Modify it so it implements the LookupListener interface. Implement it's resultChanged method as follows:

@Override
    public void resultChanged(LookupEvent ev) {
        Lookup.Result r = (Lookup.Result) ev.getSource();
        Collection c = r.allInstances();
        //Repopulate
        if (!c.isEmpty()) {
            //Reset
            scene.clear();
            try { 
                //Draw the selected one
                scene.addMarauroaApplication((IMarauroaApplication) c.iterator().next());
            } catch (Exception ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

See the source and the Visual API chapter for more details. File:5863-06-4.png

Let's Explain!

The Lookup contains the objects the other modules need to work. Selections in the explorer's tree trigger the Lookup, thanks to the Explorer API, making all listeners aware of the current selection making it really easy to display the selected object on its different views. Note: The code mentioned in this chapter is part of a real application. You can track it's development and new functionality here: https://sourceforge.net/projects/simple-marauroa/ /pre

Additional Material

Weak Listening of Node Expansion.

This code solved an issue I had in which I needed to listen for the expansion/collapse of notes on my BeanTreeView. It is based on code from here.

Create an interface:

public interface NodeExpansion {
    void nodeExpanded();
    void nodeCollapsed();
}

Add to your nodes:

public abstract class AbstractVMBeanNode extends BeanNode
        implements RefreshableCapability {

    private AbstractChildFactory factory;

    public AbstractVMBeanNode(Object bean,
            AbstractChildFactory factory, InstanceContent content) throws IntrospectionException {
        super(bean, factory == null ? Children.LEAF
                : Children.create(factory, true), new AbstractLookup(content));
        this.factory = factory;
        //Add abilities
        content.add(new NodeExpansion() {

            @Override
            public void nodeExpanded() {
                System.out.println(getName()+" node expanded!");
            }

            @Override
            public void nodeCollapsed() {
                System.out.println(getName()+" node collapsed!");
            }
        });
        content.add(bean);
    }
}

Extend the BeanTreeView:

/*
 * Based on code from: https://netbeans.org/projects/platform/lists/dev/archive/2013-09/message/90
 */
package net.sourceforge.javydreamercsw.client.ui.components.project.explorer;

import net.sourceforge.javydreamercsw.client.ui.nodes.NodeExpansionListener;
import org.openide.explorer.view.BeanTreeView;

/**
 *
 * @author Javier A. Ortiz Bultron <javier.ortiz.78@gmail.com>
 */
public class CustomBeanTreeView extends BeanTreeView {

    private final NodeExpansionListener nodeExpansionListener
            = new NodeExpansionListener();

    @Override
    public void addNotify() {
        super.addNotify();
        tree.addTreeExpansionListener(nodeExpansionListener);
    }

    @Override
    public void removeNotify() {
        super.removeNotify();
        tree.removeTreeExpansionListener(nodeExpansionListener);
    }
}

Navigation

Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo