Web Applications with OSGi – Working with Jersey and iPojo

So far, we have seen how to expose a classic web application in an OSGi container. We have also seen how to expose REST services with Jersey in OSGi. Let’s now take a look at how we can expose REST services that use iPojo services. The complete sources are on GitHub.

More exactly, what we want is that our REST resources access OSGi services. These services will be injected and managed by iPojo. This way, we do not have to write service trackers and so on. The approach described here would probably also apply to Declarative Services (or at least, that’s what I assume).

I did not find a way to make our REST resources iPojo components.
However, we can have an iPojo component that gets our iPojo service and that manually registers a REST servlet within the OSGi’s HTTP service. I found an example illustrating this approach in this Git repository. Many thanks to the author.

The Service

So, let’s start with the example.
I have 3 bundles. One contains the definition of the service I want to use in my REST resources (ipojo-service-api). It is a bundle that exports a single package with a Java interface.

public interface MyService {
	String getDataToDisplay();
}

The POM is quite simple.

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<modelVersion>4.0.0</modelVersion>
	<groupId>net.vzurczak</groupId>
	<artifactId>ipojo-service-api</artifactId>
	<name>OSGi Sample :: API</name>
	<packaging>bundle</packaging>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-bundle-plugin</artifactId>
				<configuration>
					<instructions>
						<Export-Package>net.vzurczak.sample.api</Export-Package>
					</instructions>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Then, I have one implementation of this service (ipojo-service-impl). It is a bundle that imports the service interface. It also contains the definition of an iPojo component.

public class MyServiceImpl implements MyService {

	@Override
	public String getDataToDisplay() {
		return "Hello World!";
	}
}

And here is the POM.

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<modelVersion>4.0.0</modelVersion>
	<groupId>net.vzurczak</groupId>
	<artifactId>ipojo-service-impl</artifactId>
	<name>OSGi Sample :: iPojo Implementation</name>
	<packaging>bundle</packaging>
	
	<dependencies>
		<dependency>
			<groupId>net.vzurczak</groupId>
			<artifactId>ipojo-service-api</artifactId>
			<version>${project.version}</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-bundle-plugin</artifactId>
				<configuration>
					<instructions>
						<Import-Package>net.vzurczak.sample.api</Import-Package>
					</instructions>
				</configuration>
			</plugin>
			
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-ipojo-plugin</artifactId>
				<executions>
					<execution>
						<goals>
							<goal>ipojo-bundle</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

And here is the iPojo definition for the implementation.

<?xml version="1.0" encoding="UTF-8"?>
<ipojo 
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="org.apache.felix.ipojo http://felix.apache.org/ipojo/schemas/CURRENT/core.xsd"
		xmlns="org.apache.felix.ipojo">

	<component classname="net.vzurczak.sample.impl.internal.MyServiceImpl" immediate="true" name="my-service-impl" public="false">
		<provides />
	</component>
	
	<instance component="my-service-impl" name="Default Instance for My Service" />
</ipojo>

So far, so good.
Let’s now work on the servlet.

The Servlet

The servlet and REST resources are handled in a third bundle.
It also contains an iPojo component in charge of registering and unregistering a servlet within the HTTP service. Here is its code.

public class RestComponent {

	private HttpService httpService;
	private MyService myService;


	public void starting() throws Exception {

		RestResource res = new RestResource( this.myService );
		RestApplication app = new RestApplication( res );
		ServletContainer jerseyServlet = new ServletContainer( app );

		this.httpService.registerServlet( "/jersey-ipojo", jerseyServlet, null, null );
	}

	public void stopping() {
		this.httpService.unregister( "/jersey-ipojo" );
	}
}

The fields httpService and myService will be injected by iPojo.
When the component instance is valid (i.e. when all its dependencies are resolved), the component registers a Jersey servlet for our REST application. If one of its dependencies disappear, the servlet is unregistered. In fact, I shuld check whether the HTTP service is here before unregistering the servlet. Anyway…

Here is the code of the REST application.
This is a standard REST application, as stated by JAX-RS.

