Sonntag, 24. März 2013

JavaFX FXMLLoader with OSGi

Using FXMLLoader in OSGi is not completely trivial. This article explains, how to do it. The osgisnippets project contains a project bundle_set5fxml, which is used for that.

Loading an fxml file is done like that:

URL url = this.getClass().getResource("clickme.fxml"); Pane pane = FXMLLoader.load(url);

So let's just try that and see what happens:

18:05:57.927 [JavaFX Application Thread] ERROR - Error loading FXML file javafx.fxml.LoadException: java.lang.ClassNotFoundException: example.set5fxml.impl.ClickmeController at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:726) ~[jfxrt.jar:na] ... at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2685) ~[jfxrt.jar:na] at example.set5fxml.impl.Activator$1.run(Activator.java:63) ~[na:na] ... Caused by: java.lang.ClassNotFoundException: example.set5fxml.impl.ClickmeController at java.net.URLClassLoader$1.run(URLClassLoader.java:366) ~[na:1.7.0_17] ... at java.lang.ClassLoader.loadClass(ClassLoader.java:356) ~[na:1.7.0_17] at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:724) ~[jfxrt.jar:na] ... 17 common frames omitted

The fxml file references the class ClickmeController, which is inside the set5fxml bundle. Obviously FXMLLoader doesn't use a bundle class loader and thus cannot load the controller class. There's a method FXMLLoader.setDefaultClassLoader():

FXMLLoader.setDefaultClassLoader(Activator.class.getClassLoader()); URL url = this.getClass().getResource("clickme.fxml"); Pane pane = FXMLLoader.load(url);

The class loader passed into setDefaultClassLoader must be a bundle class loader, so it should be one from a class which is loaded from this bundle. (See below for more information.) So that worked well enough:

There are a few other things, which must done to make it work. Apache Felix does not export the JavaFX packages by default, so the relevant packages must be added to the list of extra system packages. This is done in the class OSGiFwLoader:

private void prepareConfigMap(Map<Object, Object> configMap) { configMap.put( Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, "example.set2appapi.api, " + "example.set2cmdapi.api, " + "javafx.application, " + "javafx.fxml, " + "javafx.event, " + "javafx.scene, " + "javafx.scene.control, " + "javafx.scene.layout, " + "javafx.scene.paint, " + "javafx.scene.text, " + "javafx.stage, " + "org.slf4j"); ... }

Also the bundle must import the required packages. In the osgisnippets project this is done in the file build.default.properties:

prj.bundle.name=Set5_FXML prj.bundle.import-package=org.osgi.framework, org.slf4j, \ javafx.application, \ javafx.fxml, \ javafx.event, \ javafx.stage, \ javafx.scene, \ javafx.scene.control, \ javafx.scene.layout, \ javafx.scene.paint, \ javafx.scene.text #prj.bundle.export-package=

Coming back to the class loader given to FXMLLoader.setDefaultClassLoader: In the sample bundle, the ClassLoader of the Activator class is used. This works, because the Activator class is part of the bundle and thus loaded by the bundle class loader. What happens, if the class loader of the BundleActivator class is used? An error is logged and the underlying exception with stack trace is this one:

Caused by: java.lang.ClassNotFoundException: example.set5fxml.impl.ClickmeController at java.net.URLClassLoader$1.run(URLClassLoader.java:366) ~[na:1.7.0_17] at java.net.URLClassLoader$1.run(URLClassLoader.java:355) ~[na:1.7.0_17] at java.security.AccessController.doPrivileged(Native Method) [na:1.7.0_17] at java.net.URLClassLoader.findClass(URLClassLoader.java:354) ~[na:1.7.0_17] at java.lang.ClassLoader.loadClass(ClassLoader.java:423) ~[na:1.7.0_17] at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) ~[na:1.7.0_17] at java.lang.ClassLoader.loadClass(ClassLoader.java:356) ~[na:1.7.0_17] at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:724) ~[jfxrt.jar:na]

In osgisnippets and its guitool application, the classes of the OSGi framework (Apache Felix) are loaded by the system class loader. The felix jar is in the classpath of the guitool application. The BundleActivator interface is part of the felix jar and its class loader is the system class loader. However, the system class loader doesn't know about the bundles and it can't find the ClickmeController class. To summarize: Make sure, the class loader passed into FXMLLoader.setDefaultClassLoader() is actually a bundle class loader.

Resources

Samstag, 23. März 2013

JDK8: Unable to Resolve javax.swing ?

The other day I was running an OSGi project with JDK8 and noticed, that the javax.swing package could not be found. This worked in JDK7. Switching to JDK8 in osgisnippets was leading to the following error, when Swing is about to be loaded:

org.osgi.framework.BundleException: Unresolved constraint in bundle example.swing1 [1]: Unable to resolve 1.0: missing requirement [1.0] osgi.wiring.package; (osgi.wiring.package=javax.swing) at org.apache.felix.framework.Felix.resolveBundleRevision(Felix.java:3826) ~[org.apache.felix.main-4.0.2.jar:na] at org.apache.felix.framework.Felix.startBundle(Felix.java:1868) ~[org.apache.felix.main-4.0.2.jar:na] at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:944) ~[org.apache.felix.main-4.0.2.jar:na] at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:931) ~[org.apache.felix.main-4.0.2.jar:na] ...

