Maven + OSGi + Spring + JavaFX (+Ant)

We had to go through a number of steps to support the combination of Maven + OSGi + Spring + JavaFX-1.1 in our Swing-based application. This document describes all the steps and technology we’ve gone through to get it working.

! With JavaFX 1.2, some of the steps have changed. I will post a new entry covering the differences shortly.

Build / Compile

We use Maven as our build system, so JavaFX compilation must coexist with these.

Upload Dependencies to a Maven repository

First, we took every single Jar file in the JavaFX SDK 1.1 and uploaded them into Nexus, our development Maven repository. We took all jars from lib/shared and lib/desktop, uploaded them with groupID of javafx, artifactID matching the Jar name, version 1.1.

Building alongside Java code

To combat an explosion of modules, we determined that we wanted to have a src/main/fx source folder that is compiled and used right alongside src/main/java and src/main/resources. Furthermore, the JavaFX code needs to be able to refer to classes and interfaces in the same module as well as in any maven dependencies.

Our first attempt was to use the seemingly perfect fit, the m2-javafxc maven plugin. Early attempts seemed workable, but things started to fall apart and we had to abandon. Here are some of the issues:

  1. m2-javafxc is based on the maven-plexus-compiler. This compiler will only run once per module. So we were unable to have src/main/java and src/main/fx side-by-side
  2. m2-javafxc filters out anything other than .fx files. So we couldn’t have java and fx intermingled inside a single source folder.
  3. Unfortunately, m2-javafxc is supported by only one individual, and barely used by the community. While the codebase is not large or complicated, this does represent a bit of a risk.

Next, we we noticed that the SDK includes an ant task, something easily runnable in maven. So, we built up a working ant script. If you look carefully, you will notice it takes three parameters. These are covered below.

<project name="compile-jfx" basedir="." xmlns:artifact="urn:maven-artifact-ant">

	<target name="compile.jfx" depends="check-if-should-build" if="should-build">
		<path id="javafxc.class.path">
			<fileset dir="${localRepo}">
				<include name="javafx/**/${javafx.version}/*-${javafx.version}.jar" />
			</fileset>
			<pathelement path="${compile.classpath}" />
		</path>

		<echo message="localRepo: ${localRepo}" />
		<echo message="jfx ver: ${javafx.version}" />
		<property name="compiler.class.path" refid="javafxc.class.path" />

		<echo message="jfxc classpath: ${compiler.class.path}" />

		<!-- create output directories: -->
		<mkdir dir="target/fxclasses" />
		<mkdir dir="target/classes" />
		<mkdir dir="target/clover/classes" />

		<taskdef resource="javafxc-ant-task.properties" classpathref="javafxc.class.path" />

		<javafxc srcdir="src/main/fx" destdir="target/fxclasses" includes="**/*.fx" compilerclasspath="${compiler.class.path}" />

		<!-- copy output files to two places to deal with clovery issues: -->
		<fileset dir="target/fxclasses" id="fxclasses" />

		<copy todir="target/classes">
			<fileset refid="fxclasses" />
		</copy>
		<copy todir="target/clover/classes">
			<fileset refid="fxclasses" />
		</copy>
	</target>

	<target name="check-if-should-build">
		<available file="src/main/fx" property="should-build" />
		<echo message="should-build: ${should-build}" />
	</target>
</project>

To integrate into maven, we use the following snippet. It ties into the standard lifecycle, as you see, and happens just after src/main/java is compiled. It supplies three parameters to the ant script:

  • compile.classpath — the modules’s dependecy classpath
  • localRepo — the root of the local repository
  • javafx.vesion — the version of JavaFX to build with.

