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

Loosely Coupled Deletable Capabilities for CRUD Applications

05.06.2011
| 6352 views |
  • submit to reddit

After C (create), R (read), and U (update), we now arrive at D (delete). As in the previous parts, we're interested in separating the user interface of our application from the data access code. In short, we want to have a situation, in contrast to the NetBeans CRUD Application Tutorial, where the user interface engineers need to know nothing at all about the underlying JPA implementation. Even more than that, they'll not need to know that JPA is being used nor anything at all about the data access layer of the application.

When plugging the "Create" functionality into the application (as explained here), the user interface engineers only need to know about the "CreatableEntityCapability", which will have been added to the "TripQuery" object by the data access engineers (without breaking an interface, since composition rather than inheritance is used) and which they'll therefore be able to implement in the NetBeans Platform's "NewType" implementation. We'll do something very similar with the "Delete" functionality.

  1. Extend the DAO. As before, the focus of this series is not on putting together the nicest DAO imaginable, just something that works so that our capabilities can be demonstrated on top of it. So, below, the DAO is a bit ugly, and suggestions for improvement are welcome. The only difference with the DAO from the previous part is that a "remove(Trip)" method has been added to it, in line 45 - 49:
    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) {
    Random generator = new Random();
    createTransactionalEntityManager();
    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());
    em.persist(trip);
    closeTransactionalEntityManager();
    }

    public void remove(Trip trip) {
    createTransactionalEntityManager();
    em.createQuery("DELETE FROM Trip t WHERE t.tripid = " + trip.getTripid()).executeUpdate();
    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 DeletableEntityCapability. Here's the abstraction on top of the "delete(Trip)" method in our DAO. I.e., this is what we'll be implementing in our TripQuery object and then retrieving in our application's user interface for Deleting trips:
    public interface RemovableEntityCapability {

    public void remove(Trip trip) throws Exception;

    }
  3. Implement the capability. The wonderfulness of the Lookup is now going to be demonstrated yet again. Since we're not using an interface, we do not now need to go to our interface and define a new method, which all the implementations would then need to implement. Instead, we add an implementation of our new capability to the list of capabilities that the TripQuery object provides. That's also nice because the TripQuery object is able to control its own capabilities; i.e., there's nothing from the outside telling the TripQuery object what it can do, instead, it defines its capabilities for itself, which is pretty powerful.

    So, below in line 54 - 60, our implementation of RemovableEntityCapability consists of a call to the JPA "remove(Trip)" method, followed by a removal of the Trip from the list of Trips that is exposed to the rest of the application:

    import org.my.api.capabilities.ReloadableEntityCapability;
    import org.my.api.capabilities.SaveableEntityCapability;
    import org.my.api.capabilities.CreatableEntityCapability;
    import org.my.api.capabilities.RemovableEntityCapability;
    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();
    }
    });
    instanceContent.add(new SaveableEntityCapability() {
    @Override
    public void save(Trip trip) throws Exception {
    dao.save(trip);
    }
    });
    instanceContent.add(new CreatableEntityCapability() {
    @Override
    public void create(Trip trip) throws Exception {
    dao.create(trip);
    }
    });
    instanceContent.add(new RemovableEntityCapability() {
    @Override
    public void remove(Trip trip) throws Exception {
    dao.remove(trip);
    getTrips().remove(trip);
    }
    });
    }

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

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

    }

  4. Use the capability. Now it's time to switch hats. We're now user interface engineers, rather than database engineers. The database team has prepared a capability for us, named "DeletableEntityCapability", and they've documented it, so that we know what it's for, though we don't need to care about what it's actually doing under the hood. We simply need to retrieve that capability from the Lookup of the "TripQuery" object and then pass a "Trip" to it whenever we want to delete a "Trip".

    Let's assume that we, i.e., the user interface engineers, want to create this user interface for deleting trips:

    As you can see, the user is able to right-click a trip to delete it or use the Delete button (that's the one with the red cross in it) in the toolbar. When either of the Delete items are clicked, this dialog is shown prior to deletion:

    And, when multiple trips are selected for deletion...

    ...clicking "Delete" results in this dialog:

    ...followed by a deletion of all the selected trips. The good news is that none of these user interface elements (i.e., the menu item, the button, and the dialogs) need to be manually created. Instead, the NetBeans Platform provides all of these out of the box. It is your job to enable this functionality and plug in your own custom code and that is a little bit tricky. But, once everything is set up, you'll be surprised how much new functionality you have.

    • Enable the "Delete" functionality in your TopComponent. In the "TripViewerTopComponent", add the "delete" action in the ActionMap:
      ...
      ...
      ...
      ActionMap map = getActionMap();
      map.put("delete", ExplorerUtils.actionDelete(em, true)); // NOI18N
      associateLookup(ExplorerUtils.createLookup(em, map));
    • Listen for node deletions and then refresh the hierarchy. In the "RootNodeChildFactory", implement "org.openide.nodes.NodeListener". Then you'll need to implement a bunch of methods in your ChildFactory, with only the "nodeDestroyed" being relevant to this example:
      @Override
      public void nodeDestroyed(NodeEvent ev) {
      refresh(true);
      }

      @Override public void propertyChange(PropertyChangeEvent evt) {}
      @Override public void childrenAdded(NodeMemberEvent ev) {}
      @Override public void childrenRemoved(NodeMemberEvent ev) {}
      @Override public void childrenReordered(NodeReorderEvent ev) {}
    • Enable the "Delete" functionality in the "TripNode" and use the "RemoveableEntityCapability". Now we're finally in the "TripNode", i.e., this is the child node.
      @Override
      public Action[] getActions(boolean context) {
      return new Action[]{(SystemAction.get(DeleteAction.class))};
      }

      @Override
      public boolean canDestroy() {
      return true;
      }

      @Override
      public void destroy() throws IOException {
      Trip trip = getLookup().lookup(Trip.class);
      TripQuery query = getLookup().lookup(TripQuery.class);
      RemovableEntityCapability cec = query.getLookup().lookup(RemovableEntityCapability.class);
      try {

      cec.remove(trip);
      } catch (Exception e) {
      }
      //Notify the NodeListener in the RootNodeChildFactory,
      //where nodeDestroyed will call refresh on the ChildFactory:
      fireNodeDestroyed();
      }

      As you can see, we need to have no idea at all about what's going on on the database level. We only need to know that a 'RemoveableEntityCapability' exists, we pass it our Trip object, and then we're done. We notify the ChildFactory that a Node has been destroyed, which then results in a 'refresh' being called and the Node hierarchy being recreated by the ChildFactory.

    • Register the "DeleteAction" and show it in the toolbar. And now we go to the layer.xml file (I have this file in a separate branding module) and there we register the "DeleteAction", as follows, below the "NewAction". (The "NewAction" is loaded into the root trip's contextual menu, because it builds its menus from the "Actions/RootTrip" folder which is only relevant to the root node, as explained in the previous part of this series.)
      <folder name="Actions">
      <folder name="RootTrip">
      <file name="org-openide-actions-NewAction.instance">
      <attr methodvalue="org.openide.awt.Actions.context" name="instanceCreate"/>
      <attr name="delegate" newvalue="org.openide.actions.NewAction"/>
      <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
      <attr boolvalue="false" name="noIconInMenu"/>
      <attr name="type" stringvalue="org.openide.util.datatransfer.NewType"/>
      <attr name="iconBase" stringvalue="org/my/branding/record-new.png"/>
      <attr name="displayName" stringvalue="New Trip"/>
      </file>
      </folder>
      <folder name="LeafTrip">
      <file name="org-openide-actions-DeleteAction.instance">
      <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.callback"/>
      <attr name="key" stringvalue="delete"/>
      <attr name="iconBase" stringvalue="org/my/branding/delete.png"/>
      <attr name="displayName" stringvalue="Delete Trip"/>
      <attr name="noIconInMenu" boolvalue="false"/>
      </file>
      </folder>
      </folder>

      As you can see, we have bound the "delete" key in the ActionMap of the TopComponent to the "DeleteAction". We're using a "CallBack" action, as documented here in the NetBeans API javadoc.

      <folder name="Toolbars">
      <folder name="File">
      <file name="org-openide-actions-DeleteAction.shadow">
      <attr name="originalFile" stringvalue="Actions/LeafTrip/org-openide-actions-DeleteAction.instance"/>
      <attr name="position" intvalue="20"/>
      </file>
      ...
      ...
      ...

      And, as you can see above, we bind the "DeleteAction" to the File toolbar in the application.

And, though the above is slightly intricate and tweaky, you now have all the Delete functionality shown above in the screenshots. The NetBeans Platform provides everything you need, once you've enabled it to do so, as described above.

 

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

Comments

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

Well, the problem with Derby (embbed) and deleting is interesting. But the I managed to resolve it by using the principle from the Sample CRUD app from NTB:  

    public void remove(Trip trip) {
      createTransactionalEntityManager(); 
      Trip toDelete = em.find(Trip.class, trip.getId());
      try {
        em.remove(toDelete);
      } catch (Exception e) {
        System.out.println(e);
      }
        closeTransactionalEntityManager();
  }

Well, probably it could be a bit optimized (ie. send to remove method only int of the ID, but it is not probably so good in the designer point of view).

Martin Vondráček replied on Fri, 2012/03/02 - 5:32pm

For those beginners, don't forget to change also your createNodeForKey method of the RootChildFactory. There has to be:  

@Override
protected Node createNodeForKey(Trip key) {  
   TripNode tripNode = new TripNode(key, query);
   tripNode.addNodeListener(this);
   return tripNode;
}

Otherwise the NodeListener's methods like nodeDestroyed will never be called.

Comment viewing options

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