The reason is simple: A Felix version was used, which does not know about JDK8. Upgrading to a newer version like org.apache.felix.main-4.2.1.jar solves the issue.

The OSGi framework makes java.* packages available automatically. The JDK contains several packages outside the java.* namespace such as javax.swing or org.xml.sax, which are made available too, but specific to the Java version. The felix main jar contains a file default.properties which also defines the exported packages for each Java version. So felix detected jre-1.8, but in the default.properties file of v4.0.2 there is no entry for that. Hence all the extra JRE packages (other than java.*) were not exported.

Resources

Samstag, 16. März 2013

Using JavaFX in a Bundle

If a bundle shall provide a user interface, it could create a Stage, add some components and present it on screen. (For instance assume you built some home automation and a fridge monitoring bundle shall present some feedback when the temperature is too high.)

Create and Dispose the Stage

One approach is to create the Stage when the bundle's start method is called. When the bundle is stopped, it should clean up and thus dispose the Stage. Bundles may have an "Activator" which is an implementation of the BundleActivator interface. The interface defines a start method - which is called by the framework when the bundle is started - and a stop method - which is called by the framework when the bundle is stopped.

The OSGi snippets project contains a "bundle_set4jfx" project which contains exactly one class:

public class Activator implements BundleActivator { Logger logger = LoggerFactory.getLogger(Activator.class); Stage stage; @Override public void start(BundleContext context) throws Exception { logger.info("Set4Jfx Bundle: start()"); Platform.runLater(new Runnable() { @Override public void run() { stage = new Stage(); BorderPane pane = new BorderPane(); Scene scene = new Scene(pane, 400, 200); pane.setCenter(new Label( "This is a JavaFX Scene in a Stage")); stage.setScene(scene); stage.show(); } }); } @Override public void stop(BundleContext context) throws Exception { logger.info("Set4Jfx Bundle: stop()"); Platform.runLater(new Runnable() { @Override public void run() { stage.close(); } }); } }

In the guitool (*), start the framework, install the bundle file named "bundle_set4jfx" from the files list and start it in the bundles list. (Bundle files are on the right hand side and bundles are on the left hand side in the guitool screen. Bundles appear when bundle files are installed.)

(*) The JavaFX ready guitool must be used. It is located in the example.guitool.fx package.

The bundle starts and a Stage is shown:

Pressing the "Stop" button in the guitool stops the bundle and closes the Stage.

To use JavaFX classes, they must be available to the bundle, otherwise a ClassNotFoundException is thrown. The bundle has a MANIFEST file which among other things contains these three lines:

Bundle-Activator: example.set4jfx.impl.Activator Import-Package: org.osgi.framework, org.slf4j, javafx.application, jav afx.scene, javafx.scene.control, javafx.scene.layout, javafx.stage

The entry Bundle-Activator defines the class which is called when the bundle shall be started or stopped. The entry Import-Package defines the java packages which shall be available to the bundle which in this case must include javafx.application, javafx.scene, etc.

Playing with Import-Package

As a little exercise, you may want to change the Import-Package entry and see what happens. The build scripts create the Import-Package entry from information stored in the build.default.properties file in the project. This file is in the root of project bundle_set4jfx. In the line prj.bundle.import-package=org.osgi.framework, org.slf4j, javafx.application, ... remove the javafx.application package and save the file. Then build the bundle by either running build-bundles.xml or the project's build.xml script. Install the bundle and start it. The bundle information will contain "Activator start error in bundle" and the Eclipse console will show a " java.lang.ClassNotFoundException".

Resources

Freitag, 15. März 2013

guitool with JavaFX

Similar to a Swing based guitool, the osgisnippets project contains a JavaFX 2 based guitool. It is located in the project guitool in the package example.guitool.fx . The package is essentially a copy of example.guitool with adjustments to make it work with JavaFX.

Embedding OSGi into JavaFX

The class OSGiFwLoader takes care of the main work of starting and stopping the OSGi framework and managing bundles. In Embedding OSGi the main features of the OSGiFwLoader class are described. There is one major difference between the version there and the JavaFX related version.

The goal is to load bundles which use JavaFX components. To do that, the bundle imports javafx.* packages. These packages must be made available and this is done in OSGiFwLoader.prepareConfigMap():

