Monday October 06, 2008
Custom Toolbar Loaded via Lookup
Let's say you want the toolbar to be on the right side of your application. Let's also say that you want the icons to be larger than 16x16 or 24x24. At the same time, you want to use the NetBeans Actions system, so you can't simply create a TopComponent and then put buttons on the TopComponent. How to solve this problem? Use Lookup! Here's how the result looks for me:
First, I removed all the existing toolbars, by simply adding this to the layer:
<file name="Toolbars_hidden"/>
So, now the toolbars were all gone when I ran the application. Then I created a new TopComponent. I also created a new mode specifically for the TopComponent, to position it along the right side of the application.
Next, I exposed my CallableSystemActions via the META-INF/services technique:
This is the complete content of the file in META-INF/services:
org.nb.customtoolbar.Action1 org.nb.customtoolbar.Action2 org.nb.customtoolbar.Action3 org.nb.customtoolbar.Action4 org.nb.customtoolbar.Action5
Each of the actions looks as follows:
public final class Action1 extends CallableSystemAction {
@Override
public void performAction() {
JOptionPane.showMessageDialog(null, "hello from 1");
}
@Override
protected String iconResource() {
return "/org/nb/customtoolbar/pic1.png";
}
@Override
public String getName() {
return "rightSideToolBar";
}
@Override
public HelpCtx getHelpCtx() {
return HelpCtx.DEFAULT_HELP;
}
@Override
protected boolean asynchronous() {
return false;
}
}
The only interesting thing is the getName(). I use that to determine whether or not the action should be displayed in the TopComponent. And how are they displayed in the TopComponent? Like this:
private CustomToolbarDisplayerTopComponent() {
initComponents();
//Here we iterate over implementations of 'CallableSystemAction':
for (CallableSystemAction action : Lookup.getDefault().lookupAll(CallableSystemAction.class)) {
//We're only interested in an implementation if it has the right name and an icon:
if (action.getName().equals("rightSideToolBar") && action.getIcon() != null) {
//Then we add a new button to the JPanel:
addButton(action, jPanel1);
}
}
}
private static void addButton(final CallableSystemAction action, JPanel container) {
JButton button = new JButton();
button.setAlignmentX(Component.CENTER_ALIGNMENT);
//The icon is set by calling 'getIcon' on the implementation:
button.setIcon(action.getIcon());
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//The action is performed by calling 'actionPerformed' on the implementation:
action.actionPerformed(e);
}
});
container.add(button);
}
And that's all! The TopComponent is in a different module to where the actions are found; there is no direct dependency on them, only on the CallableSystemAction class. And my toolbar is pluggable because anyone can add their own module that implements CallableSystemAction. All that's needed is for their implementation to include the correct name and an icon and then the additional toolbar button will be loaded. Currently, it will be loaded upon restart. But I could add a LookupListener that would detect whenever a new module fitting the requirements is available, at which point the toolbar button would be loaded at runtime.
One potential problem will occur when the user drags the TopComponent to a different position. Potentially the images will then need to be reorganized, so that they're horizontal when they're in a horizontal mode. But apart from that, I think this is a good solution.
Oct 06 2008, 08:37:27 PM PDT Permalink
Serializing Marilyn Monroe
I made a lot of progress with node serialization. Now, when I start up my application, I see ALL of the following selected:
The above are selected, without me doing anything at start up, because those were the ones I had selected at the time the application shut down. Hence, I serialized ALL the selected nodes. Before, I only managed to work with a single selection, instead of the multiple selection you see above, because I hadn't figured out how to iterate over my handles correctly and add them to the array of nodes that is selected at start up. The important changes over the previous blog entry is in bold below, within the context of everything else that is relevant here:
@Override
public Object writeReplace() {
Handle[] selectedHandles = NodeOp.toHandles(em.getSelectedNodes());
return new ResolvableHelper(selectedHandles);
}
public final static class ResolvableHelper implements Serializable {
private static final long serialVersionUID = 1L;
public Handle[] selectedHandles;
private ResolvableHelper(Handle[] selectedHandles) {
this.selectedHandles = selectedHandles;
}
public Object readResolve() {
try {
DemoTopComponent result = DemoTopComponent.getDefault();
String path;
int noOfHandles = selectedHandles.length;
Node rootNode = result.getExplorerManager().getRootContext();
Node foundNode = null;
Node[] allFoundNodes = new Node[noOfHandles];
//We build up an array of found nodes:
for (int i = 0; i < noOfHandles; i++) {
Handle handle = selectedHandles[i];
path = handle.getNode().getDisplayName();
foundNode = NodeOp.findPath(rootNode, new String[]{path});
if (foundNode != null) {
allFoundNodes[i] = foundNode;
}
}
//Then we select the array, which means each node is selected:
if (allFoundNodes != null) {
try {
result.getExplorerManager().setSelectedNodes(allFoundNodes);
} catch (PropertyVetoException ex) {
Exceptions.printStackTrace(ex);
}
}
return result;
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
}
However, in real life your children would probably have children too. (And your children's children would have children, ad nauseum.) How to serialize the entire hierarchy of selections from parent to child to grandchild and so on? Below you see my Marilyn Monroe window (search this blog for all the code, which is definitely somewhere), but this time the nodes that are selected below are selected at start up, i.e., automatically, because I serialized them at the time the application shut down:
Below is all the relevant code. Instead of serializing the entire set of selected nodes, we only serialize the first one. The cool thing is that when you serialize a node, you store the COMPLETE node, which means that all the information on the node is available to you, such as who its parent is! So, when the application restarts, we get the parent of the serialized node and, based on whether the parent is named 'Root' (which is the name of the hidden root node of the explorer view), we either select only the node or also the parent node:
@Override
public Object writeReplace() {
Handle childHandle = NodeOp.toHandles(em.getSelectedNodes())[0];
return new ResolvableHelper(childHandle);
}
public final static class ResolvableHelper implements Serializable {
private static final long serialVersionUID = 1L;
public Handle parentHandle;
public Handle childHandle;
private ResolvableHelper(Handle childHandle) {
try {
this.childHandle = childHandle;
this.parentHandle = childHandle.getNode().getParentNode().getHandle();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
public Object readResolve() {
try {
MarilynTopComponent result = MarilynTopComponent.getDefault();
String parentName = parentHandle.getNode().getDisplayName();
String childName = childHandle.getNode().getDisplayName();
Node root = result.getExplorerManager().getRootContext();
Node parentNode = null;
Node childNode = null;
//We know a movie has been selected if the parent is not named 'Root',
//which is the name of the hidden root node of the explorer view,
//so we find two nodes and select both:
if (!parentName.equals("Root")) {
parentNode = NodeOp.findPath(root, new String[]{parentName, childName});
childNode = NodeOp.findPath(root, new String[]{parentName});
result.getExplorerManager().setSelectedNodes(new Node[]{childNode, parentNode});
}
//If the parent is 'Root', then we're dealing with a category,
//so we only need to find one node, the child of the 'Root', which we then select:
else {
childNode = NodeOp.findPath(root, new String[]{childName});
result.getExplorerManager().setSelectedNodes(new Node[]{childNode});
}
return result;
} catch (PropertyVetoException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
}
This principle could be extended for a larger hierarchy of nodes and, if you don't only serialize a single node, your user could potentially see many hierarchies of selected nodes at the time that the application restarts.
Oct 05 2008, 09:17:01 AM PDT Permalink
"CTL_MainWindow_Title": Customizing the Title Bar
There are many ways to customize a NetBeans Platform application's title in the title bar. Below, I try to list all of them, together with all related references. One typical added requirement is that the title should change dynamically, upon selection of something different in the application. For example, in the IDE the current project's name appears in the IDE's title bar. That's also quite easy to do.
import org.openide.modules.ModuleInstall;
public class Installer extends ModuleInstall {
@Override
public void restored() {
System.setProperty("netbeans.buildnumber", "");
}
}
The above comes from an interview with Emilian Bold. The most often heard solution is to go here in an application's branding folder, in the Files window in the IDE:
In the above file you'll find keys/value like this:
CTL_MainWindow_Title=DemoApp {0}
CTL_MainWindow_Title_No_Project=DemoApp {0}
Simply remove the {0} at the end of the values and you'll not see a build number in the application.
Another approach is to grab the application's frame and change its title:
public class Installer extends ModuleInstall {
@Override
public void restored() {
WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
public void run() {
JFrame frame = (JFrame) WindowManager.getDefault().getMainWindow();
String title = NbBundle.getMessage(Installer.class, "LBL_TITLE");
frame.setTitle(title);
}
});
}
}
The code above could be anywhere at all, not necessarily in the Installer class. Note that above we're referring to a bundle file that is in the same package as our class. In that bundle file, the value of the key "LBL_TITLE" (or whatever the name of your key is) sets the title of the application. This means you could have many different titles in the bundle and then dynamically switch them at runtime from the code, as done above.
However, there's more you can do with bundle files, as the code completion for NbBundle.getMessage indicates:
So, based on the current selection, you can pass something into the argument that will tweak the title of the title bar, as shown below. In this case, a listener is set on an Explorer Manager so that the currently selected node in an explorer view determines the content of the title bar:
em.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
public void run() {
JFrame frame = (JFrame) WindowManager.getDefault().getMainWindow();
String title = NbBundle.getMessage(DemoTopComponent.class, "LBL_TITLE", em.getSelectedNodes()[0].getDisplayName());
frame.setTitle(title);
}
});
}
});
In the Bundle.properties file, which is in the same package as where the above class is found, I have the following entry:
LBL_TITLE=Selected: {0}
The {0} is a placeholder for the first additional argument sent by the NbBundle.getMessage in my Java code above. Now, whenever I choose a new node in the explorer view, the title changes in the title bar, as indicated below:
Up to 4 different objects can be passed into the argument of NbBundle.getMessage, only the first of which must be a string. The others could be any object, even an array of objects. For example, it could be like this in the Bundle.properties file:
LBL_TITLE=Selected: {0} {1} {2} {3} {4}
The whole value above is a string, so you could put other strings in between the 4 placeholders, such as shown here with some random characters thrown in:
LBL_TITLE=Selected: {0} -- {1} / {2} and {3} --- {4}
This is not useful only in the context of title bars, but is a good example of its applicability in that case.
Two other useful pieces for working with the title bar are Netbeans Platform Branding and version info and Branding custom version info into NetBeans RCP apps and the missing Bundle.Properties file.
Oct 04 2008, 11:56:55 AM PDT Permalink
Serializing Nodes
By default, the NetBeans window system restores the application's customized layout, i.e., window position and size, when the application restarts (assuming the user directory hasn't been deleted). For example, even though the Projects window in the IDE is in the explorer mode, the user can move it to the properties mode. Then, when the application shuts down, the last position (and size) of the window is serialized (i.e., stored) on disk, in the user directory. When the application restarts, these settings are then loaded, so that the user's preferences are restored.
That's the default situation. I.e., only the window layout is serialized by default. Potentially, you'd also like to serialize the data in a window. That's been discussed before (here). In addition, you can use the NbPreferences class (as discussed here) or the JDK's Preferences class, if that's what you prefer. There are several different ways of doing the same thing. The advantages of one over the other depends on your scenario, as well as your personal taste.
Another further step you can take is to serialize the selected node in an explorer view. Here, for example, is what I see after restarting my application, i.e., one of the nodes in the explorer view was already selected when the application started:
And that is the case because that node was the last one I had selected before closing the application. That could be handy because it extends the amount of custom information you can restore to your user upon restart.
Providing this scenario in your application is not really trivial, but less complex than it could be. Start by reading "Serialization and traversal" in the Javadoc. There you'll find that you need Node.getHandle when writing (using writeReplace in the TopComponent) and Node.Handle.getNode when reading (using readResolve in the TopComponent) the serialized node.
Below is the most important section of code, within the TopComponent. In writeReplace I get the selected nodes from the Explorer Manager. By the way, I've limited the number of nodes the user can select, when I defined the BeanTreeView:
beanTreeView.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
Potentially, you can also serialize ALL the selected nodes. I've managed to do that, but having them automatically selected in the hierarchy after retrieving them at startup has been something I've been unable to do. So, in this scenario, I'm assuming the user can only make a single selection. So writeReplace below turns the Nodes into Handles, which are references to the nodes for serialization purposes. The utility class org.openide.nodes.NodeOp is crucial, extremely handy for converting to/from handles and putting nodes back into the tree when reading the settings back into the application. Then the handles are passed to the ResolvableHelper class, which implements Serializable, thus automatically serializing the received handles (i.e., storing them on disk). This happens every time the user closes the application.
@Override
public Object writeReplace() {
Handle[] selectedHandles = NodeOp.toHandles(em.getSelectedNodes());
return new ResolvableHelper(selectedHandles);
}
public final static class ResolvableHelper implements Serializable {
private static final long serialVersionUID = 1L;
public Handle[] selectedHandles;
private ResolvableHelper(Handle[] selectedHandles) {
this.selectedHandles = selectedHandles;
}
public Object readResolve() {
try {
DemoTopComponent result = DemoTopComponent.getDefault();
String path = selectedHandles[0].getNode().getDisplayName();
Node foundNode = NodeOp.findPath(result.getExplorerManager().getRootContext(), new String[]{path});
if (foundNode != null) {
try {
result.getExplorerManager().setSelectedNodes(new Node[]{foundNode});
} catch (PropertyVetoException ex) {
Exceptions.printStackTrace(ex);
}
}
return result;
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
}
The second part of the section of code above applies to the point where the application is restarted. It simply gets the first of the serialized nodes and then gets its display name. Then NodeOp.findPath is used to put it back in the tree. Then the ExplorerManager.setSelectedNodes selects that node in the Explorer Manager.
Two other things are important—you need to specify on your Nodes that you'd like them to create handles. Secondly, it is safe to always define a name (even if you don't need it) on your Nodes, since some other parts of the API assume that your Nodes are named. For example, this is how the children shown above are created:
@Override
protected Node createNodeForKey(String key) {
AbstractNode result = new AbstractNode(Children.LEAF, Lookups.fixed());
result.setDisplayName(key);
result.setName(key);
result.setIconBaseWithExtension("/org/nb/properties/icon.png");
result.getHandle();
return result;
}
So above the AbstractNode.getHandle and the AbstractNode.setName are really important to provide in this scenario. For the rest, I think pretty much everything related to this scenario is discussed in this blog entry.
Oct 03 2008, 09:23:10 AM PDT Permalink
Opening Multiple TopComponents For One File
In Convert your TopComponent to a MultiViewElement I explained, in the first part of that blog entry, how to open a file represented by a node in an explorer view into a TopComponent (instead of into the Source Editor, where a file opens by default). Basically, as described in that blog entry, you need to extend org.openide.loaders.OpenSupport and implement org.openide.cookies.OpenCookie and org.openide.cookies.CloseCookie. Not very nice code, but it works perfectly.
However, what if you want to open multiple TopComponent at the same time whenever you open a file? There might be different views onto the same file, all provided by different TopComponents. In this case, I've discovered that you need to override the open() method in your OpenSupport class. Then you can open the TopComponents in one of two ways: if you want group behavior you will create a TopComponentGroup and then call open on the group, within the overridden open().
@Override
public void open() {
super.open();
TopComponentGroup group = WindowManager.getDefault().findTopComponentGroup("MyGroup");
if (group != null){
group.open();
}
}
Alternatively, just call open() on each of the TopComponents separately and then call active() on the TopComponent that should be active when the file is opened. Note that in this case the TopComponent must be a CloneableTopComponent, but that doesn't necessarily mean that the TopComponent will be cloneable, since only TopComponents in 'editor' modes are cloneable, never TopComponents in 'view' modes (i.e., there's no 'Clone Document' menu item on TopComponents in 'view' modes). Below, instead of creating a TopComponentGroup, I simply opened another TopComponent from the open() in the OpenSupport class:

