Geertjan is a DZone Zone Leader and has posted 467 posts at DZone. You can read more from them at their website. View Full User Profile

Loosely Coupled Reloadable Capabilities for CRUD Applications

04.29.2011
| 20902 views |
  • submit to reddit

Antonio has an interesting series entitled NetBeans Platform on Standalone Swing Applications. I've been following especially the parts relating to the "reloadable" capabilities, since I think the area of "capabilities" is one of the most powerful yet least understood parts of the NetBeans APIs.

So, below I reproduce a small part of Antonio's Tweet scenario, but converted to a database scenario. I still have open questions about all of this, but here's a start for discussion anyway.

  1. Begin by creating a new NetBeans Platform application named "CustomerCRUD" or whatever else you'd like to call it..

  2. Generate a Customer entity (with a related Discount entity) via wizards in the IDE, as described in the first parts of the NetBeans CRUD Application Tutorial. Then you will have a JAR containing the Customer.class, Discount.class, and the persistence.xml file. Put that JAR, together with the database server client JAR (e.g., DerbyClient.jar) and the JPA-related JARs (e.g., EclipseLink JARs) into a library wrapper module (named "CustomerLibraries", for example), all of which is described in the tutorial above. You now have one module, a library wrapper module, that contains about 4 different JARs.

  3. Next, create a second module (named "CustomerAPI", for example), which will contain your API classes, together with the capabilities that should be made available to other modules. (Or maybe the capabilities should be in a separate "CustomerUtilities" module.) In this module, we will have four classes, all described in the next step.

  4. We start by creating capabilities that we are going to be reusing throughout our application in a loosely coupled way. First of all, we'll create a capability for reloading entities, followed by a capability for reloading our view:
    public interface ReloadableQueryCapability {
    public void reload() throws Exception;
    }
    public interface ReloadableViewCapability {
    public void reloadChildren() throws Exception;
    }

    Next, we create a data access object that will access the database, via a SQL string passed into it:

    import client.Customer;
    import java.util.ArrayList;
    import java.util.List;
    import javax.persistence.EntityManager;
    import javax.persistence.Persistence;

    public final class CustomerSearchDAO {

    public List<Customer> search(String search) {
    EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerPU").createEntityManager();
    javax.persistence.Query query = entityManager.createQuery(search);
    List<Customer> customers = new ArrayList<Customer>();
    List<Customer> resultList = query.getResultList();
    for (Customer c : resultList) {
    customers.add(c);
    }
    return customers;
    }

    }

    And we finish our API module by creating a query object to which we'd like to assign the capability for it to be reloaded. Therefore, we need to implement the Lookup.Provider class and return a Lookup that contains an implementation of the ReloadableQueryCapability:

    import client.Customer;
    import java.util.ArrayList;
    import java.util.List;
    import org.netbeans.api.progress.ProgressHandle;
    import org.netbeans.api.progress.ProgressHandleFactory;
    import org.openide.util.Lookup;
    import org.openide.util.lookup.AbstractLookup;
    import org.openide.util.lookup.InstanceContent;

    public final class CustomerQuery implements Lookup.Provider {

    private List<Customer> customers;
    private Lookup lookup;
    private InstanceContent instanceContent;
    private String sqlstring;

    public CustomerQuery() {
    customers = new ArrayList<Customer>();
    // Create an InstanceContent to hold abilities...
    instanceContent = new InstanceContent();
    // Create an AbstractLookup to expose InstanceContent contents...
    lookup = new AbstractLookup(instanceContent);
    // Add a "Reloadable" ability to this entity
    instanceContent.add(new ReloadableQueryCapability() {
    @Override
    public void reload() throws Exception {
    ProgressHandle handle = ProgressHandleFactory.createHandle("Loading...");
    handle.start();
    CustomerSearchDAO dao = new CustomerSearchDAO();
    List<Customer> result = dao.search(sqlstring);
    for (Customer customer : result) {
    if (!getCustomers().contains(customer)) {
    getCustomers().add(customer);
    }
    }
    handle.finish();
    }
    });
    }

    public String getSqlstring() {
    return sqlstring;
    }

    public void setSqlstring(String sqlstring) {
    this.sqlstring = sqlstring;
    }

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

    @Override
    public Lookup getLookup() {
    return lookup;
    }

    public List<Customer> getCustomers() {
    return customers;
    }

    }

    Our API module is now complete. It contains database access code, together with reload capabilities, one of which we're already using in our query object to enable it to be reloaded.


  5. Create a third module ("CustomerViewer", for example), where everything described in this step will be created. First, we create a new Action that will receive a Lookup. In the Lookup we look for the ReloadableViewCapability and, if it is there, we call the implementation's "reloadChildren" method. What that does we don't know, that depends on the implementation we get from the Lookup that is passed in.
    import java.awt.event.ActionEvent;
    import javax.swing.AbstractAction;
    import org.my.api.ReloadableViewCapability;
    import org.openide.util.Exceptions;
    import org.openide.util.Lookup;

    public final class ReloadAction extends AbstractAction {

    private ReloadableViewCapability reloadableViewCapability;

    public ReloadAction(Lookup lookup) {
    reloadableViewCapability = lookup.lookup(ReloadableViewCapability.class);
    putValue(AbstractAction.NAME, "Reload");
    }

    @Override
    public void actionPerformed(ActionEvent e) {
    if (reloadableViewCapability != null) {
    try {
    reloadableViewCapability.reloadChildren();
    } catch (Exception ex) {
    Exceptions.printStackTrace(ex);
    }
    }
    }

    }

    Next, in the same module, we create a factory class which we will, later, attach to the root node of our view. The factory class runs the query, retrieved from the Lookup of the query, and now the call to "getCustomers" on the query contains the current state of the database.

    import client.Customer;
    import java.util.List;
    import org.my.api.CustomerQuery;
    import org.my.api.ReloadableQueryCapability;
    import org.openide.nodes.AbstractNode;
    import org.openide.nodes.ChildFactory;
    import org.openide.nodes.Children;
    import org.openide.nodes.Node;

    class RootNodeChildFactory extends ChildFactory<Customer> {

    private CustomerQuery query;

    public RootNodeChildFactory(CustomerQuery query) {
    this.query = query;
    }

    @Override
    protected boolean createKeys(List<Customer> list) {
    // get this ability from the lookup ...
    ReloadableQueryCapability r = query.getLookup().lookup(ReloadableQueryCapability.class);
    // ... and use the ability
    if (r != null) {
    try {
    r.reload();
    } catch (Exception e) {
    // Empty
    }
    }
    // Now populate the list of child entities...
    list.addAll(query.getCustomers());
    // And return true since we're all set
    return true;
    }

    @Override
    protected Node createNodeForKey(Customer key) {
    Node customernNode = new AbstractNode(Children.LEAF);
    customernNode.setDisplayName(key.getName());
    return customernNode;
    }

    }

    Finally, we create our root node. This is where we hook in our ReloadAction, as well as the RootNodeChildFactory:

    import javax.swing.Action;
    import org.my.api.CustomerQuery;
    import org.my.api.ReloadableViewCapability;
    import org.openide.nodes.AbstractNode;
    import org.openide.nodes.Children;
    import org.openide.util.lookup.AbstractLookup;
    import org.openide.util.lookup.InstanceContent;

    public final class RootNode extends AbstractNode {

    private CustomerQuery query;
    private InstanceContent instanceContent;

    public RootNode(CustomerQuery query) {
    this(query, new InstanceContent());
    }

    private RootNode(CustomerQuery query, InstanceContent ic) {
    super(Children.create(new RootNodeChildFactory(query), true), new AbstractLookup(ic));
    this.query = query;
    this.instanceContent = ic;
    // Add a new ability for this node to be reloaded
    this.instanceContent.add(new ReloadableViewCapability() {
    @Override
    public void reloadChildren() throws Exception {
    // To reload this node just set a new set of children
    // using a RootNodeChildFactory object, that retrieves
    // children asynchronously
    setChildren(Children.create(new RootNodeChildFactory(RootNode.this.query), false));
    }
    });
    }

    @Override
    public String getDisplayName() {
    return "Query: " + query.getSqlstring();
    }

    @Override
    public Action[] getActions(boolean context) {
    // Pass the Lookup, which now contains ReloadableViewCapability, to the Action:
    return new Action[]{new ReloadAction(getLookup())};
    }

    }

    As usual, create a new TopComponent. In it, create a new query object, populate the root node (which creates the children), using ExplorerManager.Provider and BeanTreeView:

    CustomerQuery query = new CustomerQuery();
    query.setSqlstring("SELECT c FROM Customer c");

    RootNode node = new RootNode(query);
    em.setRootContext(node);

    setLayout(new BorderLayout());
    add(new BeanTreeView(), BorderLayout.CENTER);

    associateLookup(ExplorerUtils.createLookup(em, getActionMap()));

