Build REST services with OSGi JAX-RS whiteboard

22 minute read

Some years ago I had a requirement to access the OSGi services inside my Eclipse application via web interface. Back then I used the OSGi HTTP Whiteboard Specification and wrapped a servlet around my service. Of course I wrote a blog post about this and named it Access OSGi Services via web interface.

That blog post was published before OSGi R7 was released. And at that time there was no simple alternative available. With R7 the JAX-RS Whiteboard Specification was added, which provides a way to achieve the same goal by using JAX-RS, which is way simpler than implementing Servlets. I gave a talk at the EclipseCon Europe 2018 with the title How to connect your OSGi application. In this talk I showed how you create a connection to your OSGi application using different specifications, namely

  • HTTP Service / HTTP Whiteboard
  • Remote Services (using ECF JAX-RS Distribution Provider)
  • JAX-RS Whiteboard

Unfortunately the recording of that talk failed, so I can only link to the slides and my GitHub repository that contains the code I used to show the different approaches in action.

In the Panorama project, in which I am currently involved, one of our goals is to provide cloud services for model processing and evaluation. As a first step we want to publish APP4MC services as cloud services (more information in the Eclipse Newsletter December 2020). There are services contained in APP4MC bundles that are free from dependencies to the Eclipse Runtime and do not require any Extension Points, and there are services in bundles that have dependencies to plug-ins that use Extension Points. But all the services we want to publish as cloud services are OSGi declarative services. While there are numerous ways and frameworks to create REST based web services (e.g. Spring Boot or Microprofile to just name two of them), I was searching for a way to do this in OSGi. Especially because I want to reduce the configuration and implementation efforts with regards to the runtime infrastructure for consuming the existing OSGi services of the project.

For the services that have dependencies to Extensions Points and require a running Eclipse Runtime, I was forced to use the HTTP Service / HTTP Whiteboard approach. The main reason for this is that because of this dependency I needed to stick with a PDE project layout. Unfortunately there is no JAX-RS Whiteboard implementation available in Eclipse and therefore not available via a p2 Update Site. Maybe it would be possible somehow, but actually the solution should be to get rid of Extension Points and the requirement for a running Eclipse runtime.

But this blog post is about JAX-RS Whiteboard and not about project layouts and Extension Points vs. Declarative Services. So I will focus on the services that have a clean dependency structure. The setup should be as comfortable as possible to be able to focus on the REST service implementation, and not struggle with the infrastructure too much.

Create the project structure

To create the project structure we can follow the steps described in the enRoute Tutorial.

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=project \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = jaxrs
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier.jaxrs
  • After setting the value for package you will get the information that for the two projects that will be created, the following defaults will be used:
    • app-artifactId: app
    • app-target-java-version: 8
    • impl-artifactId: impl

Note:
IMHO app and impl are not good values for project names. Although they are sub projects inside a Maven project, imported to the IDE this leads to confusions if you have multiple such projects in one workspace. By entering ‘n’ the defaults are declined and you need to insert the values for all parameters again. Additionally you can specify the artifactId of the app and the impl project, and the target Java version you want to develop with.

If you forget to specify different values for app and impl at creation time and want to change it afterwards, you will have several things to consider. Even with the refactoring capabilities of the IDE, you need to ensure that you do not forget something, like the fact that the name of the .bndrun file needs to be reflected in the pom.xml file.

  • After accepting the inserted values with ‘y’ the following project skeletons are created:
    • project parent folder named by the entered artifactId jaxrs
    • the app project
    • the impl project

Now the projects can be imported to the IDE of your choice. As the projects are plain Maven based Java projects, you can use any IDE. But of course my choice is Eclipse with bndtools.

  • Import the created projects via File - Import… - Maven - Existing Maven Projects
  • Select the created jaxrs directory