private void prepareConfigMap(Map<Object, Object> configMap) { // List the packages which shall be exported by the System Bundle. ... configMap.put( Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, "example.set2appapi.api, " + "example.set2cmdapi.api, " + "javafx.application, " + "javafx.scene, " + "javafx.scene.control, " + "javafx.scene.layout, " + "javafx.stage, " + "org.slf4j"); // Each time the OSGi Framework starts, it shall clean up its // storage. I decided to do that, so one gets a clean environment // each time. configMap.put( Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); // This is for application provided BundleActivators: List<Object> list = new ArrayList<>(); // fillSystemBundleActivators(list); configMap.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, list); }

The bold lines let these JavaFX packages be exported by the system bundle and are thus available to the bundles which use JavaFX components. (If further packages are required, they need to be added to the list of packages.)

The JavaFX GUI Tool

The JavaFX gui tool shall be able to start bundles which display Swing components. As described in JavaFX and Swing (on a Mac) some extra steps are necessary to allow Swing components to be used in JavaFX. Instead of the typical Application.launch() call, Swing is started first and JavaFX is embedded there:

public static void main(final String[] args) { startJFXPanel(args); } private static void startJFXPanel(final String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { final JFrame mainFrame = new JFrame("gui tool (s/fx)"); final JFXPanel jfxPanel = new JFXPanel(); mainFrame.getContentPane().add(jfxPanel); mainFrame.setTitle("OSGi Snippets GUI Tool (s/fx)"); mainFrame.setSize(800, 600); mainFrame.setLocationRelativeTo(null); mainFrame.setVisible(true); Platform.runLater(new Runnable() { @Override public void run() { MainFXUI fxui = new MainFXUI(); try { fxui.start(jfxPanel, mainFrame); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); } catch (Exception e) { e.printStackTrace(); } } }); }

Doing these things on a Mac was leading to more issues than under Windows. If it is good enough under the circumstances, the following start method may be used too:

private static void startAWT(String[] args) { System.setProperty("javafx.macosx.embedded", "true"); java.awt.Toolkit.getDefaultToolkit(); Application.launch(MainFXUI.class, args); }

Things are changing a lot currently so this jumping through hoops may not be necessary in the future (JavaFX 1.x reaches EOL, JavaFX 2.x in JDK7 is out for a while, JavaFX 8 is available as early access and parts of JavaFX already went Open Source).

Resources

JavaFX and Swing (on a Mac)

The other day I was taking a look at JavaFX and how it might fit one of my OSGi related projects. As usual things are not as easy as they appear at first sight, so it is time to add JavaFX to osgisnippets.

To use JavaFX with my other project and with osgisnippets, the following goals exist:

  1. Ability to show Swing components
  2. Provide a guitool based on JavaFX like the one based on Swing
  3. Get it to work with OSGi

Goal 1 is covered in this article. The others will be covered in their own articles soon. For the tests I created the class SnipFXSwing. A link to the sources is at the end of the article. In the source archive, it is in the guitool project in package example.guitool.snip.

JFXPanel - embed JavaFX in Swing

This is straight forward, the API doc explains how. (See the Resources section at the end of the article.) Note that JFXPanel is about having a Swing application and starting JavaFX components in it. I need it the other way around.

Preparation

The class SnipFXSwing has a main method in which different start methods are used depending on the command line argument:

public static void main(final String[] args) { if (args.length<1) { System.err.println( "Usage:\n" + "SnipFXSwing fx starts FX GUI\n" + "SnipFXSwing awtfx starts fx after AWT init\n" + "SnipFXSwing swfx starts fx within Swing"); return; } switch (args[0]) { case "fx": startFX(args); break; case "awtfx": startAWTFX(args); break; case "swfx": startSwFX(args); break; default: System.err.println("Error: argument " + args[0] + " not supported"); } }

Approach 1 - Just Try It

Lets just fire up a JavaFX UI and try to create a Swing JFrame. To do that, SnipFXSwing is started with the argument fx. The relevant code is this:

public class SnipFXSwing extends Application { ... private static void startFX(String[] args) { Application.launch(SnipFXSwing.class, args); } ... Stage mainStage; ... ToolBar mainToolBar; Button exitButton; Button showSwingButton; @Override public void start(Stage stage) throws Exception { mainStage = stage; mainStage.setTitle("Snippet JavaFX with Swing"); Scene mainScene = createMainScene(); mainStage.setScene(mainScene); mainStage.setOnCloseRequest(new EventHandler<WindowEvent>() { @Override public void handle(WindowEvent event) { if (mainStage!=null) { doExit(); } } }); mainStage.show(); } }

This is typical source code for small JavaFX applications. The method createScene creates the JavaFX components. This separate method exists, because this part will be reused later. The Stage is the main window and a handler is registered to shut down the application when the window is closed.

The method doShowSwing is called, when the button in the center of the window is pressed:

protected void doShowSwing() { if (!SwingUtilities.isEventDispatchThread()) { // re-run in EDT: SwingUtilities.invokeLater(new Runnable() { @Override public void run() { doShowSwing(); } }); return; } JFrame f = new JFrame("Swing Window"); f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); f.getContentPane().add(new JLabel("This is a Swing JFrame")); f.setSize(400, 200); f.setLocationRelativeTo(null); f.setVisible(true); }

The first if statement checks, whether the method has been called from the Event Dispatch Thread (EDT) and calls it from the EDT if not. The remaining code is the usual way to create a Swing JFrame. When running on Mac OS X and pressing the button "Show Swing Window", this error is logged:

java[10613:707] [JRSAppKitAWT markAppIsDaemon]: Process manager already initialized: can't fully enable headless mode. Exception in thread "AWT-EventQueue-0" java.awt.HeadlessException at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:207) at java.awt.Window.<init>(Window.java:535) at java.awt.Frame.<init>(Frame.java:420) at javax.swing.JFrame.<init>(JFrame.java:224) at example.guitool.snip.SnipFXSwing.doShowSwing(SnipFXSwing.java:231) ...

So that doesn't work. There are a few issues which require, that AWT is initialized before JavaFX. (Link to bug reports in the Resources section.)

BTW: If the if block is commented out and the code is not run in the EDT, the following error is logged:

java[10698:707] [JRSAppKitAWT markAppIsDaemon]: Process manager already initialized: can't fully enable headless mode. Glass detected outstanding Java exception at -[GlassViewDelegate sendJavaMouseEvent:]:src/com/sun/mat/ui/GlassViewDelegate.m:541 Exception in thread "AWT-AppKit" java.awt.HeadlessException at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:207) at java.awt.Window.<init>(Window.java:535) at java.awt.Frame.<init>(Frame.java:420) at javax.swing.JFrame.<init>(JFrame.java:224) at example.guitool.snip.SnipFXSwing.doShowSwing(SnipFXSwing.java:231) ...

On to the next approach.

Approach 2 - Initialize AWT before JavaFX

Some internals require, that AWT is initialized before JavaFX. One approach is this:

private static void startAWTFX(String[] args) { java.awt.Toolkit.getDefaultToolkit(); Application.launch(SnipFXSwing.class, args); }

To try that, the SnipFXSwing class is started with the argument awtfx. Well, as can be seen, nothing is seen. The application hangs and it must be terminated.

A system property is needed:

private static void startAWTFX(String[] args) { System.setProperty("javafx.macosx.embedded", "true"); java.awt.Toolkit.getDefaultToolkit(); Application.launch(SnipFXSwing.class, args); }

Ah! It works :-)

Can we see a Swing window please?

Finally some cannons for the sparrows:

Approach 3 - Embed JavaFX in Swing

If for some reason, the javafx.macosx.embedded property cannot be set, another solution is required. Lets embed JavaFX in Swing so we can embed Swing in JavaFX.

Starting the application:

private static void startSwFX(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { final JFrame frame = new JFrame("Snippet JavaFX with Swing"); final JFXPanel jfxPanel = new JFXPanel(); frame.getContentPane().add(jfxPanel); frame.setSize(600, 400); frame.setLocationRelativeTo(null); frame.setVisible(true); Platform.runLater(new Runnable() { @Override public void run() { SnipFXSwing ui = new SnipFXSwing(); try { ui.start(jfxPanel, frame); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); } catch (Exception e) { e.printStackTrace(); } } }); }

First Swing is started by creating a JFrame. Such things must be done in the EDT, so it is wrapped in SwingUtilities.invokeLater. Basically a JFrame is created and a JFXPanel is added to the frame's content pane.

Now the JavaFX components are created. Similar to Swing with EDT that must be done in the "JavaFX Application Thread". Hence the Platform.runLater call. A different start method is used now, as there is no Stage instance:

public void start(JFXPanel jfxPanel, JFrame frame) throws Exception { this.mainFrame = frame; Scene mainScene = createMainScene(); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(java.awt.event.WindowEvent e) { if (mainFrame!=null) { Platform.runLater(new Runnable() { @Override public void run() { doExit(); } }); } else { super.windowClosing(e); } } }); jfxPanel.setScene(mainScene); }