public class RestApplication extends Application {

	private final RestResource myRestResource;

	public RestApplication( RestResource myRestResource ) {
		this.myRestResource = myRestResource;
	}

	@Override
	public Set<Object> getSingletons() {

		HashSet<Object> set = new HashSet<Object> ();
		set.add( this.myRestResource );
		return set;
	}
}

And here is the REST resource.

@Path( "/get-data" )
public class RestResource {

	private MyService myService;

	public RestResource() {
		// nothing
	}

	public RestResource( MyService myService ) {
		this.myService = myService;
	}

	@GET
	public Response getDataToDisplay() {

		String data = "No data.";
		if( this.myService != null )
			data = this.myService.getDataToDisplay();

		return Response.ok().entity( data ).build();
	}

	public MyService getMyService() {
		return this.myService;
	}

	public void setMyService( MyService myService ) {
		this.myService = myService;
	}
}

Once again, the OSGi service is not directly injected in the REST resources. You must use an intermediary component for this. Here is the iPojo definition for this component.

<?xml version="1.0" encoding="UTF-8"?>
<ipojo 
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="org.apache.felix.ipojo http://felix.apache.org/ipojo/schemas/CURRENT/core.xsd"
		xmlns="org.apache.felix.ipojo">

	<component classname="net.vzurczak.sample.servlet.internal.RestComponent" name="my-rest-component" immediate="true">
		<requires field="myService" />
		<requires field="httpService" />
		<callback transition="validate" method="starting" />
		<callback transition="invalidate" method="stopping" />
	</component>
	
	<instance component="my-rest-component" name="my-rest-component-instance" />
</ipojo>

Eventually, here is the POM.
There are quite a lot of imports. As in my previous article, Jersey packages are exported by the Jersey bundles.

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<modelVersion>4.0.0</modelVersion>
	<groupId>net.vzurczak</groupId>
	<artifactId>ipojo-servlet</artifactId>
	<name>OSGi Sample :: iPojo Servlet</name>
	<packaging>bundle</packaging>
	
	<properties>
		<jersey.stack.version>1.18.1</jersey.stack.version>
	</properties>
	
	<dependencies>
		<dependency>
			<groupId>net.vzurczak</groupId>
			<artifactId>ipojo-service-api</artifactId>
			<version>${project.version}</version>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-core</artifactId>
			<version>${jersey.stack.version}</version>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-servlet</artifactId>
			<version>${jersey.stack.version}</version>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
	
		<dependency>
			<groupId>org.apache.felix</groupId>
			<artifactId>org.osgi.core</artifactId>
			<version>1.4.0</version>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>org.apache.felix</groupId>
			<artifactId>org.osgi.compendium</artifactId>
			<version>1.4.0</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-bundle-plugin</artifactId>
				<configuration>
					<instructions>
					 	<Import-Package>
							net.vzurczak.sample.api,
							org.osgi.service.http,
							javax.ws.rs.*,
							com.sun.jersey.api.core,
							com.sun.jersey.spi.container.servlet,
							javax.servlet,
							javax.servlet.http,
							javax.naming
						</Import-Package>
					</instructions>
				</configuration>
			</plugin>
			
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-ipojo-plugin</artifactId>
				<executions>
					<execution>
						<goals>
							<goal>ipojo-bundle</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

Deployment

Once you have everything, execute mvn clean install on all 3 bundles.
As for my 2 previous articles, I deploy on Apache Karaf. Make sure the HTTP feature is installed. Then, deploy the 3 bundles under Karaf’s deploy directory.

The REST services will be available under http://localhost:8181/jersey-ipojo (e.g. http://localhost:8181/jersey-ipojo/get-data for my GET operation).

Notice

I did not succeed to make the ipojo-maven-plugin work with the _wab instruction of the maven-bundle-plugin. At least, not if you use the JAR extension. This is why I stuck with the bundle packaging for my REST services. The error message was…

Cannot find bytecode for class…

I will probably report it or at least, ask the question on the mailing-list of iPojo.

Web Applications with OSGi – Working with Jersey