Once the import is done you should double check the dependencies of the created skeletons. Some of the dependencies and transitive dependencies in the generated pom.xml files are not up-to-date. For example Felix Jetty is included in version 4.0.6 (September 2018), while the most current version is 4.1.4 (November 2020). You can check this for example by opening the Repositories view in the Bndtools perspective and expanding the Maven Dependencies section. The libraries listed inside Maven Dependencies are added from the Maven configuration of the created project. To update the version of one of those libraries, you need to add the corresponding configuration to the dependencyManagement section of the jaxrs/pom.xml, e.g.

<dependency>
  <groupId>org.apache.felix</groupId>
  <artifactId>org.apache.felix.http.jetty</artifactId>
  <version>4.1.4</version>
</dependency>

You should also update the version of the bnd Maven plugins. The generated pom.xml files use version 4.1.0, which is pretty outdated. At the time writing this blog post the most recent version is 5.2.0.

  • Open jaxrs/pom.xml
  • Locate bnd.version in the properties section
  • Update 4.1.0 to 5.2.0
  • Right click on the jaxrs project - Maven - Update Project…
    • Have all projects checked
    • OK

Implementing the OSGi service

As the goal is to wrap an existing OSGi Declarative Service to make it accessible as web service, we use the M.U.S.E (Most Useless Service Ever) introduced in my Getting Started with OSGi Declarative Services blog post. Unfortunately the combination of Bndtools workspace projects with Bndtools Maven projects does not work well. Mainly because the Bndtools workspace projects are not automatically available as Maven modules. So we create the API and the service implementation projects also by using the OSGi enRoute archetypes.

Note:
If you have an OSGi service bundle already available via Maven, you can also use that one by adding the dependency to the pom.xml files and skip this section.

  • Go to the newly created jaxrs directory and create an API module using the api archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=api \
    -DarchetypeVersion=7.0.0
  • groupId = org.fipro.modifier
  • artifactId = api
  • version = 1.0-SNAPSHOT
  • package = org.fipro.modifier.api

  • Then create the service implementation module using the ds-component archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=ds-component \
    -DarchetypeVersion=7.0.0
  • groupId = org.fipro.modifier
  • artifactId = inverter
  • version = 1.0-SNAPSHOT
  • package = org.fipro.modifier.inverter

  • Import the created projects via File - Import… - Maven - Existing Maven Projects
  • Select the jaxrs directory

Service interface

  • In the Bndtools Explorer locate the api module and expand to the package org.fipro.modifier.api
  • Implement the StringModifier interface:
public interface StringModifier {
    String modify(String input);
}
  • You can delete the ConsumerInterface and the ProviderInterface which were created by the archetype.
  • Ensure that you do NOT delete the package-info.java file in the org.fipro.modifier.api package. It configures that the package is exported. If this file is missing, the package is a Private-Package and therefore not usable by other OSGi bundles.

    The package-info.java file and its content are part of the Bundle Annotations introduced with R7. Here are some links if you are interested in more detailed information:

Service implementation

  • In the Bndtools Explorer locate the inverter module.
  • Open the pom.xml file and add the dependency to the api module in the dependencies section.
<dependency>
  <groupId>org.fipro.modifier</groupId>
  <artifactId>api</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
  • Expand to the package org.fipro.modifier.inverter
  • Implement the StringInverter service:
@Component
public class StringInverter implements StringModifier {

    @Override
    public String modify(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}
  • You can delete the ComponentImpl class that was created by the archetype.
  • Note that the package does not contain a package-info.java file, as the service implementation is typically NOT exposed.

Implementing the REST service

After the projects are imported to the IDE and the OSGi service to consume is available, we can start implementing the REST based service.

  • In the Bndtools Explorer locate the impl module.
  • Open the pom.xml file and add the dependency to the api module in the dependencies section.
<dependency>
  <groupId>org.fipro.modifier</groupId>
  <artifactId>api</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
  • Expand to the package org.fipro.modifier.jaxrs
  • Implement the InverterRestService:
    • Add the @Component annotation to the class definition and specify the service parameter to specify it as a service, not an immediate component.
    • Add the @JaxrsResource annotation to the class definition to mark it as a JAX-RS whiteboard resource. This will add the service property osgi.jaxrs.resource=true which means this service must be processed by the JAX-RS whiteboard.
    • Get a StringModifier injected using the @Reference annotation.
    • Implement a JAX-RS resource method that uses the StringModifier.
@Component(service=InverterRestService.class)
@JaxrsResource
public class InverterRestService {