Similar to the Stage related start method, the Scene is created, however, it is set in the JFXPanel instance. Also the code to handle a closed main window must be slightly different. Instead of a Stage, a JFrame must be observed, which is done with a WindowListener. A little tricky is the call to doExit. As this shall be JavaFX code, it must be run in the JavaFX Application Thread, so again Platform.runLater is used.

The "JavaFX in JFXPanel" main window:

Pressing the "Swing" button:

(*Wiping sweat away...*)

Resources

Donnerstag, 28. Februar 2013

ServiceFactory

There is an interesting interface called ServiceFactory. Citing the documentation (link at end), it

Allows services to provide customized service objects in the OSGi environment.

When registering a service, a ServiceFactory object can be used instead of a service object, so that the bundle developer can gain control of the specific service object granted to a bundle that is using the service.

So what does that mean?

  1. The service object can be created when the consuming bundle asks for it.
    In the normal scenario a service object is created and then registered when the bundle is started. With the ServiceFactory, the consuming bundle asks for the service and then the provider bundle creates the service object. (You don't need to register the newly created service object; the registration was already done with the ServiceFactory instance.)
  2. The ServiceFactory can return service objects which are different for each requesting bundle.
    In the normal scenario a service object is created and used by whichever bundle wants it. Now for each bundle which requests the service, the method ServiceFactory.getService is called and allows to return a new instance.
  3. The ServiceFactory can customize service objects before making them available.
    For instane the ServiceFactory instance can inspect the Bundle which requests the service object (like looking at certain entries in the MANIFEST). Depending on that inspection it can return different types of service objects or service objects with different configurations.

There are some interesting runtime details too:

  1. Service objects are cached by the framework.
    When a bundle requests a service, it gets delivered a service object. The next request call will deliver the same service object (unless it has been released in between). When the consuming bundle calls ungetService to tell that it doesn't need the service object anymore, the method ServiceFactory.ungetService is called by the framework. (Actually ungetService must be called as many times as getService, the framework maintains a use count.)

However, for a second bundle, the service objects of the first bundle are not reused by the framework. If two bundles retrieve a service and thus use the ServiceFactory, its getService method gets called for each of the bundles.

Samples in osgisnippets Project

The project osgisnippets contains a set of bundles with the name starting with set3. set3fac provides services using the ServiceFactory. set3cons1 and set3cons2 are two almost identical bundles, which consume the service. They create a window with a get and an unget button. The ServiceFactory provides a unique ID for each service object. The ID of the service object is shown by the consuming bundles in their window.

Resources

Error: type ServiceReference does not take parameters

When developing for an OSGi environment and especially when using Apache Felix derived projects, one may run into a strange error. It is strange, because inside Eclipse no errors are shown, but when using the Java compiler (javac), it complains like so:

[javac] .../Activator.java:53: error: type ServiceRegistration does not take parameters [javac] ServiceRegistration svcReg;

A couple of circumstances need to exist to trigger this error:

  • JDK7 is used
  • Apache Felix 4.0.2 or 4.0.3 is used
  • javac is used to compile the project

OSGi code is compiled with the -jsr14 option and that leads to problems with the JDK7 compiler. The Eclipse JDT compiler compiles it well, and therefore there is no error when building only in Eclipse.

There are updated OSGi jars available now which fix this problem (and one doesn't need to recompile). These are org.osgi.core-4.3.1.jar and org.osgi.compendium-4.3.1.jar. They are on OSGi's site or on the maven sites. (See below)

My fix for an earlier project was to build the OSGi sources (org.osgi.* packages) with JDK7 and put them before the Felix jars in the classpath. This makes the new OSGi class files being used instead of the ones which are packaged with the Felix jars.

Resources

Samstag, 26. Januar 2013

Consuming Services in Embedding Applications from Bundles

When embedding an OSGi framework, usually the application provides some services to bundles or consumes some services from bundles. In this article I describe how the application consumes services from bundles.

The Service

The bundles shall be able to provide additional commands to the application. These commands will be registered in the OSGi framework as services. To do that, an interface shall be defined under which the services are available. This interface is defined in the bundle example-set2cmdapi.

public interface Command extends Action { /* No additional features */ }

The commands implement the javax.swing.Action interface. To keep things simple, a Command interface is created which just extends javax.swing.Action.

Bundles provide commands by implementing the Command interface and registering the instances in the OSGi framework under that interface.

The Service Implementation in the Bundle

The bundle example-set2svc1 contains a BundleActivator instance. When the bundle is started, it creates Command instances and registers them in the OSGi framework. The Activator class is shown below:

public class Activator implements BundleActivator { Logger logger = LoggerFactory.getLogger(Activator.class); List<ServiceRegistration<Command>> svcRegs = new ArrayList<>(); List<Command> svcs = new ArrayList<>(); @Override public void start(BundleContext context) throws Exception { logger.info("Set2Svc1 Bundle: start()"); svcs.add(new HelloAction()); svcs.add(new HelloAction2()); for (Command cmd : svcs) { ServiceRegistration<Command> svcReg = context.registerService(Command.class, cmd, null); svcRegs.add(svcReg); } } @Override public void stop(BundleContext context) throws Exception { logger.info("Set2Svc1 Bundle: stop()"); for (ServiceRegistration<Command> svcReg : svcRegs) { svcReg.unregister(); } svcRegs.clear(); svcs.clear(); } static class HelloAction extends AbstractAction implements Command { (... creates a JDialog and shows it ...) } static class HelloAction2 extends AbstractAction implements Command { (... creates a JDialog and shows it ...) } }

In the start method, the bold lines register a Command instance in the OSGi framework under the Command interface. In the stop method the service is unregistered.

Using the Service in the Application

The application collects all services under the Command interface from the OSGi framework and adds the instances to the toolbar. To do this, it uses a ServiceTracker. This is a very convenient class which observes the service registrations in the framework and notifies when things change.

Without ServiceTracker the application would need to take care of services being added and removed throughout the applications lifetime. Given that an OSGi setup is multithreaded and concurrent modifications are likely to occur, deadlocks are waiting around each corner. One might think about polling the services at regular intervals, however, all this is not really easier. ServiceTracker is a very handy class provided by the OSGi framework and it pays off quickly to use it.

The class CommandObserver takes care of looking up the services for the application. It is located in the package example.guitool.impsvc.

public class CommandObserver { final JToolBar toolBar; final Map<Command, JButton> cmdButtonMap = new HashMap<>(); final CT ct; public CommandObserver(JToolBar toolBar, BundleContext context) { super(); this.toolBar = toolBar; this.ct = new CT(context); } public void open() { ct.open(); } public void close() { ct.close(); } public void addCommand(final Command cmd) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JButton b = toolBar.add(cmd); cmdButtonMap.put(cmd, b); } }); } public void removeCommand(final Command cmd) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JButton b = cmdButtonMap.remove(cmd); if (b!=null) { toolBar.remove(b); toolBar.revalidate(); } } }); } class CT extends ServiceTracker<Command, Command> { public CT(BundleContext context) { super(context, Command.class, null); } @Override public Command addingService(ServiceReference<Command> reference) { Command cmd = super.addingService(reference); addCommand(cmd); return cmd; } @Override public void modifiedService(ServiceReference<Command> reference, Command service) { super.modifiedService(reference, service); } @Override public void removedService(ServiceReference<Command> reference, Command service) { super.removedService(reference, service); removeCommand(service); } } }

