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

Loosely Coupled Creatable Capabilities for CRUD Applications

05.01.2011
| 9584 views |
  • submit to reddit

In part 1 you were introduced to the concept of loosely coupled reloadable capabilities, which were based on work done by Antonio, in his NetBeans Platform kitchen. An extension of that concept was shown in part 2, where the SaveableEntityCapability was introduced, which is a companion to the NetBeans SaveCookie, in the same way as ReloadableEntityCapability accompanied ReloadableViewCapability in part 1.

Today, let's look at how the "C" in CRUD (i.e., "create") can be handled via the "capabilities approach". We're going to end up with a "Create" capability in our application, which will be available depending on the current selection. In the first screenshot, you see the root node selected, hence the New action is enabled and can be invoked from the toolbar (the "New" is represented by the yellow icon on the left of the toolbar below), while the Delete and the Save are disabled because when the root node is selected there's nothing that makes sense to delete or save:

Note: As you can see, I am now using a different database to yesterday. Rather than EclipseLink, Derby, and the NetBeans Derby Sample database, I am now using EclipseLink, MySQL, and the Trip table from the Sakila database. The reason for the change is that I encountered several problems on the JPA level with the NetBeans Derby Sample database, when deleting records, which is something I'd like to resolve sometime. However, the point of this series of articles is to learn about how to use capabilities in NetBeans Platform applications, which is a separate topic to JPA, hence I'll leave those details for another time.

In the next screenshot, you can see a child node selected, which means that in addition to New action remaining enabled (users would expect to create new trips anywhere in the application, regardless of selection), the Delete action is now also enabled because each trip can be deleted, either individually or by means of a multi-select:

Finally, in the screenshot below, the editor component is selected, and so now the Delete action is disabled (since that action is only relevant when a child node is selected), while the New action is, again, enabled (since, again, the user will expect to be able to create new trips irrespective of the current selection), while the Save button is now enabled, since the cursor is in a text field (no actual change has yet been made, but here I'm assuming the Save action should be available whenever the cursor is in the text field, regardless of changes having actually been made):

So, the basis of the above context-sensitive enablement based on selection is a set of "capabilities", which you can see here, in the current state of the application:

Above, the only new capability is "CreatableEntityCapability" (and "RemoveableEntityCapability", which we will deal with next time), which, just like the other XXXEntityCapabilities, works on the model. On the view, we do not need to write a "CreatableViewCapability", just like we did not need to create a "SaveableViewCapability", because the NetBeans Platform provides built-in support for this view capability. In other words, in the same way that we plugged our view changes for saving into the SaveCookie implementation yesterday, we will now plug into the NewType class today.

The process for building up our capability is the same as before. We start by adding a "create" method to our DAO, we define a new capability for creating entities, then we add an implementation of that new capability to the Lookup of the query object, after which we use the "NewType" object to retrieve the new capability from the query object's Lookup, which lets us enable the "NewAction" which, when the user clicks it, invokes the "New" dialogs from the NetBeans APIs, followed by an invocation of the entity's implementations of the reloadable capabilities to update the view.

The above paragraph summarizes everything that follows.

  1. Extend the DAO. It's by no means perfect, but here's the current state of my DAO. As pointed out above, the purpose of this series is not to create perfect JPA code, but just to get something basic working so that the capabilities can be built on top of these:
    import client.Person;
    import client.Trip;
    import client.Triptype;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.Random;
    import javax.persistence.EntityManager;
    import javax.persistence.Persistence;
    import javax.persistence.Query;

    public final class TripSearchDAO {

    public List<Trip> search() {
    createTransactionalEntityManager();
    List<Trip> trips = new ArrayList<Trip>();
    List<Trip> resultList = query.getResultList();
    for (Trip c : resultList) {
    trips.add(c);
    }
    return trips;
    }

    public void save(Trip trip) {
    createTransactionalEntityManager();
    em.merge(trip);
    closeTransactionalEntityManager();
    }

    public void create(Trip trip) {
    createTransactionalEntityManager();
    //Create some random content
    //for the foreign key objects:
    Random generator = new Random();
    trip.setPersonid(new Person(generator.nextInt()));
    Triptype tt = new Triptype();
    tt.setDescription("Bla");
    tt.setTriptypeid(generator.nextInt());
    tt.setName("Bla bla");
    tt.setLastupdated(new Date());
    trip.setTriptypeid(tt);
    trip.setTripid(generator.nextInt());
    //Then persist the trip:
    em.persist(trip);
    closeTransactionalEntityManager();
    }

    private EntityManager em;
    private Query query;

    private void createTransactionalEntityManager() {
    em = Persistence.createEntityManagerFactory("TripPU").createEntityManager();
    query = em.createQuery("SELECT t FROM Trip t");
    em.getTransaction().begin();
    }

    private void closeTransactionalEntityManager() {
    em.getTransaction().commit();
    em.close();
    }

    }

  2. Define the CreatableEnityCapability. So, now we create a new class that defines a capability for creating new trips:
    import client.Trip;

    public interface CreatableEntityCapability {

    public void create(Trip trip) throws Exception;

    }

  3. Implement the capability. Next, in the query object, we implement the capability and, very importantly, we put it into the Lookup of the query object, so that it can be retrieved from there later. Below you see exactly the same class as in the previous two parts (though Trips are used here instead of Customers), with the addition of the new CreatableEntityCapability:
    import org.my.api.capabilities.ReloadableEntityCapability;
    import org.my.api.capabilities.SaveableEntityCapability;
    import org.my.api.capabilities.CreatableEntityCapability;
    import client.Trip;
    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 TripQuery implements Lookup.Provider {

    private List<Trip> trips;
    private Lookup lookup;
    private InstanceContent instanceContent;
    private TripSearchDAO dao = new TripSearchDAO();

    public TripQuery() {
    trips = new ArrayList<Trip>();
    // 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 ReloadableEntityCapability() {
    @Override
    public void reload() throws Exception {
    ProgressHandle handle = ProgressHandleFactory.createHandle("Loading...");
    handle.start();
    List<Trip> result = dao.search();
    for (Trip trip : result) {
    if (!getTrips().contains(trip)) {
    getTrips().add(trip);
    }
    }
    handle.finish();
    }
    });
    // ...and a "Saveable" ability to this entity...
    instanceContent.add(new SaveableEntityCapability() {
    @Override
    public void save(Trip trip) throws Exception {
    dao.save(trip);
    }
    });
    // ...and a "Creatable" ability to this entity:
    instanceContent.add(new CreatableEntityCapability() {
    @Override
    public void create(Trip trip) throws Exception {
    dao.create(trip);
    }
    });
    }

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

    public List<Trip> getTrips() {
    return trips;
    }

    }

  4. Register the NewAction. The NetBeans Platform provides a "NewAction" class out of the box, which is enabled when a "NewType" object is available in the Lookup. Read this entry in my blog, as well as the NetBeans System Properties Module Tutorial for details on this topic.

    Let's imagine that the "NewAction", i.e., an Action that exists in the NetBeans Platform, needs to be invoked from the toolbar, as shown in the screenshots earlier. In addition, we'd like the "NewAction" to be available when the user right-clicks on the root node. To that end, create a new module, named "MyBranding" and make sure to include a layer.xml file when you create this new module. In that layer.xml file, register the Action as follows:

    <folder name="Actions">
    <folder name="RootTrip">
    <file name="org-openide-actions-NewAction.instance">
    <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
    <attr name="delegate" newvalue="org.openide.actions.NewAction"/>
    <attr name="type" stringvalue="org.openide.util.datatransfer.NewType"/>
    <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
    <attr name="displayName" stringvalue="New Trip"/>
    <attr name="iconBase" stringvalue="org/my/branding/record-new.png"/>
    <attr name="noIconInMenu" boolvalue="false"/>
    </file>
    </folder>
    </folder>

    Now the "NewAction" is registered in Actions/RootTrip and notice that it will become enabled when "NewType" is in the Lookup. That's exactly what the "NewAction" needs, if you look at the source code of "NewAction".

    Next, we'd like to display the above Action in the toolbar. In the same layer.xml file, add the following, which creates a shadow file that refers to the Action registration above, enabling the Action to be invoked from the toolbar:

    <folder name="Toolbars">
    <folder name="File">
    <file name="org-openide-actions-NewAction.shadow">
    <attr name="originalFile" stringvalue="Actions/RootTrip/org-openide-actions-NewAction.instance"/>
    <attr name="position" intvalue="10"/>
    </file>
    </folder>
    </folder>

    Then add it to the context menu of the root node, like this in the RootNode:

    @Override
    public Action[] getActions(boolean context) {
    List<? extends Action> tripActions = Utilities.actionsForPath("Actions/RootTrip");
    return tripActions.toArray(new Action[tripActions.size()]);
    }
  5. Implement the NewType. Now we're going to look at the "NewType" object. An implementation of this NetBeas API class needs to be in the Lookup otherwise the "NewAction" will not be enabled. Now, we want the "NewAction" to be enabled in three different scenarios, as described at the start of this article. However, in all cases we need to have a Node available, because only a Node can have a "NewType" (via overriding the Node's "getNewTypes" method). Hence, we have two different Nodes to be concerned about. Therefore, we'll create a "NewType" that is relevant for both our Nodes, in the "MyAPI" module, so that all modules can have access to it:
    import org.openide.nodes.Node;
    import client.Trip;
    import java.io.IOException;
    import org.my.api.capabilities.CreatableEntityCapability;
    import org.my.api.capabilities.ReloadableEntityCapability;
    import org.my.api.capabilities.ReloadableViewCapability;
    import org.openide.DialogDisplayer;
    import org.openide.NotifyDescriptor;
    import org.openide.util.Exceptions;
    import org.openide.util.NbBundle.Messages;
    import org.openide.util.datatransfer.NewType;
    import static org.my.api.Bundle.*;

    @Messages({
    "LBL_NewDestination_dialog=Trip Destination:",
    "LBL_NewDeparture_dialog=Trip Departure:",
    "TITLE_NewTrip_dialog=New Trip"})
    public class TripType extends NewType {

    private final TripQuery query;
    private final Node node;
    private final boolean root;

    public TripType(TripQuery query, Node node, boolean root) {
    this.query = query;
    this.node = node;
    this.root = root;
    }

    @Override
    public String getName() {
    return TITLE_NewTrip_dialog();
    }

    @Override
    public void create() throws IOException {
    NotifyDescriptor.InputLine msg = new NotifyDescriptor.InputLine(LBL_NewDeparture_dialog(), TITLE_NewTrip_dialog());
    DialogDisplayer.getDefault().notify(msg);
    String departureCity = msg.getInputText();
    msg = new NotifyDescriptor.InputLine(LBL_NewDestination_dialog(), TITLE_NewTrip_dialog());
    Object result = DialogDisplayer.getDefault().notify(msg);
    String destinationCity = msg.getInputText();
    if (NotifyDescriptor.YES_OPTION.equals(result)) {
    try {
    //Create a new Trip object:
    Trip trip = new Trip();
    trip.setDestcity(destinationCity);
    trip.setDepcity(departureCity);
    //Pass the trip to the query's implementation of the create capability:
    CreatableEntityCapability cec = query.getLookup().lookup(CreatableEntityCapability.class);
    cec.create(trip);
    //Refresh the list of trips via the implementation of the reload capability:
    ReloadableEntityCapability r = query.getLookup().lookup(ReloadableEntityCapability.class);
    r.reload();
    //If the Node passed in is the root node, refresh the root node,
    //else refresh the child node only:
    if (!root) {
    ReloadableViewCapability rvcParent = node.getParentNode().getLookup().lookup(ReloadableViewCapability.class);
    rvcParent.reloadChildren();
    } else {
    ReloadableViewCapability rvc = node.getLookup().lookup(ReloadableViewCapability.class);
    rvc.reloadChildren();
    }
    } catch (Exception ex) {
    Exceptions.printStackTrace(ex);
    }
    }
    }

    }

  6. Put the NewType into the Lookup. Now that we have a "NewType", we need to put it into the Lookup! And we're dealing with two different Nodes, hence two different Lookups. Since we have the class above, in the API module, we can refer to it anywhere we want.

    • Root Node. As a class variable, declare the "NewType":
      private TripType tripType = null;

      In the constructor:

      ...
      ...
      ...
      tripType = new TripType(query, this, true);
      instanceContent.add(tripType);
      ...
      ...
      ...

      As you can see, we create a "TripType", which is our "NewType", and we add it to the Lookup of the RootNode. Finally, in the RootNode, we need to provide the "NewType" via the "getNewTypes" method:

      @Override
      public NewType[] getNewTypes() {
      return new NewType[]{tripType};
      }

      But why do we add the NewType to the Lookup as well as to the "getNewTypes"? Because in the toolbar we need the NewAction's button to be enabled when the RootNode is selected, which will only happen when the NewType is in the Lookup of the RootNode. 

    • Child Node. Do the same as the above in the child node, i.e., "TripNode" in the case of this specific example.

    • Editor TopComponent. Finally, add a LookupListener to the editor TopComponent and listen for the Node in the currently selected window. Make that Node the TopComponent's activated Node so that the Node's NewType is available to the TopComponent and put the NewType into the Lookup of the editor TopComponent to enable the NewAction's button in the toolbar.


Following the instructions above, you should now have the NewAction working throughout the application, irrespective of which window is selected. Next time, we'll add the DeleteAction to the mix, via a DeletableEntityCapability, following a similar pattern to the above.

 

 

 

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

Comments

Martin Vondráček replied on Sun, 2012/02/05 - 3:34pm

(resolved, I have mislooked for one code block in previous articles; therefore, post deleted)

Martin Vondráček replied on Thu, 2012/02/02 - 8:28am

Hint for others:

If you try this example with different tables and DB, don't forget that persistence unit has name set in presistence.xml, that does not go from the name of the table/entity! And you can change it of course.

Magnus Smith replied on Thu, 2013/01/10 - 11:33am

I followed this through until the last bit

Editor TopComponent. Finally, add a LookupListener to the editor TopComponent and listen for the Node in the currently selected window. Make that Node the TopComponent's activated Node so that the Node's NewType is available to the TopComponent and put the NewType into the Lookup of the editor TopComponent to enable the NewAction's button in the toolbar.

I'm stuck here as to what I should be doing.  

Can someone please elaborate a bit further?

Is the complete code available anywhere to download?


Thanks

Comment viewing options

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