What you have done above is create a small demo application (a small subset of Antonio's application) using reload capabilities that are added to the Lookup of objects (the query and the root node) and then retrieved from it where needed.

Next, once the above has been commented on by Antonio and others, I'd like to see how the above works in a SaveCookie in a foreign module.

Note: Continue with part 2 (on save capability) and part 3 (on create capability).

 

Published at DZone with permission of its author, Geertjan Wielenga.

Comments

Antonio Vieiro replied on Fri, 2011/04/29 - 5:35am

Hi,

 Looks great. What about creating a CustomerEntity that has some "insert()", "update()" and "delete()" abilities? ;-)

Cheers,

Antonio

 P.D.: I'll have to udpate the artitle with some JPA examples too!

 

 

Geertjan Wielenga replied on Fri, 2011/04/29 - 5:45am

A better ReloadAction would be like this:

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import org.my.api.ReloadableViewCapability;

import org.openide.awt.ActionRegistration;
import org.openide.awt.ActionID;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle.Messages;

@ActionID(category = "Customer", id = "org.my.viewer.ReloadableViewAction")
@ActionRegistration(displayName = "#CTL_ReloadableViewAction")
@Messages("CTL_ReloadableViewAction=Reload")
public final class ReloadAction implements ActionListener {

private final ReloadableViewCapability context;

public ReloadAction(ReloadableViewCapability context) {
this.context = context;
}

@Override
public void actionPerformed(ActionEvent ev) {
try {
context.reloadChildren();
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}

}

And, in the RootNode:

@Override
public Action[] getActions(boolean context) {
List customerActions = Utilities.actionsForPath("Actions/Customer");
return customerActions.toArray(new Action[customerActions.size()]);
}

 

Toni Epple replied on Mon, 2011/05/02 - 11:02am

Hi Geertjan,

great tutorial! 

Right now the two main benefits of you implementation are that you're shielding the UI from the implementation details (JPA), and you can add additional capabilities without breaking backward compatibility by using Composition instead of inheritance.