    @Reference
    StringModifier modifier;

    @GET
    @Path("modify/{input}")
    public String modify(@PathParam("input") String input) {
        return modifier.modify(input);
    }
}

Interlude: PROTOTYPE Scope

When you read the specification, you will see that the example service is using the PROTOTYPE scope. The example services in the OSGi enRoute tutorials do not use the PROTOTYPE scope. So I was wondering when to use the PROTOTYPE scope for JAX-RS Whiteboard services. I was checking the specification and asked on the OSGi mailing list. Thanks to Raymond Augé who helped me understanding it better. In short, if your component implementation is stateless and you get all necessary information injected to the JAX-RS resource methods, you can avoid the PROTOTYPE scope. If you have a stateful implementation, that for example gets JAX-RS context objects for a request or session injected into a field, you have to use the PROTOTYPE scope to ensure that every information is only used by that single request. The example service in the specification therefore does not need to specify the PROTOTYPE scope, as it is a very simple example. But it is also not wrong to use the PROTOTYPE scope even for simpler services. This aligns the OSGi service design (where typically every component instance is a singleton) with the JAX-RS design, as JAX-RS natively expects to re-create resources on every request.

Prepare the application project

In the application project we need to ensure that our service is available. In case the StringInverter from above was implemented, the inverter module needs to be added to the dependencies section of the app/pom.xml file. If you want to use another service that can be consumed via Maven, you of course need to add that dependency.

  • In the Bndtools Explorer locate the app module.
  • Open the pom.xml file and add the dependency to the inverter module in the dependencies section.
<dependency>
  <groupId>org.fipro.modifier</groupId>
  <artifactId>inverter</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
  • Open app.bndrun
  • Add org.fipro.modifier.inverter to the Run Requirements
  • Click on Resolve and double check that the modules api, impl and inverter are part of the Run Bundles
  • Click on Run OSGi
  • Open a browser and navigate to http://localhost:8080/modify/fubar to see the new REST based service in action.

JSON support

As returning a plain String is quite uncommon for a web service, we now extend our setup to return the result as JSON. We will use Jackson for this, so we need to add it to the dependencies of the impl module. The simplest way is to use org.apache.aries.jax.rs.jackson.

  • In the Bndtools Explorer locate the impl module.
  • Open the pom.xml file and add the dependency to org.apache.aries.jax.rs.jackson in the dependencies section.
<dependency>
    <groupId>org.apache.aries.jax.rs</groupId>
    <artifactId>org.apache.aries.jax.rs.jackson</artifactId>
    <version>1.0.2</version>
</dependency>

Alternative: Custom Converter

Alternatively you can implement your own converter and register it as a JAX-RS Whiteboard Extension.

  • In the Bndtools Explorer locate the impl module.
  • Open the pom.xml file and add the dependency to the Jackson in the dependencies section.
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.0</version>
</dependency>
  • Implement the JacksonJsonConverter:
    • Add the @Component annotation to the class definition and specify the PROTOTYPE scope parameter to ensure that multiple instances can be requested.
    • Add the @JaxrsExtension annotation to the class definition to mark the service as a JAX-RS extension type that should be processed by the JAX-RS whiteboard.
    • Add the @JaxrsMediaType(APPLICATION_JSON) annotation to the class definition to mark the component as providing a serializer capable of supporting the named media type, in this case the standard media type for JSON.
    • Internally make use of the OSGi Converter Specification for the implementation.
@Component(scope = PROTOTYPE)
@JaxrsExtension
@JaxrsMediaType(APPLICATION_JSON)
public class JacksonJsonConverter<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {

    @Reference(service=LoggerFactory.class)
    private Logger logger;

    private final Converter converter = Converters.newConverterBuilder()
            .rule(String.class, this::toJson)
            .rule(this::toObject)
            .build();

    private ObjectMapper mapper = new ObjectMapper();

    private String toJson(Object value, Type targetType) {
        try {
            return mapper.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            logger.error("error on JSON creation", e);
            return e.getLocalizedMessage();
        }
    }

    private Object toObject(Object o, Type t) {
        try {
	    if (List.class.getName().equals(t.getTypeName())) {
                return this.mapper.readValue((String) o, List.class);
            }
            return this.mapper.readValue((String) o, String.class);
        } catch (IOException e) {
            logger.error("error on JSON parsing", e);
        }
        return CANNOT_HANDLE;
    }

    @Override
    public boolean isWriteable(
        Class<?> c, Type t, Annotation[] a, MediaType mediaType) {

        return APPLICATION_JSON_TYPE.isCompatible(mediaType)
            || mediaType.getSubtype().endsWith("+json");
    }

    @Override
    public boolean isReadable(
        Class<?> c, Type t, Annotation[] a, MediaType mediaType) {

        return APPLICATION_JSON_TYPE.isCompatible(mediaType)
            || mediaType.getSubtype().endsWith("+json");
    }

    @Override
    public void writeTo(
        T o, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4,
        MultivaluedMap<String, java.lang.Object> arg5, OutputStream out)
        throws IOException, WebApplicationException {

        String json = converter.convert(o).to(String.class);
        out.write(json.getBytes());
    }

    @SuppressWarnings("unchecked")
    @Override
    public T readFrom(
        Class<T> arg0, Type arg1, Annotation[] arg2, MediaType arg3,
        MultivaluedMap<String, String> arg4, InputStream in)
        throws IOException, WebApplicationException {

    	BufferedReader reader =
            new BufferedReader(new InputStreamReader(in));
        return (T) converter.convert(reader.readLine()).to(arg1);
    }
}

Update the InverterRestService

  • Add the JAX-RS @Produces(MediaType.APPLICATION_JSON) annotation to the class definition to specify that JSON responses are created.
  • Add the @JSONRequired annotation to the class definition to mark this class to require JSON media type support.
  • Optional: Get multiple StringModifier injected and return a List of Strings as a result of the REST resource.
@Component(service=InverterRestService.class)
@JaxrsResource
@Produces(MediaType.APPLICATION_JSON)
@JSONRequired
public class InverterRestService {

	@Reference
	private volatile List<StringModifier> modifier;

	@GET
	@Path("modify/{input}")
	public List<String> modify(@PathParam("input") String input) {
		return modifier.stream()
				.map(mod -> mod.modify(input))
				.collect(Collectors.toList());
	}
}
  • Optional: Implement an additional StringModifier in the inverter module.
@Component
public class Upper implements StringModifier {