And here's the OpenSupport class:
class DemoOpenSupport extends OpenSupport implements OpenCookie, CloseCookie {
TwoTopComponent tc;
String name;
public DemoOpenSupport(Entry primaryEntry) {
super(primaryEntry);
DemoDataObject dobj = (DemoDataObject) primaryEntry.getDataObject();
this.name = dobj.getName():
this.tc = new TwoTopComponent();
}
@Override
protected CloneableTopComponent createCloneableTopComponent() {
OneTopComponent tc = new OneTopComponent();
tc.setDisplayName(name);
return tc;
}
@Override
public void open() {
super.open();
tc.setDisplayName(name);
tc.open();
tc.requestActive();
}
}Oct 02 2008, 12:00:00 AM PDT Permalink
Spring RCP Application Initialized From Griffon
To get them to co-exist, somehow Spring RCP's application class should replace the Griffon application class, or the other way round. Maybe I need to create an ApplicationBuilder for Spring RCP? There's also threading issues to figure out. Anyway, right now I'm able to show both applications: the Griffon application and the Spring RCP application (at least, their main windows):
Here's the application context, containing one single bean only, for the application class:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="application" class="org.springframework.richclient.application.Application">
</bean>
</beans>
Now the question is from where to initialize the above. In "Startup.groovy", I have this:
def rootController = app.controllers.root rootController.startApplication()
...which points to this in the above referenced controller:
def appContextPath = "richclient-application-context.xml"
def startApplication = { evt = null ->
edt {
withWorker( start: true ) {
onInit {
}
work {
try {
new ApplicationLauncher(appContextPath)
} catch (RuntimeException e) {
println 'Failure'
}
}
onDone {
}
}
}
}
Without SwingWorker, I couldn't get it to work. Then when the application is run, first the Griffon view is shown, then you see Spring RCP output (all about initialization of Spring RCP internals), and then the application class is initialized and displayed. Whether the above is a step forward or not I don't know. But at least it shows the Spring RCP application, which is nice to see in this context.
Sep 29 2008, 01:01:28 PM PDT Permalink
Spring's MethodInvokingFactoryBean & Griffon
Here's my Spring application context:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="sysProps" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass">
<value>java.lang.System</value>
</property>
<property name="targetMethod">
<value>getProperties</value>
</property>
</bean>
<bean id="javaVersion" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject">
<ref local="sysProps"/>
</property>
<property name="targetMethod">
<value>getProperty</value>
</property>
<property name="arguments">
<list>
<value>java.version</value>
</list>
</property>
</bean>
</beans>
And here's my Griffon "Initialize.groovy":
import org.springframework.richclient.application.ApplicationLauncher
def appContextPath = "richclient-application-context.xml"
try {
new ApplicationLauncher(appContextPath)
} catch (RuntimeException e) {
println 'Failure'
}
Next, in "Startup.groovy", I initialize the controller:
def rootController = app.controllers.root rootController.gotoPage()
And now I can get the application context and inject the Spring bean defined at the start of this blog entry:
class GriffonDemo1Controller {
def view
def gotoPage() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("richclient-application-context.xml")
String jVersion = ctx.getBean("javaVersion")
view.versionLabel.text = "Java Version: " + jVersion
}
}
The final line above refers to my view, which is exactly this:
application(title:'GriffonDemo1', size:[250,50], location:[50,50], locationByPlatform:true) {
label(id:'versionLabel')
}
Having invoked the method on the Spring bean, the code above shows the result in a label in the view, which therefore now looks like this:
One improvement might be to set the application context in the model, via "Initialize.groovy", and then retrieve it in the view, so that everything is nicely centralized, instead of repeating the location/name of the application context, which is what I'm doing above.
It follows from the above that if you use Spring to create your Swing applications, you can code in Groovy and structure your sources according to the strict MVC pattern encouraged by Griffon. I also believe that this could be the basis for Spring RCP integration with Griffon, which implies docking framework support (VLDocking, JIDE, etc) for Griffon.
Sep 28 2008, 02:57:53 PM PDT Permalink
Case Study: Customizing the NetBeans Window System
The NetBeans window system can be used "as is" in, I guess, 90% of cases. However, it's that final 10% that's always the problem! That's when the NetBeans window system becomes, potentially, a little bit painful. Less so, though, when you become aware of the MANY different ways in which you can tweak it. I want to illustrate this point by means of a concrete example.
You're working on a welcome screen with the following requirements:
- It should be undocked when the application starts.
- It should be modal so that when the user clicks on the application behind it, the welcome screen shouldn't disappear behind it.
- It should have a certain default size.
- It should have no buttons or right-click menu items or anything else in its titlebar, except for 'Close'.
- When 'Close' is clicked, it should dock into the right sliding side.
- When it is 'slided in', i.e., when the mouse hovers over the button in the right sliding side, the full welcome screen should be shown, instead of the default slided in size, which is much smaller.
- The application must run on NetBeans Platform 6.1, which means that the previous requirement cannot be solved in the new 6.5 way of doing so (via a client property in the TopComponent constructor).
- When the 'Pin' event is invoked, i.e., when the user clicks the 'Pin' button while the welcome screen is slided in from its position in the right sliding side, it should be docked back into its original undocked mode.
Now, in case you don't realize it, the above puts a lot of strain on the NetBeans window system. In terms of the concepts made available by the NetBeans window system, the above requirements are extremely obtuse. Below I will describe one solution to this case study. I think if you read what follows closely enough, anyone wrestling with the NetBeans window system should gain a lot from it. Just to give a clear impression of the intended end result, below the screenshots show a mock up of the solution. In the first screenshot, you see the welcome screen (empty of content, but that's not the point), exactly as it should appear when the user starts the application:
When the user clicks behind the welcome screen, the welcome screen does not disappear behind the application, i.e., the welcome screen is modal. Next, here's a screenshot that shows the welcome screen when it is closed, i.e., it is never actually closed; it 'closes' into the right sliding side:
When it is opened from its closed state, it should be exactly as shown in the first screenshot.
And the reason for all of the above? The users of the application are either not finding the welcome screen or find that it gets in their way. They can't find the 'undock' functionality very easily and so, whenever the 'editor' mode is occupied by documents (or whatever else is docked there) the welcome screen (which is in the 'editor' mode by default) is hidden and thereby unusable. By making the welcome screen more prominent (as described in the list of requirements above), the user is more likely to find it and also to find it useful. The easy switching between undocked and slided out also aids in this.
A summary of everything that you'd need to do to obtain the above result:
- Create a new mode for the undocked welcome screen. When the application starts, the welcome screen should be modal and undocked. So you need a new mode (i.e., a new position) in the window system, because none of the default modes provide these features. Modes are defined in XML. Here's the new mode for the above:
<?xml version="1.0" encoding="UTF-8"?> <mode version="2.3"> <name unique="welcomeScreen" /> <kind type="view" /> <state type="separated" /> <constraints> <path orientation="vertical" number="0" weight="0.5"/> <path orientation="horizontal" number="1" weight="0.5"/> </constraints> <bounds x="312" y="237" width="679" height="378" /> <frame state="0"/> <active-tc id="DemoTopComponent" /> <empty-behavior permanent="true"/> </mode>Brief explanation of each element: 'name' is used in the layer to hook the welcome screen to this mode; 'kind' specifies that the mode will cause its TopComponents to be modal (instead of 'editor' and 'slidingSide'); 'state' specifies that the mode supports undocked TopComponents; 'constraints' and 'bounds' set position and size; 'frame' sets whether the frame is normal or iconified (or a few others, see the JFrame documentation); 'active-tc' sets which of the TopComponents within the mode will be active by default; 'empty-behavior' sets whether the mode will be destroyed if it is empty (normally, best to always set it to be 'permanent=true').
- Connect the mode to the TopComponent. In the layer, you need this, taking careful note of the section in bold:
<folder name="Windows2"> <folder name="Components"> <file name="DemoTopComponent.settings" url="DemoTopComponentSettings.xml"/> </folder> <folder name="Modes"> <file name="welcomeScreen.wsmode" url="welcomeScreenWsmode.xml"/> <folder name="welcomeScreen"> <file name="DemoTopComponent.wstcref" url="DemoTopComponentWstcref.xml"/> </folder> </folder> </folder> - Use the installer to force the TopComponent into the mode. Despite all of the above, I found that the TopComponent was still not modal. It appeared in the right place, but it wasn't modal. So I forced it via the installer:
public class Installer extends ModuleInstall { @Override public void restored() { WindowManager.getDefault().invokeWhenUIReady(new Runnable() { public void run() { DemoTopComponent demoTC = DemoTopComponent.findInstance(); Mode mode = WindowManager.getDefault().findMode("welcomeScreen"); if (mode != null) { mode.dockInto(demoTC); demoTC.open(); } } }); } } - Override TopComponent.componentClose. Instead of the standard Close behavior, you want the welcome screen to be docked into the right sliding side. The right sliding side is provided by one of the default modes, called 'rightSlidingSide'. You can reuse it as follows:
@Override public void componentClosed() { Mode mode = WindowManager.getDefault().findMode("rightSlidingSide"); if (mode != null) { mode.dockInto(this); this.open(); } }So, whenever Close is clicked, the welcome screen will dock into the right sliding side.
- Handle the Pin event problem. When the user hovers with the mouse over the slided out welcome screen (i.e., when it is docked into the right sliding side), the welcome screen forms a pop up (called 'slided in' in NetBeans terminology). This slided in mode has some problems for our scenario. Firstly, the default size cannot be controlled by our module. In 6.5 this is changed, i.e., in 6.5 each TopComponent can set its slided in size (client property in constructor specifies that TopComponent preferred size should be used when sliding in). However, if we want the whole welcome screen to be shown in 6.1, we'd need to create a new mode. And that's where the TopComponent should be slided out to, instead of the right sliding side. Secondly, when slided in, the TopComponent has a button which, when you hover over it, says 'Pin'. When clicked 'Pin' behaves oddly, in my opinion. It should return our TopComponent to its original undocked modal state, but instead it puts the welcome screen in the 'explorer' view, where it doesn't make any sense.
To get around both these problems, override TopComponent.requestVisible, which is called when the welcome screen is slided in. So, whenever the TopComponent slides in, you will force it into your own custom mode, which is the undocked and modal mode with which we started:
@Override public void requestVisible() { Mode mode = WindowManager.getDefault().findMode("welcomeScreen"); if (mode != null) { mode.dockInto(this); this.open(); } }In this way, the user is never confronted with the 'Pin' event. The user also doesn't ever see the 'slided in' mode, so that the size of the popup is irrelevant.
- Force the menu item in the menu bar to dock the TopComponent into the custom mode. To make sure the custom mode is used when the user chooses the menu item that opens the welcome screen, do something similar to the code shown above, such as the below:
@Override public void actionPerformed(ActionEvent evt) { DemoTopComponent demoTC = DemoTopComponent.findInstance(); Mode mode = WindowManager.getDefault().findMode("welcomeScreen"); if (mode != null) { mode.dockInto(demoTC); demoTC.open(); } } - Remove the actions from the welcome screen titlebar. When the user right-clicks in the titlebar, there's an undock menu item, among others. These are dangerous because you want to control this particular TopComponent as much as possible. So remove them:
@Override public Action[] getActions() { ArrayListactions = new ArrayList (); Action[] retVal = new Action[actions.size()]; actions.toArray(retVal); return retVal; }
And that's "all". Now you have absolute control of your welcome screen and it does exactly what you want it to do. As you can see, modifying the NetBeans window system can be done in various ways—through overriding methods on the TopComponent as well as some carefully thought out XML entries. The trick with the XML is to start up the application, put all the windows where you want them to be at start up, close down the application, and then move the generated XML from the application's user directory into your module's XML files. That, at least, gives a strong starting point, after which your tweaking can begin.
Sep 27 2008, 10:50:03 AM PDT Permalink
From ServiceLoader to Lookup for Griffon
In An Approach to Pluggable Griffon Applications, I discussed how the JDK 6 ServiceLoader class is useful in the context of Griffon. In effect, it provides Griffon with plugins. However, the ServiceLoader doesn't have the concept of 'hot deploy'. Changes made on the classpath are only presented to a Griffon application upon restart. Let's take an alternative approach—instead of the ServiceLoader class, we'll use the Lookup class. Just add org-openide-util.jar from the NetBeans IDE distribution (and then, if that's how you feel, simply delete NetBeans IDE, because that one JAR is all you need) to the 'lib' folder of your Griffon application.
Now, as before, to get started, we need to create a JAR that provides a service. It exposes the service via this very simple service provider interface:
package com.example;
public interface ResultSet {
public int getResult();
}
In this case, for this example, the JAR contains nothing other than the above. In reality, there could be a lot of supporting classes. But all that we need, in essence, is an interface to implement in our service provider.
Now, in our Griffon controller, we'll want to invoke the above method on all the implementations of that interface. However, in addition, wouldn't it be cool if we could set a listener on the result? Then, if the result changes, we'd know about it within our Griffon application. This is something that the ServiceLoader cannot do. As before, from our Griffon Startup.groovy, we call a method in the controller, for loading our service:
def rootController = app.controllers.root rootController.loadService()
However, our service loader method (i.e., in the controller), is now as follows:
def Result res;
def loadService(){
Lookup lkp = Selection.getSelection()
res = lkp.lookupResult(ResultSet.class)
res.addLookupListener(this)
resultChanged(null)
}
The first line in our service loader method refers to a provider class, called 'Selection', which will define our lookup. We haven't defined this provider class yet. And what is a lookup, anyway? A lookup is "a bag of stuff". It is a map, with its keys being objects, with the values being instances of that object. Look at the code above. The 'key' of our map is called 'ResultSet'. That is the object that the service provider will have implemented, since that is the interface with which this blog entry started. And what is a 'Result' object? Well, for that you'll need to know our import statements for the above snippet:
import org.openide.util.Lookup; import org.openide.util.LookupEvent; import org.openide.util.LookupListener; import org.openide.util.Lookup.Result;
The result is the key 'ResultSet', to which we add a LookupListener. Finally, a method resultChanged will be called. This is for the first time we start up our Griffon application, at which point the result will not have changed. After that, whenever a change occurs to the Result, the resultChanged will be called automatically. Why automatically? Because our controller implements the LookupListener class. The only abstract method (i.e., the only required method) for a class implementing LookupListener is resultChanged. And here is the definition of that method:
StringBuilder sb = new StringBuilder()
@Override
public void resultChanged(LookupEvent arg0) {
long mills = System.currentTimeMillis()
Collection extends ResultSet> instances = res.allInstances()
instances.each() {
def nextResult = it.getResult()
sb.append("At " + mills + ": " + nextResult + "\n")
view.encoderList.text = sb.toString()
}
}
For each iteration through the collection of instances of the interface, we add a timestamp to the StringBuilder, together with the next result. This happens whenever a change is made to the instance. Again, "why?" And again, because the Groovy class implements LookupListener, which calls resultChanged whenever a change is made to any of the implementations of the given interface.
Then, in the final line above, the stringified version of the StringBuilder is put into 'view.encoderList.text'. What's that? Firstly, 'view' is defined as the view class (thanks to one of the Griffon configuration files), 'encoderList' is the id property of the JTextArea in the view, and 'text' is the JTextArea's 'text' property. See it in the penultimate line below, which is the complete definition of the view:
import javax.swing.border.TitledBorder
import javax.swing.border.EtchedBorder
application(title:'Encoder Sales', pack:true) {
border = new TitledBorder( new EtchedBorder(), 'Incoming Results')
panel(border:border) {
textArea( id:'encoderList', rows:10, columns:30, editable:false )
}
}
Now we'll create one implementation of the ResultSet interface. We'll do so in a completely new JAR. Create a new Java application, put the JAR that provides the interface on the JAR's classpath and then implement it (and have it created) as follows:
public class Selection {
private Selection() {
}
private static MyLookup LKP = new MyLookup();
//Accessor for the lookup:
public static Lookup getSelection() {
return LKP;
}
//The lookup, which adds new ResultSetImpls to itself:
private static final class MyLookup extends ProxyLookup implements Runnable {
private static ScheduledExecutorService EX = Executors.newSingleThreadScheduledExecutor();
public MyLookup() {
EX.schedule(this, 2000, TimeUnit.MILLISECONDS);
}
private int i;
@Override
public void run() {
//Add to the Lookup a new ResultSetImpl:
setLookups(Lookups.singleton(new ResultSetImpl(i++)));
EX.schedule(this, 2000, TimeUnit.MILLISECONDS);
}
}
//The implementation of the interface:
private static final class ResultSetImpl implements ResultSet {
public int result;
public ResultSetImpl(int i) {
result = i;
}
public int getResult() {
return result;
}
}
}
Now, based on the above ScheduledExecutorService, a new instance of ResultSetImpl is added to the lookup, which is exposed via getSelection(). And that's the method that defined the lookup in the controller:
Lookup lkp = Selection.getSelection()
And when you run the above Griffon application, with the two JARs above on the classpath? The view shows the following:
The view is automatically updated, based on the above ScheduledExecutorService, which specifies that every few milliseconds a new instance of the implementation is created and added to the lookup, on which a LookupListener is set in the controller. Clearly, the key to all of this is that LookupListener. Without it, there would have been no way of knowing that the result is changing every few milliseconds and therefore no way of updating the view. In this way, long running processes can be handled by a separate JAR, with output asynchronously loaded into a Griffon application.
Sep 26 2008, 01:18:16 PM PDT Permalink
Injecting a Splash Screen via Spring into Griffon
Assuming the Spring RCP JARs are in the Griffon application's 'lib' folder, I believe it should be possible to put this into 'Startup.groovy':
import org.springframework.richclient.application.ApplicationLauncher
def startupContextPath = "richclient-startup-context.xml"
try {
new ApplicationLauncher(startupContextPath)
} catch (RuntimeException e) {
println 'Failure'
}
The XML file referred to above has this content:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="splashScreen" class="org.springframework.richclient.application.SplashScreen">
<property name="imageResourcePath" value="splash-screen.jpg" />
</bean>
</beans>
Together with the referenced 'splash-screen.jpg', the above XML file is in the Griffon application's 'griffon-app/resources' folder, which means that when I invoke 'griffon run-app', both these files are included in the JAR. Here's the application structure:
However, sometimes this seems to work and sometimes it doesn't. Sometimes the splash screen is shown and sometimes not. When it is shown, it is always shown very quickly. But often it is not shown. It is these varying results that are the strangest part of it. Possible reason for the problems—I'm using the wrong lifecycle file (though I've tried all of them). Maybe I should be using the controller somehow, but that seems too late, since the splash screen needs to load before the main window of the application. Maybe this in the controller:
def startupContextPath = "richclient-startup-context.xml";
def loadSplash(){
doLater {
try {
new ApplicationLauncher(startupContextPath);
} catch (RuntimeException e) {
println 'Failure'
}
}
}
And then this in 'Startup.groovy':
def rootController = app.controllers.root rootController.loadSplash()
Or maybe I should be loading the application context in a different way. However, since the splash screen is shown sometimes, it possibly means that there's something I'm not doing right in the lifecycle files, maybe need to put something in a special thread? But that's all I can think of.
Once the above works, it'll mean that a Spring bean created via Spring RCP has been successfully injected into a Griffon application. After that it should be investigated if/how the docking frameworks supported by Spring RCP can be used within a Griffon application, which would be very cool. One of the benefits from the perspective of Spring RCP users would be that they'd be able to code their Spring RCP application in Groovy.
Sep 25 2008, 08:56:59 AM PDT Permalink
2nd Attempt at Griffon Support
I got tired of seeing the Grails icon whenever I was working with Griffon, so fashioned my own Griffon icon and then recreated the whole set of modules from scratch, with a completely fresh check out from hg. I.e., the first time I did everything on a hunch: I had the hunch that getting from Grails support to Griffon support would be a matter of branding. This time, I knew that was true, so went about it all a bit more carefully and have come up with a more reliable result. Still a few problems to fix though. Also, the Griffon support should, ideally, work in the same IDE as where Grails is found (though that's not a hard requirement, I think). So, enough things to do.
Meantime, here are the screenshots. You start with the project template:
...which creates your application and opens it inside the IDE. This time, the logical view is much better than before, with more nodes displayed:
There are many menu items on the project node, such as 'Run' and 'Compile' and so on, which all work. Similarly, on the folder-level, you can choose menu items which pop up GUIs for creating the Griffon artifacts, like controller, views, etc, via the actual Griffon scripts in the official Griffon distro that do the work under the hood. I'm hoping to be able to make some kind of distribution available in the Plugin Portal soon.
Sep 24 2008, 01:58:48 PM PDT Permalink
Griffon as a Quick and Dirty Learning Tool
It's not like I'm constantly looking for useful scenarios for Griffon and then blogging about it. It's more like I keep tripping over them accidentally, all the time. I just can't help it! Here's the latest one. Let's say you watched Roumen's very cool Javalobby NetBeans Visual Library Presentation. "Cool, you think, let's learn about that." At some point you put together a piece of code like this:
public class GraphSceneImpl extends GraphScene {
private LayerWidget mainLayer
public GraphSceneImpl() {
mainLayer = new LayerWidget (this)
addChild (mainLayer)
createWidget(mainLayer, LayoutFactory.SerialAlignment.JUSTIFY, 50, 50)
createWidget(mainLayer, LayoutFactory.SerialAlignment.JUSTIFY, 20, 20)
}
private static void createWidget(LayerWidget layer, LayoutFactory.SerialAlignment align, int x, int y) {
Widget widget = new Widget(layer.getScene())
widget.setBorder(BorderFactory.createResizeBorder(8, Color.BLACK, false))
widget.setLayout(LayoutFactory.createVerticalFlowLayout(align, 0))
widget.setPreferredLocation(new Point(x, y))
widget.setPreferredBounds(new Rectangle(200, 200))
widget.getActions().addAction(ActionFactory.createResizeAction())
widget.getActions().addAction(ActionFactory.createMoveAction())
layer.addChild(widget)
}
protected Widget attachNodeWidget(Object node) {return null}
protected Widget attachEdgeWidget(Object edge) {return null}
protected void attachEdgeSourceAnchor(Object edge, Object oldSourceNode, Object newSourceNode) {}
protected void attachEdgeTargetAnchor(Object edge, Object oldTargetNode, Object newTargetNode) {}
}
The widgets, border factory, action factory, and so on, all come from the org.netbeans.api.visual package, which is exactly the point: the Visual Library handles all these concerns for you, you can simply reuse them in your own code. Now, the question is how do you actually try out this code above?
For a start, you're a sloppy programmer, so you've left out all the semi-colons. Hurray. That's Groovy. Normally, to show the above Visual Library scene in a Java application, you'd need to create a Java application, add a container like a JFrame or a JPanel, and then add a JScrollPane, mess around with the ViewportView to which you add the scene's JComponent, via scene.createView(). Phew! That's a lot of work. How about doing it like this instead?:
That's literally the entire view of my application! The frame, the JScrollPane, the JComponent, everything, it's all there... just in a Groovy way, within the context of a simple Griffon application:
Now, when I run the application, I can immediately see, for three lines of code shown above (only one of which I wrote myself, since the other two are part of the output from 'griffon create-app'!), that my code has been correctly hooked up and that it's doing something useful. The resize action is working and so is the move action. The resize border is also how I'd expect it to be:
Furthermore, I can also deploy my small application as an applet, where it works exactly the same way:
In short, Griffon provides a cool prototyping & learning environment which can be used, for example, to focus on the specific APIs you're learning about... because Griffon handles everything else around it for you, from the creation of the application, to its structure, to giving you a placeholder for coding the few Groovy lines needed for hooking the API class into the Griffon application. Pretty cool.
Sep 22 2008, 11:14:24 AM PDT Permalink
Pluggable Applets via ServiceLoaders, Groovy, and Griffon
The first Griffon issue is solved: I made the mistake of including "pack: true", which fails in applets. Now I can start playing a bit with the applet side of Griffon. So, this is the pluggable Griffon application discussed yesterday:
In what way is the application pluggable? Read yesterday's blog entry but, in summary, be aware that none of the items in the list that you see above are defined in the Griffon application. Each item is made available by a different service provider and loaded into the list via the JDK 6 ServiceLoader class. For more details, read yesterday's blog entry. The only change is that I've added a border so that later, when the application is deployed as an applet, I have something to hold on to as I drag the applet out of the browser:
import javax.swing.border.TitledBorder
import javax.swing.border.EtchedBorder
application(title:'Encoder Sales') {
border = new TitledBorder( new EtchedBorder(), 'Available Encoders')
panel(border:border) {
textArea( id:'encoderList', rows:10, columns:30, editable:false )
}
}
Also note that above I removed "pack:true" from the application's properties. And here it is deployed to the web as an applet:
That's possible because when Griffon creates the applet (when you do "griffon run-app"), the value of the applet's archive element is a list of all the JARs on the application's classpath. Those JARs include the service providers (and the service itself), hence the resources defined in META-INF/services are made available to the applet. As a result, applets are pluggable too!
And, of course, one can drag the applet out of the browser (Firefox3 with JDK 6 Update 10):
By the way, did you know that you can close the browser... and the applet will still exist, running independently on your desktop?
Summary: I created the applet by first creating a Swing application in Groovy via the Griffon framework. I used the JDK 6 ServiceLoader class to plug three service providers into my application. Then I ran "griffon run-app" and, in addition to a Swing application, an applet (and a JNLP application) was created by Griffon. However, for the applet to work, I needed to remove "pack:true" from the view. When I ran the applet, I had the same result as the Swing application, i.e., the ServiceLoader loaded the service provider interface, thus letting me call the exposed method on each of the implementations registered in their JAR's META-INF/services folder. Hence, I did nothing at all to create the applet, Griffon simply generated it for me from my Groovy (in the Griffon application) and Java (in the service providers) source code. Among many other things, this example therefore shows yet another scenario where Groovy and Java work seamlessly together.
Sep 21 2008, 07:02:22 AM PDT Permalink
An Approach to Pluggable Griffon Applications
If I were someone evaluating the existing Swing desktop frameworks, I wouldn't hesitate to choose the NetBeans Platform over Griffon or Spring RCP—for one very specific reason: any application built atop the NetBeans Platform is inherently extensible. John O'Conner's definition of extensibility applies here: "An extensible application is one that you can extend easily without modifying its original code base." In this sense, a plugin is not what Grails understands a plugin to be. A Grails plugin is applicable to a developer making use of the Grails framework. For example, the Grails Wicket plugin lets users of Grails incorporate Wicket view technology into the development of a Grails application. I imagine that the Griffon creators have the same definition of "plugin" in mind. Plugins for users of a framework are incredibly useful—users of Grails and, at some point, Griffon, are not limited to the original code that the Grails (and Griffon) creators originally provided, nor do they need to wait for the next release of the framework before making use of their favorite technology in combination with Grails (or Griffon). They're able to create a plugin that extends the Grails (or Griffon) framework and can then merrily continue creating the application of their dreams using the technologies that they're personally most comfortable with.
Those plugins, though, are not the ones I have in mind here. I mean end-user plugins, such as those that users of Firefox can plug into Firefox to add new functionality to it. (Here's a nice opportunity for me to plug the DZone Voting plugin for Firefox. Try it, try it now! It's great.) Similarly, unlike the Griffon framework (and unlike Spring RCP), the NetBeans Platform lets you extend existing applications without changing the original code base, by creating plugins. Does this current gap in Griffon (and Spring RCP) functionality mean it should not be used? Not at all. Since both Griffon and Spring RCP let you create good old Java applications, you can use the JDK 6 java.util.ServiceLoader class and, without the Griffon (and Spring RCP) creators needing to do anything else, you're able to let users of applications built atop these frameworks extend it too! In addition, in each case there are certain benefits in using the ServiceLoader class in these two frameworks (the benefits being distinct and particular to each) that the NetBeans Platform cannot, at least on the face of it, benefit from. However, the NetBeans Platform doesn't make use of the ServiceLoader class, it has an objectively superior approach (which could be reused in Griffon or Spring RCP, by the way), but that's a different story. The short point of this whole story is that you can already create extensible applications in Griffon (and in Spring RCP, but I'll provide a full scenario around that another day).
Here's a condensed step-by-step generic approach to working with ServiceLoader:
- Create a service. A service is a set of classes that exposes features via a public interface.
- Create a service provider. A service provider provides one or more implementations of the service. In order to provide implementations of the service, the service provider needs only to have the JAR that defines the service on its classpath. In other words, the service and the service provider can be (but do not have to be) in different JARs. The service provider is sometimes referred to as an 'extension point'. The service provider can also be seen as a 'plugin'.
- Publish the service provider. A provider configuration file needs to be placed in the service provider JAR's META-INF/services folder. The name of the file needs to match the FQN of the service. Each service provider made available by the JAR needs to be named in the file, by its FQN.
- Distribute the service provider. The service provider JAR needs to be put on the classpath of the application that needs to be extended. The JAR that contains the service needs to be on that classpath too.
- Load the service. Within the application that needs to be extended, a ServiceLoader needs to have been defined. The ServiceLoader will load the service. Then methods defined on the service (i.e., the interface) are invoked, which are called on each of the available service providers, if they have been published as described in step 3 above.
Via the above approach, the application has no direct relationship to any of the service providers. If a service provider isn't there, it simply isn't loaded. A default service provider can be created to handle the situation where no service providers are available.
But... can this work with Groovy? If the answer is "Yes", then Griffon applications are extensible, aren't they, since Griffon is nothing more than strictly structured Groovy code? And what could be the answer other than "Yes", given that Groovy is Java? And that right there is the benefit that Griffon has over the NetBeans Platform when it comes to creating extensible applications—you have the additional option of using Groovy to do so. (However, I guess that one could probably also create NetBeans Platform applications in Groovy, but lets leave those ruminations for another day too.)
I reckon the ServiceLoader Javadoc is very good and so I'll use the example described there in my scenario below. So, based on that (maybe read it all, if you haven't yet, before going further) here's a simple scenario of how everything described above fits together concretely in the context of Griffon:
- Create the application. Run "griffon create-app" and create an application called "EncoderSales". Here's the application (at least, here's how it looks for me in NetBeans IDE, via my tweaked Grails plugins for NetBeans IDE):
So, this is the application that we will deliver to our users. Let's say that it will let the user choose an encoder (for something or other) from a list and then (at some further stage in the application, not covered here) somehow purchase it. However, we want to make it possible for the application to be extensible, so that providers of other encoders can add their encoders to the list. The encoder market is large and growing, one assumes, so we need to let the application be supplemented externally with additional encoder offerings. That's a pretty realistic scenario.
- Create the service. So, and this is unavoidably step 1 of the whole process, we'll create a service. To that end, create a brand new Java application called 'CodecSetService', with an interface named 'com.example.CodecSet' (which is the name of the example service in the Javadoc). The service will define what the set of encoders will consist of, in order for a new encoder to be allowed to be added to the application.
The service could also be created in Groovy, that's neither here nor there, whatever you're comfortable with:
package com.example; public interface CodecSet { public String getEncoder(); }To really simplify things, we'll have one method instead of two and we'll use strings instead of the Encoder/Decoder return types referred to in the Javadoc example.
- Create the service provider. Next, we'll create our first service provider. Remember that a service provider is an implementation of a service. We will create it in a new Java application. Again, we'll follow the example from the Javadoc and call our service provider 'StandardCodecsProvider', with the implementing class being called 'com.example.impl.StandardCodecs'. Again, for now we'll use a Java class for the service provider too.
To fulfil all the requirements for creating a service provider, the 3 bullets that follow will result in a Java application that looks as follows:
- First, build the service and put its JAR on the classpath of the service provider's application. Now that the service is available to the service provider, the latter can implement the former.
- We'll create a very simple implementation (how could it be otherwise, since we're simply returning the name of an encoder):
package com.example.impl; import com.example.CodecSet; public class StandardCodecs implements CodecSet { @Override public String getEncoderName() { return "Standard Encoder"; } } - Finally, in the service provider's application, create a folder structure within 'src', named 'META-INF/services'. Within it, create a file, without any extension, named 'com.example.CodecSet'. Inside that file, write one line and one line only, the content being 'com.example.impl.StandardCodecs' (without the quotes around it).
- Put the service provider on the application's classpath. Now, put both JARs that you've created (i.e., the service JAR, as well as the service provider JAR) in the Griffon application's "lib" folder. Your EncoderSales application should now look as follows:
- Load the service interface. Now we simply need to load the service interface into our Griffon application! Here we go—we use generics to specify the type and are then able to call the "getEncoderName()" on each service provider that is on our classpath and that has been registered according to the META-INF/services approach, as described above:
import com.example.CodecSet class EncoderSalesController { def view def i = 1 def loadService(){ ServiceLoader<CodecSet> sl = ServiceLoader.load(CodecSet.class) sl.each() { view.encoderList.text = view.encoderList.text + "\n" + i++ + ". "+ it.getEncoderName() } } }We call the above from Startup.groovy:
def rootController = app.controllers.root rootController.loadService()
And 'view.encoderList.text' in the controller? What's that all about? That refers to a JTextArea in the view, which is defined as follows:
application(title:'Encoder Sales', pack:true, locationByPlatform:true) { textArea( id:'encoderList', rows:10, columns:30 ) }Run the application. Isn't it beautiful? Here it is:
It's clearly time to distribute your application to all your customers! Do so now.
- Extend the distributed application. Good, your wonderful application is now distributed to your users and they're making use of it and telling you how wonderful it is. Then comes the moment when they'd like to extend it and, for whatever reason (you don't want to create the requested features in your original code base, or you don't have the time to do so, or the customer has some private features that need to be added, i.e., features that are germane to the customer and irrelevant to all the other users). In other words, there's a new encoder to be added to the list. Time to create a new service provider:
- Put the service on your new service provider's classpath.
- Implement the service:
package com.example.impl; import com.example.CodecSet; public class SpecialCodecs implements CodecSet { public String getEncoderName() { return "Special Encoder"; } } - Publish the service provider via MET-INF/services. Your service provider should look very similar to the one discussed earlier, only the implementation is different (and that's exactly the point):
- Distribute the new JAR to to the end users, who need to put it on their EncoderSales application's classpath. Below you can see that I have three service provider JARs, together with the service JAR:
- When the application restarts (which is just one area where the NetBeans Lookup class is superior, in that it has a handy Listener, unlike the ServiceLoader, which means that if the classpath changes dynamically, you will be able to unload and reload objects, which lets you hot-reload JARs without restarting the application, as described by Tim here on Javalobby), i.e., via the reloaded service the new service providers, i.e., those that are on the classpath and registered correctly, are invoked, and they'll provide new entries in the list, each potentially provided by different service providers, all simply as a result of the ServiceLoader loading the service and then having the methods invoked on the service providers:
Only the fourth bullet above, i.e., distribution, is slightly inconvenient. (On the other hand, that's how it works for the Lobo Browser too, last time I checked.) Your granny Smith end user isn't very happy receiving JARs and being told to put them in special places. So, why not create a Plugin Manager in your 'EncoderSales' application? Add a menu item that says 'Plugins' and, when selected, a dialog appears that lets the user browse for the JAR. When they click 'Install', the JAR will simply be put into one of the application's folders (a user directory or even in the installation directory itself), so long as it is on the classpath, which is all that is needed for the ServiceLoader in the application to call its 'getEncoderName' method. (Perhaps the Griffon framework could provide this kind of functionality itself, so that the Griffon user could via a few lines of code simply enable the presence of a Plugin Manager, which is something the NetBeans Platform allows you to do too.)
(By the way, in case you're wondering about this, you can also specify the order of instantiation as well.) And that's how Griffon application are, in fact, extensible. (And, as one should be able to see, Spring RCP too.) So, returning to my original (slightly provocative) statement, extensibility is not a reason for choosing the NetBeans Platform over Griffon or Spring RCP. Yes, there's a little bit of extra work involved, at the moment anyway, but isn't that always the case with plugins?
Postscript: The John O'Conner quote at the start of this story comes from his excellent article Creating Extensible Applications With the Java Platform, which you should definitely read if you haven't already!
Sep 20 2008, 01:12:38 PM PDT Permalink