The CommandObserver class creates an instance of the CT class which is a subclass of ServiceTracker. Three methods are overridden. These three methods are called by the ServiceTracker when services are added, modified or removed. The CT class just calls addCommand and removeCommand of the CommandObserver class from within these three methods.

When a Command service is added, the Command instance is added to the toolbar. When a Command service is removed, it must be removed from the JToolBar. However, the JToolBar allows adding Action instances, but does not support removing them. When an Action is added with JToolBar.add(), the method returns a JButton instance which wraps the Action. The CommandObserver class stores these JButton instances in a map. When a Command shall be removed, the matching JButton is fetched from the map and used to remove the Action from the JToolBar.

Running

Time to try it. When the guitool is started, the toolbar at the bottom of the window doesn't contain any buttons. When the bundle file example-set2svc1.jar is installed and started, two buttons will appear on that toolbar.

When the bundle is stopped, the buttons will disappear.

Bundle Wiring

Since the CommandRegistry service is provided by the application (guitool) which embeds the OSGi framework, additional steps must be taken for the visibility of the packages.

The bundle example-set2svc1 imports the package example.set2cmdapi.api which contains the interface Command. The guitool application must have this package and interface on its classpath. A wiring between the bundle example-set2svc1 and the system classpath must be established now.

There is a configuration entry in the OSGi framework, which "connects" the system classloader with bundle classloaders. The OWGiFwLoader class contains a method prepareConfigMap, which is shown below:

