Platinum Partner
netbeans

Loosely Coupled Saveable Capabilities for CRUD Applications

Let's now go a step further and add Saveable capabilities to the scenario outlined in Loosely Coupled Reloadable Capabilities for CRUD Applications. I.e., before beginning this article, the assumption is that you've read the previous part, otherwise what follows will make no sense at all.

So, the aim is to use the same "capabilities approach" in adding save functionality to our application.

We start with our DAO. In addition to the "search()" method, we now also have a "save()" method:

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

public final class CustomerSearchDAO {

EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerPU").createEntityManager();

public List<Customer> search(String search) {
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;
}

public void save(Customer customer) {
entityManager.getTransaction().begin();
entityManager.find(Customer.class, customer.getCustomerId());
entityManager.getTransaction().commit();
}

}

As you can see, the above is all pure standard JPA code. That's nice, a clean separation between our NetBeans API code and our data access functionality.

Now we define a capability that we're going to implement in our query object. The query object needs, in addition to a capability for reloading, a capability for saving. Therefore, in a separate class in our API module, we define our new capability:

public interface SaveableEntityCapability {

public void save(Customer customer) throws Exception;

}

Next, we implement our new capability in the query object. The only change to the query object from yesterday is the addition of the SaveEntityCapability to the InstanceContent of the query object, which you can see in the last block below:

public CustomerQuery() {
customers = new ArrayList();
// 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 result = dao.search(sqlstring);
for (Customer customer : result) {
if (!getCustomers().contains(customer)) {
getCustomers().add(customer);
}
}
handle.finish();
}
});
instanceContent.add(new SaveableEntityCapability() {
@Override
public void save(Customer customer) throws Exception {
dao.save(customer);
}
});
}

So, later we'll be able to retrieve the SaveableEntityCapability from the query object's Lookup and then call its Save method whenever we need to save a Customer object.

Now create a new module (named "MyEditor", for example). In the new module, create a new TopComponent (named "MyEditorTopComponent", for example). In this TopComponent, create a JTextField that will display the name of the currently selected customer. I.e., the user will select a customer node and then the current customer's name will appear in the editor TopComponent.

In the editor TopComponent, we're going to create a SaveCookie implementation as follows:

private class SaveableViewCapability implements SaveCookie {
@Override
public void save() throws IOException {
SaveableEntityCapability saveable = query.getLookup().lookup(SaveableEntityCapability.class);
try {
customer.setName(nameField.getText());
saveable.save(customer);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
ReloadableEntityCapability r = query.getLookup().lookup(ReloadableEntityCapability.class);
try {
r.reload();
} catch (Exception e) {
}
ReloadableViewCapability rvc = customerNode.getLookup().lookup(ReloadableViewCapability.class);
try {
rvc.reloadChildren();
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
fire(false);
}
}

The above is the key to understanding everything discussed in this article. What you have above is a SaveCookie which is, essentially, a "SaveableViewCapability". So, in the same way that yesterday we had a capability pair of "ReloadableEntityCapability / ReloadableViewCapability", we now have "SaveableEntityCapability / SaveableViewCapability". From the view capability, which is the SaveCookie, we first call the model capability (the "SaveEntityCapability"), after which we need to reload the entity, followed by a reload of the view.

So, the above is completely correct and exactly the order of capabilities that we would expect.

Note that for the above to be possible, we need to have access to the query, as well as to the Customer. After all, the query provides implementations for saving Customers and reloading itself. Then, we also need to have access to the Node, since the Node needs to have an implementation of the capability for itself to be reloaded.

And here is the Node, i.e., the CustomerNode. The CustomerNode needs to expose the customer and the query, together with an implementation of its capability of being reloaded:

import client.Customer;
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 CustomerNode extends AbstractNode {

public CustomerNode(Customer customer, CustomerQuery query) {
this(customer, query, new InstanceContent());
}

private CustomerNode(final Customer customer, final CustomerQuery query, InstanceContent ic) {
super(Children.LEAF, new AbstractLookup(ic));
final String oldName = customer.getName();
ic.add(customer);
ic.add(query);
ic.add(new ReloadableViewCapability() {
@Override
public void reloadChildren() throws Exception {
String newName = customer.getName();
fireDisplayNameChange(oldName, newName);
}
});
}

@Override
public String getDisplayName() {
Customer c = getLookup().lookup(Customer.class);
return c.getName();
}

}

The CustomerNode is created from the RootNodeChildFactory, as follows:

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

class RootNodeChildFactory extends ChildFactory {

private CustomerQuery query;

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

@Override
protected boolean createKeys(List list) {
// get this ability from the lookup ...
ReloadableEntityCapability r = query.getLookup().lookup(ReloadableEntityCapability.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) {
return new CustomerNode(key, query);
}

}

Finally, you need to listen for multiple objects in the EditorTopComponent. Not only should you be listening for the Customer object and the query object, but also to the Node:

@Override
public void componentOpened() {
customerNodeResult = Utilities.actionsGlobalContext().lookupResult(Node.class);
customerResult = Utilities.actionsGlobalContext().lookupResult(Customer.class);
customerQueryResult = Utilities.actionsGlobalContext().lookupResult(CustomerQuery.class);
customerNodeResult.addLookupListener(this);
customerQueryResult.addLookupListener(this);
customerResult.addLookupListener(this);
}

@Override
public void componentClosed() {
customerNodeResult.removeLookupListener(this);
customerQueryResult.removeLookupListener(this);
customerResult.removeLookupListener(this);
}

And, in the "resultChanged" in the EditorTopComponent, set global variables, for each of the objects you're interested in:

@Override
public void resultChanged(LookupEvent le) {
//Get the query:
Collection allQueries = customerQueryResult.allInstances();
Iterator it1 = allQueries.iterator();
while (it1.hasNext()) {
query = it1.next();
setDisplayName(query.getSqlstring());
}
//Get the customer:
Collection allCustomers = customerResult.allInstances();
Iterator it2 = allCustomers.iterator();
while (it2.hasNext()) {
customer = it2.next();
jTextField1.setText(customer.getName());
}
//Get the node:
Collection allNodes = customerNodeResult.allInstances();
Iterator it3 = allNodes.iterator();
while (it3.hasNext()) {
customerNode = it3.next();
}
}

And now you can understand the SaveCookie implementation better (which is added to the Lookup of the TopComponent, via InstanceContent) shown earlier, reproduced again below:

private class SaveableViewCapability implements SaveCookie {
@Override
public void save() throws IOException {
SaveableEntityCapability saveable = query.getLookup().lookup(SaveableEntityCapability.class);
try {
customer.setName(jTextField1.getText());
saveable.save(customer);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
ReloadableEntityCapability r = query.getLookup().lookup(ReloadableEntityCapability.class);
try {
r.reload();
} catch (Exception e) {
}
ReloadableViewCapability rvc = customerNode.getLookup().lookup(ReloadableViewCapability.class);
try {
rvc.reloadChildren();
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
fire(false);
}
}

And now the "Save" capabilities, consisting of a save view capability and a save entity capability, should do exactly what you would expect. Automatically, whenever a change is saved in the editor TopComponent, the viewer TopComponent is updated.

 

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}