In this previous article, I explained how to deploy standard web applications into an OSGi container. In this article, I will talk about how to deploy a web application that exposes REST services with Jersey. Once again, the complete sources are available on GitHub.

I have tried many things.
And actually, only one really worked. Let’s start with it. For the record, I used Jersey 1.x. This is because the application I plan to migrate to OSGi uses this version of Jersey. As in the previous article, I give explanations for Apache Karaf.

Explanations

First, you need to install Jersey bundles in your OSGi container.
If you use version 1.18, you MUST switch to version 1.18.1. Version 1.18 relies on an old version of ASM (version 3.x), while Karaf uses ASM 4.x. Both versions of ASM are incompatible. Using version 1.18 generally result in…

java.lang.IncompatibleClassChangeError: Implementing class

So, deploy jersey-core, jersey-server and jersey-servlet.

bundle:install –start mvn:com.sun.jersey/jersey-core/1.18.1
bundle:install –start mvn:com.sun.jersey/jersey-server/1.18.1
bundle:install –start mvn:com.sun.jersey/jersey-servlet/1.18.1

These bundles export packages your web application will import.
Here is what your POM should look like for your REST bundle.

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<modelVersion>4.0.0</modelVersion>
	<groupId>net.vzurczak</groupId>
	<artifactId>rest-with-jersey-as-a-bundle</artifactId>
	<name>OSGi Sample :: REST :: Jersey as a Bundle</name>
	<packaging>bundle</packaging>
	
	<properties>
		<jersey.stack.version>1.18.1</jersey.stack.version>
	</properties>
	
	<dependencies>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-core</artifactId>
			<version>${jersey.stack.version}</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-bundle-plugin</artifactId>
				<configuration>
					<instructions>
					 	<Import-Package>
							javax.ws.rs.*,
							com.sun.jersey.api.core,
							com.sun.jersey.spi.container.servlet
						</Import-Package>
						<Web-ContextPath>rest-bundle</Web-ContextPath>
						<Webapp-Context>rest-bundle</Webapp-Context>
						<_wab>src/main/webapp</_wab>
					</instructions>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

As you can see, this is a WAB (Web Archive Bundle).
Unlike what I did in my first examples, this is not a WAR. This is a JAR file, with OSGi metadata in its MANIFEST, and the structure of a WAR. Why I chose the JAR extension is because when somebody see a WAR, the first reflex is to deploy it in a web application server (such as Tomcat). But in this case, we use OSGi mechanisms and this will NOT work within Tomcat. The JAR prevents any ambiguity.

So, you bundle must import the JAX-RS annotations, as well as classes related to the servlet specification. Notice the _wab instruction for the maven-bundle-plugin. It indicates where the web resources are located. The plug-in will generate the right metadata (mainly the class path).

Here is my JAX-RS resource…

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path( "/get-data" )
public class MyRestResource {

	@GET
	public Response getDataToDisplay() {
		return Response.ok().entity( "Yes, it works." ).build();
	}
}