The last two parameters are used in the script to build the classpath to insure all the JavaFX libraries are present during compilation.

            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-antrun-plugin</artifactId>
               <version>1.1</version>
               <executions>
                   <execution>
                       <id>ant-process-classes</id>
                       <phase>compile</phase>
                       <configuration>
                           <tasks>
                            <property name="compile.classpath" refid="maven.compile.classpath"/>
                            <property name="localRepo" value="${settings.localRepository}"/>
                            <property name="javafx.version" value="${javafx.version}"/>

                            <ant antfile="javafxc.build.xml">
                                <target name="compile.jfx" />
                            </ant>
                           </tasks>
                       </configuration>
                       <goals>
                           <goal>run</goal>
                       </goals>
                   </execution>
               </executions>
               <dependencies>
                    <!-- needed to compile javafx -->
                    <dependency>
                        <groupId>javafx</groupId>
                        <artifactId>javafxc</artifactId>
                        <version>${javafx.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>javafx</groupId>
                        <artifactId>javafxrt</artifactId>
                        <version>${javafx.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>javafx</groupId>
                        <artifactId>scenario</artifactId>
                        <version>${javafx.version}</version>
                    </dependency>
               </dependencies>
           </plugin>

Working Around Bugs

Ant Mojo

Next, we tried to wrap the above into a Maven plugin via the nifty Ant script-driven Mojo concept. However, I could not specify dependencies as I could with a Java mojo, and I was unable to get maven to pass the compile classpath into the script

maven-antrun-plugin

It turns out, if you have a multimodule project (we do) and you have multiple invocations of maven-antrun-plugin, the classpath gets set once, by the first run. We had the choice of putting every dependency on every antrun, or the light hack of having an empty antrun in the parent project with every dependency specified.

Clover

When doing a build that includes Clover numbers, clover changes the build classpath to include the artifact with the clover classifier instead of the regular one. We were putting our result classes directly into target/classes

javafxc crash

A dependency on org.apache.felix:org.osgi.compendium will cause the javafxc ant task to crash, hard, with a JVM dump. I suspect it is the javax-servlet 1.0.0, but I’m too lazy to prove that.

+- org.apache.felix:org.osgi.compendium:jar:1.2.0:compile
|  +- org.apache.felix:javax.servlet:jar:1.0.0:compile
|  \- org.apache.felix:org.osgi.foundation:jar:1.2.0:compile

OSGi / Runtime

The above gets us to a compiling and building state. However, our target platform is OSGi, specifically the Felix implementation. We need to be able to supply the JavaFX runtime classes to other bundles.

The javafx-runtime bundle

Rather than packaging each SDK jar individually into a bundle, we just dropped all of them into a single mega bundle. The following POM accomplishes this:

 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <parent>
        <artifactId>PT2</artifactId>
        <groupId>org.bjc.es</groupId>
        <version>1.11.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>org.bjc.pt2.javafx-runtime</artifactId>
    <name>org.bjc.pt2.javafx-runtime</name>

    <!-- the <packaging> pulls bundle from the parent's properties -->
    <packaging>${packaging}</packaging>

    <properties>
        <packaging>bundle</packaging>
        <clover-percentage>-100</clover-percentage>
    </properties>

    <description>
        This bundle simply wraps the various javafx-compiled jars as a bundle.
    </description>
    <dependencies>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>decora-d3d</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>decora-hw</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>decora-ogl</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>javafx-swing</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>javafxc</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <!--
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>javafxdoc</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        -->
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>gluegen-rt</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>scenario</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>jogl</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>jmc</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>javafxgui</artifactId>
          <version>${javafx.version}</version>
        </dependency>
        <dependency>
          <groupId>javafx</groupId>
          <artifactId>javafxrt</artifactId>
          <version>${javafx.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>
                            ${pom.artifactId}
                        </Bundle-SymbolicName>
                        <!-- embed all compile and runtime scope dependencies -->
                        <!-- <Embed-Dependency>*;scope=compile;artifactId=!log4j|javafxc</Embed-Dependency> -->
                        <Import-Package>
                            com.sun.imageio.plugins.gif;resolution:=optional,
                            javax.imageio.*,
                            javax.swing.*,
                            javax.sound.*,
                            javax.xml.parsers,
                            javax.media.*;resolution:=optional,
                            javax.script.*;resolution:=optional,
                            netscape.javascript.*;resolution:=optional,
                            sun.*;resolution:=optional,
                            com.sun.javafx.api;resolution:=optional,
                            com.sun.javafx.runtime.adapter;resolution:=optional,
                            org.jdesktop.applet.util;resolution:=optional,
                            org.jdesktop.animation.timing.*;resolution:=optional,
                            org.xml.sax.*;resolution:=optional,
                        </Import-Package>

                        <Export-Package>
                            com.sun.gluegen.*,
                            !com.sun.javafx.runtime.adapter,
                            !com.sun.javafx.runtime.liveconnect,
                            !com.sun.javafx.runtime.provider,
                            !com.sun.javafx.api.*,
                            com.sun.javafx.*,
                            com.sun.embeddedswing,
                            com.sun.media.*,
                            com.sun.medialib.*,
                            com.sun.opengl.*,
                            com.sun.scenario.*,
                            com.sun.stylesheet.*,
                            javax.media.opengl.*,
                            org.jdesktop.layout,
                            !javafx.fxunit,
                            javafx.*,
                        </Export-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

You can alternately use Eclipse to export a bundle like this, see the post Running a JavaFX script from an OSGi bundle— generally a good article, but slightly skewed from our purpose.

The bundle created above is not quite perfect; some imports marked optional are probably not optional, but are for parts we aren’t using just yet, like sound, or applets. It is missing all the native code shipped with the SDK, which probably has a performance impact. We also haven’t tested a fully-featured screen yet. Finally, there is another problem, which we’ll see in the next section.

Integrating JavaFX into a Swing Application

I can’t create a Scene!

There is a pretty nice article called How to Use JavaFX in Your Swing Application on the main JavaFX blog. It gives details for how to convert a Scene subclass into a JComponent for use in a swing app. The only problem with that: With the javafx-runtime bundle above, I always get a NullPointerException during some StyleSheet intialization. Even when we do it from the command-line, the result is the same. Again, the javafx-runtime bundle is not quite perfect, and I think that this bears that out.

Getting the JComponent

So, we had a workaround: Extend CustomNode, then create a Scene in Java and inject the custom node in. The class below has a field called node, which is of type javafx.scene.Node, set in by Spring (see next section). We steal some of the code from the above link to help with the final extraction. Also note, we had to duplicate SingletonSequence.java in our package, since it is package-protected in the SDK.

        Scene scene = new Scene(true);

        Sequence<Node> seq = new SingletonSequence<Node>(TypeInfo.getTypeInfo(Node.class), node);
        scene.get$content().setAsSequenceFromLiteral(seq);
        scene.initialize$();

        // stolen from JFXPanel.java by Richard Bair and Jasper Potts:

        String helperName = "com.sun.javafx.scene.JSGPanelSceneImpl";
        FXClassType type = FXContext.getInstance().findClass(helperName);
        FXObjectValue panelSceneImpl = type.allocate();
        panelSceneImpl.initVar("scene", FXLocal.getContext().mirrorOf(scene));
        panelSceneImpl.initialize();

        FXValue jsgPanelV = type.getVariable("jsgPanel").getValue(panelSceneImpl);
        JComponent jsgPanel = (JComponent) ((FXLocal.ObjectValue) jsgPanelV).asObject()

Working with Spring

The last piece of the puzzle is the ability to allow Spring to create JavaFX instances and set properties on them. Here is an example JavaFX class:

package whatever;

import javafx.scene.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.text.*;

public class EDText extends CustomNode {
    var text:String = Dialog.MESSAGE;

    override public function create():Node {
        return Group {
            content: [
                Rectangle {
                    width: 300
                    height: 200
                    fill: Color.BLUE
                },
                Text {
                    x: 20
                    y: 20
                    content: bind text
                    fill: Color.WHITE
                }
            ]
        }; // endgroup
    }

    function setDisplayString(newValue:String) {
        text = newValue;
    }
}

It has a single operation, setDisplayString(). Then came this Spring config snippet to access it:

    <bean id="edNode" class="org.bjc.pt2.authentication.ui.EDText">
        <property name="displayString" value="set a value from spring!"/>
    </bean>

… which of course doesn’t work. JavaFX infers that the setter has a return value because none was specified. Spring is looking for public void setDisplayString(String) but finds public string setDisplayString(String) and complains. Minor tweak to the JavaFX code for the function declaration:

 function setDisplayString(newValue:String):Void {

And it’s all happy!

2 Comments

  1. The_Wizard_of_wind_Oz said,

    May 10, 2009 @ 9:23 am

    Very good step by step tutorial.

    For those interested, I have found out how to easily configure Maven to compile JavaFX classes alongside with Java classes: http://jfxpictureview.wiki.sourceforge.net/Maven+2

    The only drawback is that Java classes must be separated from the JavaFX classes (which means two separate projects).

  2. pforhan said,

    May 11, 2009 @ 7:54 am

    Thanks for the link. The site discusses the same problem (only one plexus compiler per module) that I mention in “Building alongside Java code”. We have an OSGi project with lots of small bundles/modules — the thought of doubling all those was scary, so we implemented the solution above.

RSS feed for comments on this post