Swing & OSGi — please play nice!


In a recent blog by Peter Karich, he showed how to create a pluggable Swing application using OSGi. While this works fine for smaller examples, you might run into more serious issues once you application starts to grow.

Plugging Swing: it leaks?

Let’s start with an application not unlike the one from aforementioned blog; it uses a window as host, and has a pluggable menu, and a pluggable table.

SWING_OSGI_Pluggable components600

You can find the code we used at the end of this entry (or, for the impatient, here).

Using this pluggable system, we could end up with several curious situations. For instance, you might have a mixed look and feel in you application.

SWING_OSGI_wrong_menus

Or worse, you might end up with a UI that (sometimes) fails to start, and spits a stacktrace your way.

swing_osgi_NPE

It leaks, but why?

Our host, and all components have been stored in separate bundles, meaning we don’t have full control about the order in which actions are performed (more about that later). However, we do know there are orders of execution that are less than ideal; let’s force one of those.

The project contains an Ant script to make things easier. From the root of the extracted project, run

$ > ant run1

This starts the framework, installing the necessary bundles, but does not start them (note that this step uses Pax Runner, and therefore needs internet access). We can now start our bundles in the order we like.

A tale of two look-and-feels

After starting the framework, wait for the “Welcome to Felix” message, and run

     [java] Welcome to Felix
     [java] ================
     [java]
start 2
start 1

The situation arises because the look and feel is a static concept in Swing. The menu bundle creates its JMenu before (see Menu.java, ln 30) the host sets its look and feel (Host.java, ln 51), and keep that look and feel, even when the host bundle changes it later.

Tables, ScrollPanes and NPEs

The NullPointerException above is a different story, but it goes back to the same staticness of Swing too. To force this situation, start only bundle 4.

     [java] Welcome to Felix
     [java] ================
     [java]
start 4
     [java] Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
     [java]      at net.luminis.swingosgi.part1.scrolltable.impl.TableComponent$1.run(TableComponent.java:31)
     [java]      at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
     [java]      at java.awt.EventQueue.dispatchEvent(EventQueue.java:633)
     [java]      at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:296)
     [java]      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:211)
     [java]      at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:201)
     [java]      at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:196)
     [java]      at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:188)
     [java]      at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)

Let’s take a look at the line where this NPE happens:

JScrollPane scrollPane = new JScrollPane(table);
scrollPane.getColumnHeader().setBackground(Color.blue);
m_panel.add(scrollPane);

We know that the ColumnHeader is null. This is because its JTable’s responsibility to create the header, but this is only done once the table knows it is part of an AWT hierarchy. The following lines come from the 1.5 JDK on a Mac; configureEnclosingScrollPane() creates the column header. This addNotify method comes from Component, and notifies of, exactly, the event of being added to an AWT container.

public void addNotify() {
  super.addNotify();
  configureEnclosingScrollPane();
}

Order, order!

So, the static nature of Swing and the dynamic nature of OSGi seem to hurt each other seriously here.

One way to get the application right is by fixing the order in which Swing components can be created. By starting bundle 1 first in our application, we at least fix the look and feel. Getting the scrolling table to run correctly is an entirely different story.

Regarding order, a few possible solutions spring to mind immediately,

  1. Put all UI stuff in one bundle
  2. Use OSGi bundle start levels

Sure, all UI in a single bundle will give you the control necessary, but it also defeats the purpose. OSGi start levels can at least solve the ordering issues, but will not get you out of the NullPointerException and might have more impact than you desire.

What order?

As we have seen, absolute order does not solve our problem. How about separating creation and initialization? Still, we need to impose some order, or at least some hierarchy.

SWING_OSGI_Application composition600

We represent each Swing component by an OSGi service, and leverage the OSGi service dependency resolution to build up our hierarchy; this way, we know the host service will be started last.

  1. Resolve services Once the host bundle starts, we know all components are locked and loaded; the host can now start setting up Swing’s static elements like the look and feel.
  2. Create components Component creation ripples downward: the host gets its direct children, adding them to its container, and in the process triggering the children to get their child components.
  3. Initialize components Once the component creation is done, the host instructs each component to initialize; we can now be certain that all components are part of the AWT hierarchy.

To reach this situation, we introduce a new OSGi service that wraps the component.

SWING_OSGI_Using component provider600

All components are handled by a service implementing ComponentProvider; notice how methods are required to be called on the EventDispatchThread, making sure that all components are created on the EDT, while retaining the order necessary.

public interface ComponentProvider {
 /**
 * Constant to identify ComponentProvider services.
 */
 public static final String COMPONENT_ID_KEY = "component.id";
 
 /**
 * This function should always be called from the EDT. The implementor
 * may assume that this function is called once and before {@link #addedToContainer()}
 *
 * @return the implementors (Swing) component which it provides.
 */
 public JComponent getComponent();
 
 /**
 * Triggered when the component is added to a container. The implementation
 * can validate some stuff. This function must be called on the EDT.
 * Implementors may assume this function is called after {@link #getComponent()}.
 */
 public void addedToContainer();
}

The getComponent function is analogous to the create step above; the addedToContainer triggers the initialize.

Let’s try that out!

To check that this actually works OK, run

$ > ant run2

from the root of the project, and start the bundles in any order you like. The UI will only show up once all required components are available; notice that the Table and the ScrollPane component can be used interchangeably.

Is it all good?

For the most part, yes. You do give up some flexibility: the UI is assembled at runtime, but it is no longer possible to (easily) plug components into a running system without special provisions. Then again, how often do you deploy new Swing-based functionality to a running application?

In the example application, we use ServiceTrackers to keep track of the components needed by the host. In a real system, you should consider using some dependency management mechanism; we have used the Apache Felix Dependency Manager in the past.

The project and the story

The project mentioned above is available as a zipped Eclipse project. You can directly import this into Eclipse, or just unzip it and run the Ant build file.

To run the examples, you will need Apache Ant. Also, since we use Pax Runner, you will need an internet connection.

The presentation we gave about this at Devoxx 09 is at SlideShare.

, ,

  1. #1 by dead_devil_66 at February 8th, 2011

    Greetings.

    Is it possible to run this example just by using Apache Servicemix???

    Thanks in advance

  2. #2 by Angelo van der Sijpt at February 8th, 2011

    Hi,

    I guess you could, but there’s no need for that; just run the ant build and you’ll be fine.

    Can I ask why you would want to run this in ServiceMix?

  3. #3 by dead_devil_66 at February 8th, 2011

    because i need to deploy some services over servicemix and add some GUI elements to control some of the services.

    by the way, how do i run the ant script, in Windows??? Sorry but i can’t find the script (im a noob with with OSGi)

  4. #4 by dead_devil_66 at February 8th, 2011

    example: i have several services and i want a console for each one. So, i’m associating streams, Jframe’s and JTextPane’s to each service.

  5. #5 by Angelo van der Sijpt at February 8th, 2011

    The zip above contains, among other files, a build.xml, which you can run with Ant. You can install Ant from the link in the article, or you can just import the project into Eclipse and right-click on the build.xml. This will build the example bundles.
    Note that the bundles are meant as an illustration of the principle outlined above, and you are free to base your own code on it; they are not a framework or general solution.

    The approach above is intended for a situation in which you know at startup-time what components will be in your UI; you give up runtime dynamism for a simpler startup. If I understand you correctly, you want to dynamically generate components, and plug them into an existing UI. This is very well possible, also in OSGi, but the approach above won’t help you.

  6. #6 by dead_devil_66 at February 8th, 2011

    no no. I just want to generate a console for each service that i start.

  7. #7 by dead_devil_66 at February 8th, 2011

    sorry but…right click and what option do i choose???

  8. #8 by Angelo van der Sijpt at February 9th, 2011

    To build the project, right-click the build.xml and select ‘Run As -> Ant Build’. This will build you the bundles, ready for deployment.

(will not be published)