… and the web.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">

	<display-name>Welcome Page</display-name>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>

	<servlet>
		<servlet-name>MyServlet</servlet-name>
		<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
		
		<init-param>
			<param-name>com.sun.jersey.config.property.packages</param-name>
			<param-value>net.vzurczak.sample.rest.internal</param-value>
		</init-param>
		
		<load-on-startup>1</load-on-startup>
	</servlet>
	
	<servlet-mapping>
		<servlet-name>MyServlet</servlet-name>
		<url-pattern>/rest/*</url-pattern>
	</servlet-mapping>
</web-app>

The web context being rest-bundle (check the POM), the REST services will found under rest-bundle/rest/. For the deployment, make sure the WAR feature is installed and started. Then, build your project and deploy the resulting JAR in Karaf’s deploy directory.

You should find your REST service under http://localhost:8181/rest-bundle/rest/get-data.

Complementary Information

I guess things must work the same way with JAX-RS 2.x.
The main difference will probably be about the name of the packages to import. In fact, for Jersey 2.x, you can try the osgi-jax-rs-connector.

I also said the WAR features had to be installed and started before deploying the web application. This can be a problem if you restart the OSGi container. Indeed, if you web application starts at the same time or before the web server, your web bundle will not be registered by the HTTP service. I have not really investigated a solution about that. You may have to play with the bundle start level to solve it.

Eventually, I tried to embed a specific version of Jersey (let’s say 0.18) inside my bundle. This would fit situations where a specific bundle needs another version of Jersey. For this, I used 2 instructions of the maven-bundle-plugin.

<instructions>
	< Embed-Dependency>*;scope=compile|runtime;inline=true</Embed-Dependency>
	< Embed-Transitive>true</Embed-Transitive>
</instructions>

Roughly, it repackages the Maven dependencies inside the bundle.
Unfortunately, no matter what I tried, there was always an error during the deployment. Either there were no provider classes, or there was a missing package (javax.ejb), or no WebApplication provider was present. Resource scanning does not work, and setting the application in the web.xml does not solve it either. This is a problem if you want to expose REST services (server side). I do not think it is a problem for clients.

Anyway, for the server side, you can only rely on the first mechanism I described here. One good practice, if several versions of a same package are present in your OSGi container, is to hard-code the package version in your imports. You can specify a given version or version ranges in your imports…

<plugin>
	<groupId>org.apache.felix</groupId>
	<artifactId>maven-bundle-plugin</artifactId>
	<configuration>
		<instructions>
		 	<Import-Package>
				javax.ws.rs.*,
				com.sun.jersey.api.core;version="[1.0,2.0)",
				com.sun.jersey.spi.container.servlet;version="[1.18.1,1.19)"
			</Import-Package>
			<Web-ContextPath>rest-bundle</Web-ContextPath>
			<Webapp-Context>rest-bundle</Webapp-Context>
			<_wab>src/main/webapp</_wab>
		</instructions>
	</configuration>
</plugin>

See comments below about OSGi version ranges.
You can also take alook at this post on stackoverflow and in the OSGi Core Specification.

Web Applications with OSGi – Static Files

Context

I currently work on migrating an existing web application to OSGi (*).
The current solution relies on Jersey to expose REST services. It also exposes static resources for the web. And what I need to do is to drop Tomcat and deploy it inside an OSGi container. This is the last step to perform to complete our OSGi migration.

(*) Our motivation to use OSGi is classloaders isolation.

Beyond deploying a web application and Jersey services, we also have to plug it with iPojo. iPojo is the framework we chose to define components and services in our application. And to be complete in our description, we will deploy it on Apache Karaf 3.0.x, with Felix as the default container.

Before breaking my application, and given all the things that are involved, I wanted to create a short sample to make sure verything works together. Most of all, and because I did not find a ready-to-use solution on the web, I wanted to determine HOW everything could work together. To be honest, I did not succeeed on the first time. In fact, I have been forced to proceed step by step.

So, this post is the first of a series about web applications in OSGi. And it will start with very simple cases.

Serving Static Files in OSGi

I am NOT going to tell you to write a bundle activator and register a servlet with the HTTP service. Now, OSGi containers support the deployment of web bundles (WAB). This is a ZIP archive that contain both web resources (like in a WAR) and an OSGi bundle. Said differently, it is a WAR file with OSGi metadata in a MANIFEST. And it does not have to use a specific file extension. So, it could also be a JAR file with the structure of a WAR. Using one extension or another depends on your context.

So, here is an example of a WAB that will server static resources. No servlet, no JSP… Since it is very simple, and not dependent on OSGi, I will create a WAR file and add OSGi metadata. This way, I will be able to deploy it in an OSGi container with web support, or in any application server such as Tomcat.

Let’s start with the POM.

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<modelVersion>4.0.0</modelVersion>
	<groupId>net.vzurczak</groupId>
	<artifactId>serving-static-files</artifactId>
	<name>OSGi Sample :: Web App :: Static Files</name>
	<packaging>war</packaging>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<configuration>
					<archive>
						<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
					</archive>
				</configuration>
			</plugin>
			
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-bundle-plugin</artifactId>
				<executions>
					<execution>
						<id>bundle-manifest</id>
						<phase>process-sources</phase>
						<goals>
							<goal>manifest</goal>
						</goals>
						<configuration>
							<instructions>
								<Web-ContextPath>static-files</Web-ContextPath>
								<Webapp-Context>static-files</Webapp-Context>
							</instructions>
						</configuration>
					</execution>
				</executions>
				<configuration>
					<supportedProjectTypes>
						<supportedProjectType>jar</supportedProjectType>
						<supportedProjectType>bundle</supportedProjectType>
						<supportedProjectType>war</supportedProjectType>
					</supportedProjectTypes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

The project structure must be…

pom.xml
src/main/webapp/
* Your static resources (HTML, CSS, JPG…)
* WEB-INF/web.xml

And here is the content of the web.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<web-app 
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
		xmlns="http://java.sun.com/xml/ns/javaee" 
		xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
		xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
		id="WebApp_ID" version="3.0">

	<display-name>Welcome Page</display-name>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
	
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.jpg</url-pattern>
	</servlet-mapping>
	
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.html</url-pattern>
	</servlet-mapping>
</web-app>

The sources are available on GitHub.

Run mvn clean install.
Pick up the WAR file under the target directory and you can directly deploy it in a Tomcat server, or in an OSGi container with web support. How to enable web support in an OSGi container? Well, you can rely on Pax Web. Pax Web is a project that embeds application servers such as Jetty and Tomcat in bundles. Since I deploy on Apache Karaf, I used the WAR feature, which relies on Pax Web.

So, if you are using Apache Karaf, start it and type in feature:install war (or if you are using Karaf 2.x, features:install war). This will enable web support in your installation. And to deploy your web bundle in Karaf, just drop it in Karaf’s deploy directory.

If you want to make sure it is correctly deployed, you can first check in your browser that your resources are displayed at http://localhost:8181/static-files (assuming you kept the default settings for Jetty). And you can also install Karaf’s web console (feature:install web-console) and go to http://localhost:8181/system/console/http (default credentials: karaf / karaf). You will see the HTTP and web contexts there.

Serving a JSP in OSGi

Now, let’s assume you want to use JSP files and have a custom servlet in your web application. What was defined before is still valid. The only modification to bring is in the POM file.

Simply add the following dependency.

<dependencies>
	<dependency>
		<groupId>javax.servlet</groupId>
		<artifactId>servlet-api</artifactId>
		<version>2.5</version>
		<scope>provided</scope>
	</dependency>
</dependencies>

Make sure the scope is provided.
The Maven-bundle-plugin will automatically add the package imports in the MANIFEST. And these imports will be provided at runtime by other bundles.

This post is quite detailed.
But I found a lof of information on this blog entry. You may be interested by its reading.

Deploying to Maven Repositories from Tavis CI

Those that follow my work must probably know I work on an open source project called Roboconf.
This project is mainly developed in Java and is hosted by GitHub.

We recently got Maven repositories hosted by Sonatype.
This is really useful, since until now, Roboconf users had to compile the project themselves before using it. And we have not yet released anything. With all the changes we are doing, it is really hard to speak of stable versions. At least, snpashot versions may help to gain some time. Besides, we have crossed-repositories dependencies. As an example, our Maven plug-in needs a Maven module from the platform. So, we need a snapshot repository to share and reuse some built artifacts.

So, in addition to use these repositories to perform clean releases, we also wanted to use the provided snapshot repository for continuous integration. Our CI system is Travis. And what we wanted to do is to deploy our artifacts to the snapshot repository when a build succeeds.

There are quite a lot of articles on the web about this.
This one helped me a lot, as well as this one. Obviously, you must have an account and a Maven repository on Sonatype.

One thing that was difficult (and not really documented anywhere), is how to deal with the build matrix. In our travis.yml file, we ask Travis to build our project against 3 different JDK (Open JDK 6, Open JDK 7 and Oracle JDK 7). But given the way Travis works, we do not want it to upload our artifacts 3 times per build. And since there is no after_build step in the Travis life cycle, I had to find a trick.

So, here is my travis.yml file.

language: java

# Test several JDK
jdk:
  - oraclejdk7
  - openjdk6
  - openjdk7

# Remove all the bla-bla...
script:
  - mvn clean install -q

# Actions to perform after the build.
after_success:
  - ./after_success.sh

What is interesting is the content of the after_success script.
Here is its content.

#!/bin/bash

# Get the Java version.
# Java 1.5 will give 15.
# Java 1.6 will give 16.
# Java 1.7 will give 17.
VER=`java -version 2>&1 | sed 's/java version "\(.*\)\.\(.*\)\..*"/\1\2/; 1q'`

# We build for several JDKs on Travis.
# Some actions, like analyzing the code (Coveralls) and uploading
# artifacts on a Maven repository, should only be made for one version.

# If the version is 1.6, then perform the following actions.
# 1. Copy Maven settings on the VM.
# 2. Notify Coveralls.
# 3. Upload artifacts to Sonatype.
# 4. Use -q option to only display Maven errors and warnings.
# 5. Use --settings to force the usage of our "settings.xml" file.
# 6. Enable the profile that generates the javadoc and the sources archives.

if [ $VER == "16" ]; then
	wget http://roboconf.net/resources/build/settings.xml
	mvn clean deploy cobertura:cobertura org.eluder.coveralls:coveralls-maven-plugin:cobertura -q --settings settings.xml -P jdoc-and-sources
else
	echo "No action to undertake (not a JDK 6)."
fi

Since we do not want to perform heavy tasks 3 times per build, I managed to filter these builds in my script. This way, complex tasks are performed only once. In this case, evaluating code coverage and Maven deployment is only done when we build on a JDK 6. For this, I get the Java version in my bash script and makes the right checks. Heavy tasks are performed if and only if the right conditions are met.

The key thing to be able to deploy on Maven is the use of a settings.xml file that contains credentials of the release server. Since I have other related repositories that need the same Maven settings, I hosted this settings.xml file on a web server. If you do not need to reuse it, you could simply put it in our source repository. Anyway, in my case and before deploying the artifacts, Travis downloads this file and uses it for the deployment.

Here is the content of this file.

<?xml version="1.0" encoding="UTF-8"?>
<settings 
		xmlns="http://maven.apache.org/SETTINGS/1.0.0"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
		
	<servers>
		<server>
			<id>sonatype-snapshots</id>
			<username>${env.USER_ID}</username>
			<password>${env.USER_PWD}</password>
		</server>
	</servers>
	
</settings>

This file gives the credentials (user and password) to use to connect to my Sonatype account. Since I do not want to put their values in clear, I defined them as environment variables on Travis. I simply visited the page of my project on Travis and went into the settings menu. There is an option to define environment variables per repository. env.USER_ID refers to the environment variable called USER_ID. I defined its value in Travis. Same thing for the password.

Now, how does Maven know to which repository deploy?
Well, that’s easy. In my project, the parent POM contains a distributionManagement section. Here is what it looks like.

<distributionManagement>
	<snapshotRepository>
		<id>sonatype-snapshots</id>
		<name>Sonatype Nexus snapshot repository</name>
		<url>https://oss.sonatype.org/content/repositories/snapshots</url>
	</snapshotRepository>

	<repository>
		<id>sonatype-release-staging</id>
		<name>Sonatype Nexus release repository</name>
		<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
	</repository>
</distributionManagement>

There is one repository for releases.
And there is another one for snapshots.

Notice that the ID of my snapshot repository matches in both the parent POM and my settings.xml. The fact that Travis must upload to the snapshot repository is defined by the project version. If it contains SNAPSHOT, Maven will automatically select it. And in continuous integration, we decided we were only building snpashot versions. Releases are performed manually, independently of Travis.

That’s it. 😉
As you can see, there is no clean solution to handle the build matrix in Travis with Maven deployment. But the solution I described in this post works and is quite simple to manage and maintain. So, maybe it will help others.