Referencing Maven projects from Eclipse plugins
Maven makes dependency management between java projects easy, and with maven tycho, building Eclipse RCP plugins and products from the command-line is just as simple. But Eclipse RCP projects and maven projects have very different dependency resolutions, and mixing both project types is not as simple as adding a pom.xml to an Eclipse plugin project. In this blog post, I'll show what I've done to reference maven projects from our Eclipse RCP application.
Our editorial Sophora CMS client (called DeskClient) is an Eclipse RCP application comprised of the standard Eclipse plugins and several custom plugins. Most third-party libraries are included in the main DeskClient plugin as jars in a "lib"-folder in the plugin project. Our own libraries are deployed in the same way, e.g. the Sophora api- and the client libraries for accessing a Sophora server. The api and client libs are plain java maven projects. A distinct disadvantage of this approach is that after each change in e.g. the client project, the client-jar needs to be built and put into the "lib"-folder of the DeskClient plugin. Also, hot-replacement of client classes during debugging does not work.
To improve this situation, I explored the possibility of referencing the Sophora client project directly from the DeskClient plugin project. I'll first show how this works within Eclipse, with plain Eclipse + m2e, then with additional comfort using m2e-tycho. In a second step, I'll show how to build the DeskClient plugin without Eclipse, using command-line maven.
Preparations
The first thing to understand is that for both approaches, the client project needs to be a valid Eclipse plugin, i.e. have a MANIFEST.MF declaring the dependencies. It does not necessarily need to have the plugin project nature, just have that manifest. Referencing a java project without a manifest as a plugin dependency from an Eclipse plugin will not work, even using maven tycho.
Let's have a look at the folder structure of the DeskClient plugin:
com.subshell.sophora.eclipse - src - lib -- apache commons jars -- slf4j jar -- sophora client jar -- spring jar ...
Currently, the DeskClient plugin includes the client and all necessary third-party jars using the Bundle-Classpath directive. This way, the plugin and all libs are loaded using the same classloader and can see each other. This includes all transitive dependencies needed by the client. If I move the client lib into its own plugin, it will also need to see the third-party libs. The OSGi-way is to turn each lib into a plugin and make it available either in the workspace or in the target-platform. This is not my focus at this point, so I'll just move them all to a single plugin called "thirdparty", which exports all included packages. This way, both the DeskClient plugin and the sophora client plugin, which I'll create in the next step, can both depend on the third-party plugin.
Beware that each plugin has its own classpath. In this case, there is a problem with the spring bean context, which is used by the client. What happens is, the spring classloader cannot see classes from the sophora client anymore, because they're now in a different plugin. I can't (easily) add a dependency from the third-party plugin to the client, because then there'd be a dependency cycle. The solution is to set the context classloader in the client before initializing the bean context.
Building using Eclipse + m2e
In this first approach, I'll have maven generate a valid MANIFEST.MF for the client project to turn it into an Eclipse plugin. This is done using the maven-bundle-plugin with the following excerpt from the client's pom.xml:
<build> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> <configuration> <manifestLocation>META-INF</manifestLocation> <instructions> <!- This adds import statements based on the packages used in this project. -> <Import-Package>*;version="0.0.0"</Import-Package> <!- This is needed to get all the spring stuff which is not statically referenced into the classpath. -> <Require-Bundle>com.subshell.sophora.eclipse.thirdparty</Require-Bundle> <!- The DeskClient uses ImageUtils from impl-package, which is not exported by default. -> <Export-Package>!.,com.subshell.sophora.client.*</Export-Package> </instructions> </configuration> <executions> <execution> <id>bundle-manifest</id> <phase>process-classes</phase> <goals> <goal>manifest</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestFile>META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> </plugins> <pluginManagement> <plugins> <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself. --> <plugin> <groupId>org.eclipse.m2e</groupId> <artifactId>lifecycle-mapping</artifactId> <version>1.0.0</version> <configuration> <lifecycleMappingMetadata> <pluginExecutions> <pluginExecution> <pluginExecutionFilter> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <versionRange>[1.0.0,)</versionRange> <goals> <goal>manifest</goal> </goals> </pluginExecutionFilter> <action> <ignore></ignore> </action> </pluginExecution> </pluginExecutions> </lifecycleMappingMetadata> </configuration> </plugin> </plugins> </pluginManagement> </build>
Running "mvn package" in the client project now generates a MANIFEST.MF with import statements generated from the packages used in the sources. Unfortunately, this does not detect classes loaded using the spring configuration. I could manually add the necessary import statements in the configuration of the maven-bundle-plugin, but I'll opt to just depend on the entire third-party plugin instead. In turn, the client plugin exports all of its own packages, even internal packages containing "impl" (we should fix that reference someday...). After refreshing the client project, Eclipse automatically recognizes it as a plugin, even though it doesn't have the plugin nature. The client plugin or its packages can now be added as a dependencies in the MANIFEST.MF of the DeskClient plugin.
It is a bit cumbersome to run the "mvn package" command manually each time the set of imported and exported packages of the client needs to change. Using the m2e Tycho Configurator, it is possible to have Eclipse automatically rebuild the client's MANIFEST.MF on each full rebuild. First, I remove the pluginManagement-block containing the lifecycle-mapping from the client's pom. Eclipse now shows the error that the execution of the maven-bundle-plugin is not covered by any lifecycle configuration. I select the error and open the quick-fix wizard using ctrl-1. In the wizard, I select the option to "Discover new m2e connectors". After choosing this option, the "Tycho Configurator" is offered for installation. If the wizard doesn't work for some reason, the Tycho Configurator can also be installed using Preferences > Maven > Discovery > Open Catalog. Once it is installed, the client's MANIFEST.MF is generated on each full rebuild of the project (Project > Clean...).
Building from the command-line using maven tycho
To build the DeskClient plugin on the command-line, I use maven tycho. This is an extension to maven for building Eclipse plugins, features, products and related artifacts. Tycho is a world of its own, so I'll just post my pom files and highlight a few noteworthy things.
This build has the added difficulty of using both manifest-first and pom-first dependency resolution. The client project is a pom-first project (packaging "jar"), whereas the DeskClient and third-party plugins are manifest-first projects (packaging "eclipse-plugin"). Unfortunately, on cannot mix pom-first and manifest-first projects in a single reactor-build, see Dependency on pom-first artifacts. So I first run "mvn clean install" on the client project to have the pom-first project out of the way. The client is now deployed to my local maven repository and can be accessed by the DeskClient build in the next step.
The most simple way to build the two DeskClient projects is in a maven reactor using a parent project, which I created next to the other projects. So my build directory contains the following projects (the DeskClient project is called com.subshell.sophora.eclipse for historical reasons):
com.subshell.sophora.eclipse com.subshell.sophora.eclipse.parent com.subshell.sophora.eclipse.thirdparty
The parent project contains only a pom.xml, with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.subshell.sophora.eclipse</groupId>
<artifactId>com.subshell.sophora.eclipse.parent</artifactId>
<packaging>pom</packaging>
<name>DeskClient Parent POM</name>
<!-- This must match the version in the MANIFEST.MF. -->
<version>1.33.0-SNAPSHOT</version>
<properties>
<deskclient.targetplatform.p2.url> file:/c:/Users/theess/dev/target-platform/eclipse-SDK-3.5.2_delta_NLS_swttools_swtbot_gef_jubula_src_p2 </deskclient.targetplatform.p2.url>
<tycho-version>0.12.0</tycho-version>
<project.build.sourceEncoding>iso-8859-1</project.build.sourceEncoding>
<project.reporting.outputEncoding>iso-8859-1</project.reporting.outputEncoding>
</properties>
<modules>
<module>../com.subshell.sophora.eclipse.thirdparty</module>
<module>../com.subshell.sophora.eclipse</module>
</modules>
<repositories>
<repository>
<id>TargetPlatform</id>
<layout>p2</layout>
<url>${deskclient.targetplatform.p2.url}</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>maven-osgi-compiler-plugin</artifactId>
<version>${tycho-version}</version>
<configuration>
<encoding>ISO-8859-1</encoding>
<showDeprecation>true</showDeprecation>
<showWarnings>true</showWarnings>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>tycho-maven-plugin</artifactId>
<version>${tycho-version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>target-platform-configuration</artifactId>
<version>${tycho-version}</version>
<configuration>
<resolver>p2</resolver>
<ignoreTychoRepositories>true</ignoreTychoRepositories>
<pomDependencies>consider</pomDependencies>
<environments>
<environment>
<os>win32</os>
<ws>win32</ws>
<arch>x86</arch>
</environment>
<environment>
<os>win32</os>
<ws>win32</ws>
<arch>x86_64</arch>
</environment>
<environment>
<os>linux</os>
<ws>gtk</ws>
<arch>x86_64</arch>
</environment>
<environment>
<os>linux</os>
<ws>gtk</ws>
<arch>x86</arch>
</environment>
<environment>
<os>macosx</os>
<ws>cocoa</ws>
<arch>x86_64</arch>
</environment>
</environments>
</configuration>
</plugin>
</plugins>
</build>
</project>Highlights:
- The property "deskclient.targetplatform.p2.url" is used to reference the target platform, which must be a p2 repository.
- The version in the pom must match the version in the MANIFEST.MF of both DeskClient plugins.
- The configuration for the target-platform-configuration plugin contains the option "<pomDependencies>consider</pomDependencies>". This tells maven to look up dependencies using the<dependencies>-element in the pom in addition to the dependencies in the MANIFEST.MF. Those dependencies still need to contain a valid MANIFEST.MF, but they can be located in the maven repository instead of the target-platform, and need not have the packaging "eclipse-plugin". Tycho gathers all pom-dependencies from the maven repository and makes them available als OSGi plugins to the build. The dependency resolution of the main build is then done using the manifests of all participating plugins.
With most stuff set up in the parent, the pom of the DeskClient plugin is very simple:
<?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <artifactId>com.subshell.sophora.eclipse</artifactId> <packaging>eclipse-plugin</packaging> <parent> <version>1.33.0-SNAPSHOT</version> <groupId>com.subshell.sophora.eclipse</groupId> <artifactId>com.subshell.sophora.eclipse.parent</artifactId> <relativePath>../com.subshell.sophora.eclipse.parent</relativePath> </parent> <dependencies> <dependency> <groupId>com.subshell.sophora</groupId> <artifactId>com.subshell.sophora.client</artifactId> <version>1.33.0-SNAPSHOT</version> </dependency> </dependencies> </project> <build> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> <configuration> <manifestLocation>META-INF</manifestLocation> <instructions> <!-- This adds import statements based on the packages used in this project. --> <Import-Package>*;version="0.0.0"</Import-Package> <!-- This is needed to get all the spring stuff which is not statically referenced into the classpath. --> <Require-Bundle>com.subshell.sophora.eclipse.thirdparty</Require-Bundle> <!-- The DeskClient uses ImageUtils from impl-package, which is not exported by default. --> <Export-Package>!.,com.subshell.sophora.client.*</Export-Package> </instructions> </configuration> <executions> <execution> <id>bundle-manifest</id> <phase>process-classes</phase> <goals> <goal>manifest</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestFile>META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> </plugins> <pluginManagement> <plugins> <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself. --> <plugin> <groupId>org.eclipse.m2e</groupId> <artifactId>lifecycle-mapping</artifactId> <version>1.0.0</version> <configuration> <lifecycleMappingMetadata> <pluginExecutions> <pluginExecution> <pluginExecutionFilter> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <versionRange>[1.0.0,)</versionRange> <goals> <goal>manifest</goal> </goals> </pluginExecutionFilter> <action> <ignore></ignore> </action> </pluginExecution> </pluginExecutions> </lifecycleMappingMetadata> </configuration> </plugin> </plugins> </pluginManagement> </build>
This just declares the dependecy to the client, and that's it. The pom of the third-party plugin is identical except for the artifact id.
With the reactor set up, a simple "mvn package" in the parent project now builds the DeskClient and the third-party plugins.
To wrap up, the manifest-first DeskClient plugin now references the pom-first client library directly. This works within the Eclipse IDE as well as on the command-line. Unfortunately, building client and DeskClient projects on the command-line is still a two-step process, but this can easily be scripted. Now I just need to switch our build-process from PDE-build to maven tycho...
Categories
Sophora CMS - A New Take on Content Management
Sophora is optimized to meet the needs of modern companies that produce up-to-date multimedia content on a large scale and that require fast delivery of their content.
Toromiro
Toromiro is a professional tool for the administration and editing of Java Content Repositories (JCR). JCR is powering some of today’s most successful solutions for content management and digital asset management.