	@Override
	public String modify(String input) {
		return input.toUpperCase();
	}
}
  • In the Bndtools Explorer locate the app module.
  • Open app.bndrun
  • If you use org.apache.aries.jax.rs.jackson, add it to the Run Requirements
  • Click on Resolve to ensure that the Jackson libraries are part of the Run Bundles
  • Click on Run OSGi
  • Open a browser and navigate to http://localhost:8080/modify/fubar to see the updated result.

Multipart file upload

In the Panorama project the REST based cloud services are designed as file processing services. So you upload a file, process it and download the result. This way you can for example migrate Amalthea Model files to a newer version, perform a static analysis of an Amalthea Model and even transform an Amalthea Model to some executable format and execute the result for simulation scenarios.

When searching for file uploads with REST and Java, you only find information on how to do this with either Jersey or Apache CXF. But even though the Aries JAX-RS Whiteboard reference implementation is based on Apache CXF, none of the tutorials worked for me. The reason is that the Aries JAX-RS Whiteboard completely hides the underlying Apache CXF implementation. Thanks to Tim Ward who helped me on the OSGi mailing list, I was able to solve this. Therefore I want to share the solution here.

Multipart file upload requires support from the underlying servlet container. Using the OSGi enRoute Maven archetypes Apache Felix HTTP Jetty is included as implementation of the R7 OSGi HTTP Service and the R7 OSGi HTTP Whiteboard Specification. So a Jetty is included in the setup and multipart file uploads are supported.

Enable Multipart Support

According to the HTTP Whiteboard Specification, Multipart File Uploads need to be enabled via the corresponding component properties. This can be done for example by creating a custom JAX-RS Whiteboard Application and adding the @HttpWhiteboardServletMultipart Component Property Type annotation with the corresponding attributes.

Note: In this tutorial I will not use this approach, but for completeness I want to share how the creation and usage of a JAX-RS Whiteboard application can be done.

@Component(service=Application.class)
@JaxrsApplicationBase("app4mc")
@JaxrsName("app4mcMigration")
@HttpWhiteboardServletMultipart(enabled = true)
public class MigrationApplication extends Application {}

In this case the JAX-RS Whiteboard resource needs to be registered on the created application by using the @JaxrsApplicationSelect Component Property Type annotation.

@Component(service=Migration.class)
@JaxrsResource
@JaxrsApplicationSelect("(osgi.jaxrs.name=app4mcMigration)")
public class Migration {
...
}

Creating custom JAX-RS Whiteboard Applications make sense if you want to publish multiple applications in one installation/server. In a scenario where only one application is published in isolation, e.g. one REST based service in one container (e.g. Docker), the creation of a custom application is not necessary. Instead it is sufficient to configure the default application provided by the Aries JAX-RS Whiteboard implementation using the Configuration Admin. The PID and the available configuration properties are listed here.

Configuring an OSGi service programmatically via Configuration Admin is not very intuitive. While it is quite powerful to change configurations at runtime, it feels uncomfortable to provide a configuration to a component from the outside. Luckily with R7 the Configurator Specification was introduced to deal with this. Using the Configurator, the component configuration can be provided using a resource in JSON format.

  • First we need to specify the requirement on the Configurator. This can be done by using the @RequireConfigurator Bundle Annotation. Using the archetype this is already done in the app module.
    • In the Bndtools Explorer locate the app module.
    • Locate the package-info.java file in src/main/java/config.
    • Verify that it looks like the following snippet.
@RequireConfigurator
package config;

import org.osgi.service.configurator.annotations.RequireConfigurator;
  • Now locate the configuration.json file in src/main/resources/OSGI-INF/configurator
  • Modify the file to contain the multipart configuration:
    • org.apache.aries.jax.rs.whiteboard.default is the PID of the default application
    • osgi.http.whiteboard.servlet.multipart.enabled is the component property for enabling multipart file uploads
{
    ":configurator:resource-version" : 1,
    ":configurator:symbolic-name" : "org.fipro.modifier.app.config",
    ":configurator:version" : "1.0-SNAPSHOT",

    "org.apache.aries.jax.rs.whiteboard.default" : {
        "osgi.http.whiteboard.servlet.multipart.enabled" : "true"
    }
}
  • Open app.bndrun
    • Add org.fipro.modifier.app to the Run Requirements
    • Click Resolve to recalculate the Run Bundles

Note: While writing this blog post and tested the tutorial I noticed that on Resolve the inverter module was sometimes not resolved for whatever reason. To ensure that the application is started with all necessary bundles, add impl, app and inverter to the Run Requirements. Double check after Resolve that the following bundles are part of the Run Bundles:

  • org.fipro.modifier.api
  • org.fipro.modifier.app
  • org.fipro.modifier.impl
  • org.fipro.modifier.inverter

Process Multipart File Uploads

As the JAX-RS standards do not contain multipart support, we need to fallback to Servlet implementations. Fortunately we can get JAX-RS resources injected as method parameter or fields by using for example the @Context JAX-RS annotation. For the multipart support we can get the HttpServletRequest injected and extract the information from there.