private void prepareConfigMap(Map<Object, Object> configMap) { ... // List the packages which shall be exported by the System Bundle. configMap.put( Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, "example.set2appapi.api, " + "example.set2cmdapi.api, " + "org.slf4j"); ... }

The entry Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA defines the packages which are exported by the system bundle. So if a bundle wants to import the package example.set2cmdapi.api, it does not "connect" to another bundle's classloader, but to the system bundle and thus to the system classloader. (The system classloader is the parent classloader of the system bundle.)

Since the classes of the bundle example-set2cmdapi are already available with the system classloader, the bundle does not need to be installed. This can be checked by starting the bundle set2svc1 without installing set2cmdapi. It will start and register the commands in the toolbar.

Bundle Wiring Details

The following diagram describes the bundle wiring when two bundles are involved and the embedding application does not need to access the package. The diagram is using the conventions described in the OSGi spec.

Gray boxes represent bundles. White boxes with package names indicate, that the package is required by the bundle; black boxes provide the package. Hence a wiring exists between the white and the black box, because one bundle uses the package which is provided by the other.

The class Activator uses the Command interface for the HelloAction and HelloAction2 classes, and to register the Actions in the OSGi framework under that interface.

Things get a bit more complicated when the embedding application uses or provides the package. The following diagram shows among other things the wiring when the application consumes Command service from other bundles.

The application is started as a standard Java application without knowing about OSGi initially. The CommandObserver class uses the Command interface and thus this interface must be on the classpath. This is the system classpath. The package example.set2cmdapi.api is denoted as a black box in the gray application box.

When the OSGi framework is started, the framework acts as the system bundle. The system bundle has its own classloader just like any other bundle has its own classloader. The configuration entry FRAMEWORK_SYSTEMPACKAGES_EXTRA declares packages which shall be exported by the system bundle. For these packages the system bundle sets the parent classloader to the system classloader. So it "forwards" the wiring from the white package box in bundle example-set2svc1 to the black package box which actually is available by the system classloader.

The bundle example.set2cmdapi is not needed in this scenario, because the package is already provided. Hence it is "crossed out" in the diagram.

Note: In the guitool project the set2cmdapi bundle jar file is simply added to the classpath. It is used as a standard jar file and the extra information about OSGi is ignored this way. It will not appear as a bundle in the OSGi frame just by being on guitool's classpath.

Resources

Providing Services from Embedding Applications to Bundles

When embedding an OSGi framework, usually the application provides some services to bundles or consumes some services from bundles. In this article I describe how the application provides services to bundles.

The Service

The application shall offer a service so bundles can register additional commands. The interface is shown below. It is defined in the sub project bundle_set2appapi.

public interface CommandRegistry { abstract void addCommand(Action action); abstract void removeCommand(Action action); }

Bundles provide commands by implementing the interface javax.swing.Action and registering them with the CommandRegistry.addCommand method.

The Service Implementation in the Application

The application (which is the guitool project) implements the CommandRegistry interface. The implementing class is CommandRegistryImpl in the package example.guitool.expsvc:

public class CommandRegistryImpl implements CommandRegistry { final JToolBar toolBar; final Map<Action, JButton> actionToButtonMap = new HashMap<>(); ServiceRegistration<CommandRegistry> svcReg; public CommandRegistryImpl(JToolBar toolBar) { super(); this.toolBar = toolBar; } public void open(BundleContext context) { svcReg = context.registerService(CommandRegistry.class, this, null); } public void close() { svcReg.unregister(); } @Override public void addCommand(final Action action) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JButton b = toolBar.add(action); actionToButtonMap.put(action, b); } }); } @Override public void removeCommand(final Action action) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JButton b = actionToButtonMap.remove(action); if (b==null) return; toolBar.remove(b); toolBar.revalidate(); } }); } }

The application creates an instance of CommandRegistryImpl and calls the open method. The open method registers the instance (note the second argument this to the registerService call) under the interface CommandRegistry in the OSGi framework. The returned ServiceRegistration is later used to unregister again.