 Maybe you could enhance it by further decoupling:

If I understand it correctly, there's no way to declaratively swap implementations, because RootNode knows the CustomerQuery class, which in turn knows DAO, which knows the PU and everything else, right? So in the end the Ui is still coupled to the implementation.

You could place Customer and Discount in a separate Module from the Derby.jar, so they can be reused without JPA. CustomerQuery could implement an SPI (ICustomerQuery) and register itself as a service implementing the interface in default Lookup. Then RootNode can retrieve the registered Service from the Lookup to CRUD the DataObjects (Customer, Discount) and you could easily replace "JPADerbyCustomerQuery" with a different impl (e.g. based on serializing to files).

 Cheers,

Toni

 

 

Matthew Russell replied on Tue, 2011/09/27 - 4:15am

Great article!

I have quick question though...

Unless I'm understanding the code incorrectly, the reload action minimizes the children nodes underneath the root node. I've worked around this by creating a new action "expand" that fires a function call to the viewer to expand the root node. Is there a better way to auto expand the root node on a refresh?

Matthew Russell replied on Wed, 2011/09/28 - 6:11am in response to: Matthew Russell

found a better work around: http://www.vcskicks.com/treeview-state.php

Carlos Delgado replied on Fri, 2011/10/14 - 4:37am in response to: Geertjan Wielenga

Hello Geertjan,

I changed my code to retrieve the available actions for a particular path, as you mention above, but the actions appear disabled.

Any idea why?

Thanks, and also thanks for your tutorials, great stuff!

 Carlos

Matt Coleman replied on Fri, 2013/01/11 - 1:04am

this tutorial will help a lot of programmers..i am sure of it

buffalo search engine optimization 

Danish12 Ali replied on Wed, 2013/08/21 - 2:40am

 You will find different kinds of travelling approaches one too which may become intriguing will be the highway, generally we use cars just for this especially my buddy southee that's surviving in On the internet services wants exploring throughout car.  Thomson Three

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.