2007-12-18

Central Lookup: Creating a central repository and lookup for an application context in a NetBeans RCP application

I have been working on a NetBeans RCP project where I needed a central place to register different interfaces and allow different views to operate on these regular POJOs one would use in a regular Swing application and to be able to listen to changes to them from the standpoint of some kind of an Application Context where different instances will come and go. A good example is a desktop application requiring a login. You will have user information and want to be able to track a global login or validation token.

I create a simple class and API in a NetBeans module and called it CentralLookup. This is all the code for such a simple thing (please ignore the formatting of the code...maybe I can update it later, but it seems blogger isn't wanting to format it correctly when posted):


package org.netbeans.modules.centrallookup.api;

import java.util.Collection;

import org.openide.util.Lookup.Result;

import org.openide.util.lookup.AbstractLookup;

import org.openide.util.lookup.InstanceContent;

/**
* Class used to house anything one might want to store

* in a central lookup which can affect anything within

* the application. It can be thought of as a central context

* where any application data may be stored and watched.

*

* A singleton instance is created using @see getDefault().

* This class is as thread safe as Lookup. Lookup appears to be safe.

* @author Wade Chandler

* @version 1.0

*/

public class CentralLookup extends AbstractLookup {

private InstanceContent content = null;

private static CentralLookup def = new CentralLookup();

public CentralLookup(InstanceContent content) {
super(content);
this.content = content;
}

public CentralLookup() {

this(new InstanceContent());
}

public void add(Object instance) {

content.add(instance);
}

public void remove(Object instance) {

content.remove(instance);
}

public static CentralLookup getDefault(){
return def;
}
}


The nice thing about this code is that it is real simple. The strange thing for me is that there hasn't been something already implemented in the base RCP. It seems pretty common a thing. For instance, the normal global lookups deal with Actions, Services, and Nodes (which are part of the NetBeans data model), and that is fine until one needs to work with generic interfaces and classes from a global perspective.


Regardless, I can now write code to listen to changes in this global context or Lookup in this case. The code doesn't have to know about any implementation except for the code which does the injection, and the code in other places can just setup a result and a listener. The following illustrates this a bit, and of course it is just a sub-set of the code:

final class CentralLookupTest1TopComponent extends TopComponent {

private static CentralLookupTest1TopComponent instance;
private Lookup.Result userInfoResult = null;
private CentralLookupTest1TopComponent() {

Lookup.Template template = new Lookup.Template(UserInformation.class);

CentralLookup cl = CentralLookup.getDefault();
userInfoResult = cl.lookup(template);
userInfoResult.addLookupListener(new UserInformationListener());
}

private javax.swing.JTextField name;
private javax.swing.JTextField pwd;
private javax.swing.JTextField token;
private javax.swing.JTextField uid;

private class SetterRunnable implements Runnable {
UserInformation ui = null;

public SetterRunnable(UserInformation ui) {
this.ui = ui;
}

public void run() {
name.setText(ui.getName());
pwd.setText(ui.getPassword());
uid.setText(ui.getUserID());
token.setText(ui.getToken());
}
}

private class UserInformationListener implements LookupListener {

public void resultChanged(LookupEvent evt) {
Object o = evt.getSource();
if (o != null) {
Lookup.Result r = (Lookup.Result) o;
Collection infos = r.allInstances();
if (infos.isEmpty()) {
EventQueue.invokeLater(new SetterRunnable(new DefaultUserInformation()));
} else {
Iterator it = infos.iterator();
while (it.hasNext()) {
UserInformation info = it.next();
EventQueue.invokeLater(new SetterRunnable(info));
}
}
}
}
}
}


Notice we have a result. The result has attached to it a listener. The listener knows about DefaultUserInformation and the UserInformation interface, and that is it. It then knows it needs to listen for instances of UserInformation and act accordingly. It doesn't know how it got there, but knows what it must do with it. We then have some other code in another class which will inject the instances into the CentralLookup based on some user input removing any previous:

private void buttonActionPerformed(java.awt.event.ActionEvent evt) {
//ok we need to remove any user information from the central lookup
//and then we need to add this new one

CentralLookup cl = CentralLookup.getDefault();
Collection infos = cl.lookupAll(UserInformation.class);

if(!infos.isEmpty()){
Iterator it = infos.iterator();
while(it.hasNext()){
UserInformation info = it.next();
cl.remove(info);
}
}

DefaultUserInformation info = new DefaultUserInformation();
info.setName(name.getText());
info.setPassword(pwd.getText());
info.setUserID(uid.getText());
info.setToken(token.getText());
cl.add(info);

}


You can get a good idea from here the possibilities. This is a rather simple example, but it certainly shows it working. This can be used for any other services which need to use regular POJOs and not much else. Why add more overhead of class wrappers etc when they are not needed for everything? Sometimes one just needs a dynamic application context which can breakup the application into a more modular framework of simple classes and interfaces without major restrictions.

I have an AVI video formatted video of this in action. It is a very simple example, but it shows it working. The one piece of code doesn't know anything other than the fact that user information will appear magically in the CentralLookup in the form of the UserInformation interface.


5 comments:

Tim Boudreau said...

I don't know if you prefer it to be a separate thing from the global selection (Utilities.actionsGlobalContext()), but if you're really using it in a selection-like way, you might want to try actually replacing the global selection context which is normally based on the active TopComponent's Lookup.

For an example, see my Imagine project (something I had grand ambitions for but not nearly enough time to work on).

Tim Boudreau said...

The nice thing about replacing the selection context is that then existing actions such as SaveAction, OpenAction, other CookieAction-based things should work.

Wade Chandler said...

I had looked at ContextGlobalProvider . I wasn't sure, but I thought maybe I could implement an instance of it and the global context would merge mine with any others, and then I would be able to simply add a ContextGlobalProvider and expose the CentralLookup's information in the global context. Is this not how it would work? I haven't played around with it.

The thing I like about CentralLookup is it provides a place where everything can register or add any instance. For instance, I have a global transaction id which is the most recent transaction id, and I also have authentication credentials used by multiple modules. So, all these things are regular JavaBeans, and all the modules have to do is listen to what they want and others just call CentralLookup.add and they all do their thing on user events.

If I'm using the global context then I am dealing with the Lookup classes interface which doesn't have any methods to change the content, so I still have to have a global InstanceContent or something which does the same thing or accesses one, and that was where CentralLookup came into play. If I leave the main instance of ContextGlobalProvider will the NB APIs use both and merge them together?

Unknown said...

Thanks. I could apply some of these ideas into my application. I was using a "Session" object which acted in a similar manner as your "CentralLookup". The main difference is that I was using META-INF/services to load my Session object. Then I could obtain the "Session" from the global lookup and query it for the login information, etc.

Anonymous said...

CentralLookup is a nice concept but where's the update() method ? You have an add() and a remove() but no update() method ??

The reason why I ask is that add() doesn't actually do what most people would think. If the object already exists in the lookup (as determined by object's equals() method) it will replace the old object instance with the new one, but it will not fire any notifications.

You can of course simulate an update by doing remove() followed by an add() but that will trigger two notifications instead of just one.

This is one aspect of the CentralLookup that I haven't really tackled.