The CommandRegistryImpl adds the Action instances to a JToolBar. The JToolBar can only remove Component instances and not Actions. The JToolBar.add() method returns a JButton instance which wraps the Action. This JButton is stored in the actionToButtonMap so it can be looked up when the Action shall be removed. The revalidate() is needed to let the JToolBar update its appearance after the remove() call.

Note that since the addCommand and removeCommand methods can be called from outside the EDT, the actual work is scheduled to run on the EDT with the SwingUtilities.invokeLater method.

The class CommandRegistryImpl is used like this:

commandRegistry = new CommandRegistryImpl(pluginCmdToolBar); ... commandRegistry.open(osgiFwLoader.getBundleContext());

Using the Service in a Bundle

The bundle bundle_set2cons1 uses the CommandRegistry service. When the bundle is started, the CommandRegistry service is looked up and the Action instance is added with the CommandRegistry.addCommand() call. When the bundle is stopped, the Action instance is removed. The methods for starting and stopping a bundle are implemented in the bundle activator. The source code is shown below:

public class Activator implements BundleActivator { Logger logger = LoggerFactory.getLogger(Activator.class); Action action; @Override public void start(BundleContext context) throws Exception { logger.info("Set2Cons1 Bundle: start()"); action = new HelloAction(); ServiceReference<CommandRegistry> svcRef = context.getServiceReference(CommandRegistry.class); if (svcRef!=null) { try { CommandRegistry cmdReg = context.getService(svcRef); if (cmdReg!=null) { cmdReg.addCommand(action); } } finally { context.ungetService(svcRef); } } } @Override public void stop(BundleContext context) throws Exception { logger.info("Set2Cons1 Bundle: stop()"); ServiceReference<CommandRegistry> svcRef = context.getServiceReference(CommandRegistry.class); if (svcRef!=null) { try { CommandRegistry cmdReg = context.getService(svcRef); if (cmdReg!=null) { cmdReg.removeCommand(action); } } finally { context.ungetService(svcRef); } } action = null; } static class HelloAction extends AbstractAction { // details omitted for brevity // (creates a JDialog and presents it on screen.) } }

In the bold lines the CommandRegistry service is fetched and CommandRegistry.addCommand() (in Activator.start()) and CommandRegistry.removeCommand() (in Activator.stop()) are called. Note that the returned ServiceReference may be null, which means, that no CommandRegistry service is available right now. Also note that after the CommandRegistry service is not needed anymore, the ungetService method is called, to let the OSGi framework know, that the service is not needed.

Running

Time to try it. When the guitool is started, the toolbar at the bottom of the window doesn't contain any buttons. When the bundle file example-set2cons1.jar is installed and started, a button will appear on that toolbar.

When the bundle is stopped, the button will disappear.

Bundle Wiring

Since the CommandRegistry service is provided by the application (guitool) which embeds the OSGi framework, additional steps must be taken for the visibility of the packages.

The bundle example-set2cons1 imports the package example.set2appapi.api which contains the interface CommandRegistry. The guitool application must have this package and interface on its classpath. A wiring between the bundle example-set2cons1 and the system classpath must be established now.

There is a configuration entry in the OSGi framework, which "connects" the system classloader with bundle classloaders. The OWGiFwLoader class contains a method prepareConfigMap, which is shown below:

private void prepareConfigMap(Map<Object, Object> configMap) { ... // List the packages which shall be exported by the System Bundle. configMap.put( Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, "example.set2appapi.api, " + "example.set2cmdapi.api, " + "org.slf4j"); ... }

The entry Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA defines the packages which are exported by the system bundle. So if a bundle wants to import the package example.set2appapi.api, it does not "connect" to another bundle's classloader, but to the system bundle and thus to the system classloader. (The system classloader is the parent classloader of the system bundle.)

Since the classes of the bundle example-set2appapi are already available with the system classloader, the bundle does not need to be installed. This can be checked by starting the bundle set2cons1 without installing set2appapi. It will start and register the command in the toolbar.

Bundle Wiring Details

The following diagram describes the bundle wiring when two bundles are involved and the embedding application does not need to access the package. The diagram is using the conventions described in the OSGi spec.

Gray boxes represent bundles. White boxes with package names indicate, that the package is required by the bundle; black boxes provide the package. Hence a wiring exists between the white and the black box, because one bundle uses the package which is provided by the other.

The class Activator uses the CommandRegistry interface to add and remove Action instance by calling CommandRegistry.addCommand and CommandRegistry.removeCommand.

Things get a bit more complicated when the embedding application uses or provides the package. The following diagram shows among other things the wiring when the application provides the CommandRegistry service to other bundles.

The application is started as a standard Java application without knowing about OSGi initially. The CommandRegistryimpl class implements the CommandRegistry interface and thus this interface must be on the classpath. This is the system classpath. The package example.set2appapi.api is denoted as a black box in the gray application box.

When the OSGi framework is started, the framework acts as the system bundle. The system bundle has its own classloader just like any other bundle has its own classloader. The configuration entry FRAMEWORK_SYSTEMPACKAGES_EXTRA declares packages which shall be exported by the system bundle. For these packages the system bundle sets the parent classloader to the system classloader. So it "forwards" the wiring from the white package box in bundle example-set2cons1 to the black package box which actually is available by the system classloader.

The bundle example.set2appapi is not needed in this scenario, because the package is already provided. Hence it is "crossed out" in the diagram.

Note: In the guitool project the set2appapi bundle jar file is simply added to the classpath. It is used as a standard jar file and the extra information about OSGi is ignored this way. It will not appear as a bundle in the OSGi frame just by being on guitool's classpath.

Resources

Samstag, 19. Januar 2013

Using Swing in a Bundle

If a bundle shall provide a user interface, it could create a JFrame, add some components and present it on screen. (For instance assume you built some home automation and a fridge monitoring bundle shall present some feedback when the temperature is too high.)

Create and Dispose the JFrame

One approach is to create the JFrame when the bundle's start method is called. When the bundle is stopped, it should clean up and thus dispose the JFrame. Bundles may have an "Activator" which is an implementation of the BundleActivator interface. The interface defines a start method - which is called by the framework when the bundle is started - and a stop method - which is called by the framework when the bundle is stopped.

The OSGi snippets project contains a "bundleswing1" project which contains exactly one class:

public class Activator implements BundleActivator { Logger logger = LoggerFactory.getLogger(Activator.class); volatile JFrame frame; @Override public void start(BundleContext context) throws Exception { logger.info("Swing1 Bundle: start()"); // All access to Swing must be on the EDT. This start method // may have been called outside the EDT, so the JFrame // creation must be put into the EDT. And this is how // it is done: SwingUtilities.invokeLater(new Runnable() { @Override public void run() { frame = new JFrame("Swing1 Bundle"); Container contentPane = frame.getContentPane(); contentPane.setLayout( new BoxLayout(contentPane, BoxLayout.PAGE_AXIS)); contentPane.add(new JLabel( "JFrame created in Activator.start(), stop bundle to dispose")); contentPane.add(new JButton(new HelloAction())); contentPane.add(new JButton(new HelloSharedOwnerAction())); frame.setSize(300, 200); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); } @Override public void stop(BundleContext context) throws Exception { logger.info("Swing1 Bundle: stop()"); // Access Swing in EDT - see comment in start method: SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (frame!=null) { frame.dispose(); frame = null; } } }); } ... (two action classes) }

In the guitool, start the framework, install the bundle file named "bundleswing1" from the files list and start it in the bundles list. (Bundle files are on the right hand side and bundles are on the left hand side in the guitool scteen. Bundles appear when bundle files are installed.)

The bundle starts and a JFrame is shown:

Pressing the "Stop" button in the guitool stops the bundle and closes the JFrame.

To use Swing classes, they must be available to the bundle, otherwise a ClassNotFoundException is thrown. The bundle has a MANIFEST file which among other things contains these two lines:

Bundle-Activator: example.swing1.impl.Activator Import-Package: org.osgi.framework, org.slf4j, javax.swing

The entry Bundle-Activator defines the class which is called when the bundle shall be started or stopped. The entry Import-Package defines the java packages which shall be available to the bundle which in this case must include javax.swing.

Playing with Import-Package

As a little exercise, you may want to change the Import-Package entry and see what happens. The build scripts create the Import-Package entry from information stored in the build.default.properties file in the project. This file is in the root of project bundleswing1. In the line prj.bundle.import-package=org.osgi.framework, org.slf4j, javax.swing remove the javax.swing package and save the file. Then build the bundle by either running build-bundles.xml or the project's build.xml script. Install the bundle and start it. The bundle information will contain "Activator start error in bundle" and the Eclipse console will show " java.lang.ClassNotFoundException: javax.swing.JLabel not found by example.swing1 [2]".

The Shared Owner Frame

When creating a JDialog with a null owner, Swing creates a so called "shared owner frame". This will prevent a clean exit, when - as usual - System.exit shall not be used.

The JFrame contains two buttons. The first refers to an Action which uses JOptionPane.createDialog with the main JFrame as owner. As a test, do these steps:

  1. Start guitool
  2. Start Framework
  3. Install and start bundleswing1
  4. Press the "Hello" button (a message box will apear - do not close it!)
  5. Stop the bundle
  6. Exit guitool
  7. The application will terminate and the JVM exits. This is the expected behaviour.

The class HelloSharedOwnerAction looks like shown below. Interesting are the lines marked with bold font. When passing null to createDialog, Swing sets a Shared Owner Frame as parent of the JDialog. The two bold lines in the propertyChange method fetch the owner and dispose it.

class HelloSharedOwnerAction extends AbstractAction { private static final long serialVersionUID = 1L; public HelloSharedOwnerAction() { super(); putValue(NAME, "Hello Shared Owner"); } @Override public void actionPerformed(ActionEvent e) { JOptionPane p = new JOptionPane( "Hello there! My owner is the Shared Owner Frame"); final JDialog dlg = p.createDialog(null, "Hello"); dlg.setModalityType(ModalityType.MODELESS); p.addPropertyChangeListener("value", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { dlg.dispose(); Window parent = dlg.getOwner(); parent.dispose(); } }); dlg.setVisible(true); } }

As a test, comment these two lines out (the ones around parent.dispose()) and repeat the steps listed above, except that the button "Hello Shared Owner" is used. The guitool will disappear when pressing "Exit", but the JVM will not terminate. It does so, because a displayable component still exists. The two commented lines were disposing that component.

Resources