  • Update the InverterRestService
  • Add the following JAX-RS resource method
@POST
@Path("modify/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)
public Response upload(@Context HttpServletRequest request)
        throws IOException, ServletException {

    // get the part with name "file" received by within
    // a multipart/form-data POST request
    Part part = request.getPart("file");
    if (part != null
            && part.getSubmittedFileName() != null
            && part.getSubmittedFileName().length() > 0) {

        StringBuilder inputBuilder = new StringBuilder();
        try (InputStream is = part.getInputStream();
                BufferedReader br =
                    new BufferedReader(new InputStreamReader(is))) {

            String line;
            while ((line = br.readLine()) != null) {
                inputBuilder.append(line).append("\n");
            }
        }

        // modify file content
        String input = inputBuilder.toString();
        List<String> modified = modifier.stream()
            .map(mod -> mod.modify(input))
            .collect(Collectors.toList());

            return Response.ok(String.join("\n\n", modified)).build();
    }

    return Response.status(Status.PRECONDITION_FAILED).build();
}
  • @Consumes(MediaType.MULTIPART_FORM_DATA) Specify that this REST resource consumes multipart/form-data.
  • @Produces(MediaType.TEXT_PLAIN) Specify that the result is plain text, which is for this use case the easiest way for returning the modified file content.
  • @Context HttpServletRequest request The HttpServletRequest is injected as method parameter.
  • Part part = request.getPart("file") Extract the Part with the name file (which is actually the form parameter name) from the HttpServletRequest.

If you are using a tool like Postman, you can test if the multipart upload is working by starting the app via app.bndrun and execute a POST request on http://localhost:8080/modify/upload

Interlude: Static Resources

To also be able to test the upload without additional tools, we publish a simple form as a static resource in our application. We use the HTTP Whiteboard Specification to register an HTML form as static resource with our REST service. For this add the @HttpWhiteboardResource component property type annotation to the InverterRestService.

@HttpWhiteboardResource(pattern = "/files/*", prefix = "static")

With this configuration all requests to URLs with the /files path are mapped to resources in the static folder. The next step is therefore to add the static form to the project:

  • In the Bndtools Explorer locate the impl module.
  • Right click src/main/java - New - Folder
  • Select the main folder in the tree
  • Add resources/static in the Folder name field
  • Finish
  • Right click on the created resources folder in the Bndtools Explorer
  • Build Path - Use as Source Folder
  • Create a new file upload.html in scr/main/resources/static
<html>
<body>
    <h1>File Upload with JAX-RS</h1>
    <form
        action="http://localhost:8080/modify/upload"
        method="post"
        enctype="multipart/form-data">

        <p>
            Select a file : <input type="file" name="file" size="45"/>
        </p>

        <input type="submit" value="Upload It"/>
    </form>
</body>
</html>

After starting the app via app.bndrun you can open a browser and navigate to http://localhost:8080/files/upload.html Now you can select a file (don’t use a binary file) and upload it to see the modification result of the REST service.

Debugging / Inspection

To debug your REST based service you can start the application by using Debug OSGi instead of Run OSGi in the app.bndrun. But in the OSGi context you often face issues even before you can debug code. For this the app archetype creates an additional debug run configuration. The debug.bndrun file is located next to the app.bndrun file in the app module.

  • In the Bndtools Explorer locate the app module.
  • Open debug.bndrun
  • Click on Resolve
  • Click on Run OSGi

With the debug run configuration the following additional features are enabled to inspect the runtime:

This allows to interact with the Gogo Shell in the Console View. And even more comfortable by using the Webconsole. For the later open a browser and navigate to http://localhost:8080/system/console. Login with the default username/password admin/admin. Using the Webconsole you can check which bundles are installed and in which state they are. You can also inspect the available OSGi DS Components and check the active configurations.

Build

As the project setup is a plain Java/Maven project, the build is pretty easy:

  • In the Bndtools Explorer locate the jaxrs module (the top level project).
  • Right click - Run As - Maven build…
  • Enter clean verify in the Goals field
  • Run

From the command line:

  • Switch to the jaxrs directory that was created by the archetype
  • Execute mvn clean verify

Note:
It can happen that an error occurs on building the app module if you followed the steps in this tutorial exactly. The reason is that the build locates a change in the Run Bundles of the app.bndrun file. But it is just a difference in the ordering of the bundles. To solve this open the app.bndrun file, remove all entries from the Run Bundles and hit Resolve again. After that the order of the Run Bundles will be the same as the one in the build.

Note:
This build process works because we used the Eclipse IDE with Bndtools. If you are using another IDE or working only on the command line, have a look at the OSGi enRoute Microservices Tutorial that explains the separate steps for building from command line.

After the build succeeds you will find the resulting app.jar in jaxrs/app/target. Execute the following line to start the self-executable jar from the command line if you are located in the jaxrs folder:

java -jar app/target/app.jar

If you also want to build the debug configuration, you need to enable this in the pom.xml file of the app module:

  • In the Bndtools Explorer locate the app module.
  • Open pom.xml
  • In the build/plugins section update the bnd-export-maven-plugin and add the debug.bndrun to the bndruns.
<plugin>
    <groupId>biz.aQute.bnd</groupId>
    <artifactId>bnd-export-maven-plugin</artifactId>
    <configuration>
        <bndruns>
            <bndrun>app.bndrun</bndrun>
            <bndrun>debug.bndrun</bndrun>
        </bndruns>
    </configuration>
</plugin>

Executing the build again, you will now also find a debug.jar in the target folder of the app module, you can use to inspect the OSGi runtime.

Summary

While setting up this tutorial I faced several issues that mainly came from missing information or misunderstandings. Luckily the OSGi community was really helpful in solving this. So my contribution back is to write this blog post to help others that struggle with similar issues. The key takeaways are:

  • Using the OSGi enRoute Maven archetypes we have plain Java Maven projects. That means:
    • There is no Bundle Descriptor File (.bnd), so the package-info.java file is an important source for the MANIFEST.MF creation.
    • Dependencies to other modules need to be specified in the pom.xml files. This also includes modules in the same workspace.

Note:
The Maven project structure also causes quite some headache if you want to wrap OSGi services from Eclipse projects like APP4MC. Usually Eclipse projects publish their results as p2 update sites and not via Maven. And for Maven projects it is no possible to consume p2 update sites. Luckily more and more projects publish their results on Maven Central. And the APP4MC project plans to also do this. We are currently cleaning up the dependencies to make it possible to at least consume the model implementation easily from any Java based project. As long as dependencies are not available via Maven Central, the only way to solve the build is to install the artifacts in the local repository. This can either be done by building and installing the resulting artifacts locally via mvn clean install. Alternatively you can use the maven-install-plugin, which can even be integrated into your Maven build if you add the artifact to install to the source code repository. Thanks to Neil Bartlett who gave me the necessary pointer on this topic.

  • With OSGi R7 there are quite some interesting new specifications, that in combination make development with OSGi a lot more comfortable. The ones used in this tutorial are:
  • Using the Maven archetypes and the OSGi R7 specifications, implementing JAX-RS REST based services is similar to approaches with other frameworks like Spring Boot or Microprofile. And if you want to wrap existing OSGi services, it is definitely the most comfortable one. If consuming OSGi services is not needed, well then every framework has its pros and cons.

The sources of this tutorial are available on GitHub.

For an extended example have a look at the APP4MC Cloud Services.

Now I have a blog post about HTTP Service / HTTP Whiteboard and JAX-RS Whiteboard. The still missing blog post about Remote Services is not forgotten, but obviously I need more time to write about it, as it is the most complicated specification in OSGi. So stay tuned for that one. :)

Updated: