Access OSGi Services via web interface

17 minute read

In this blog post I want to share a simple approach to make OSGi services available via web interface. I will show a simple approach that includes the following:

  • Embedding a Jetty  Webserver in an OSGi application
  • Registering a Servlet via OSGi DS using the HTTP Whiteboard specification

I will only cover this simple scenario here and will not cover accessing OSGi services via REST interface. If you are interested in that you might want to look at the OSGi - JAX-RS Connector, which looks also very nice. Maybe I will look at this in another blog post. For now I will focus on embedding a Jetty Server and deploy some resources.

I will skip the introduction on OSGi DS and extend the examples from my Getting Started with OSGi Declarative Services blog. It is easier to follow this post when done the other tutorial first, but it is not required if you adapt the contents here to your environment.

As a first step create a new project org.fipro.inverter.http. In this project we will add the resources created in this tutorial. If you use PDE you should create a new Plug-in Project, with Bndtools create a new Bnd OSGi Project using the Component Development template.

PDE - Target Platform

In PDE it is best practice to create a Target Definition so the work is based on a specific set of bundles and we don’t need to install bundles in our IDE. Follow these steps to create a Target Definition for this tutorial:

  • Create a new target definition
    • Right click on project org.fipro.inverter.http → New → Other… → Plug-in Development → Target Definition
    • Set the filename to org.fipro.inverter.http.target
    • Initialize the target definition with: Nothing: Start with an empty target definition
  • Add a new Software Site in the opened Target Definition Editor by clicking Add… in the Locations section
    • Select Software Site
    • Software Site http://download.eclipse.org/releases/oxygen
    • Disable Group by Category
    • Select the following entries
      • Equinox Core SDK
      • Equinox Compendium SDK
      • Jetty Http Server Feature
    • Click Finish
  • Optional: Add a new Software Site to include JUnit to the Target Definition (only needed in case you followed all previous tutorials on OSGi DS or want to integrate JUnit tests for your services)
    • Software Site http://download.eclipse.org/tools/orbit/R-builds/R20170307180635/repository
    • Select JUnit Testing Framework
    • Click Finish
  • Save your work and activate the target platform by clicking Set as Target Platform in the upper right corner of the Target Definition Editor

Bndtools - Repository

Using Bndtools is different as you already know if you followed my previous blog posts. To be also able to follow this blog post by using Bndtools, I will describe the necessary steps here.

We will use Apache Felix in combination with Bndtools instead of Equinox. This way we don’t need to modify the predefined repository and can start without further actions. The needed Apache Felix bundles are already available.

PDE - Prepare project dependencies

We will prepare the project dependencies in advance so it is easier to copy and paste the code samples to the project. Within the Eclipse IDE the Quick Fixes would also support adding the dependencies afterwards of course.

  • Open the MANIFEST.MF file of the org.fipro.inverter.http project and switch to the Dependencies tab
  • Add the following two dependencies on the Imported Packages side:
    • javax.servlet (3.1.0)
    • javax.servlet.http (3.1.0)
    • org.fipro.inverter (1.0.0)
    • org.osgi.service.component.annotations (1.3.0)
  • Mark org.osgi.service.component.annotations as Optional via Properties…
  • Add the upper version boundaries to the Import-Package statements.

Bndtools - Prepare project dependencies

  • Open the bnd.bnd file of the org.fipro.inverter.http project and switch to the Build tab
  • Add the following bundles to the Build Path
    • org.apache.http.felix.jetty
    • org.apache.http.felix.servlet-api
    • org.fipro.inverter.api

Create a Servlet implementation

  • Create a new package org.fipro.inverter.http
  • Create a new class InverterServlet
@Component(
    service=Servlet.class,
    property= "osgi.http.whiteboard.servlet.pattern=/invert",
    scope=ServiceScope.PROTOTYPE) 
public class InverterServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Reference
    private StringInverter inverter;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String input = req.getParameter("value"); 
        if (input == null) { 
            throw new IllegalArgumentException("input can not be null"); 
        } 
        String output = inverter.invert(input);

        resp.setContentType("text/html"); 
        resp.getWriter().write( "<html><body>Result is " + output + "</body></html>"); 
    }
}

Let’s look at the implementation:

  1. It is a typical Servlet implementation that extends javax.servlet.http.HttpServlet
  2. It is also an OSGi Declarative Service that is registered as service of type javax.servlet.Servlet
  3. The service has PROTOTYPE scope
  4. A special property osgi.http.whiteboard.servlet.pattern is set. This configures the context path of the Servlet.
  5. It references the StringInverter OSGi service from the previous tutorial via field reference. And yes since Eclipse Oxygen this is also supported in Equinox (I wrote about this here).

PDE - Launch the example

Before explaining the details further, launch the example to see if our servlet is available via standard web browser. For this we create a launch configuration, so we can start directly from the IDE.

  • Select the menu entry Run -> Run Configurations…
  • In the tree view, right click on the OSGi Framework node and select New from the context menu
  • Specify a name, e.g. OSGi Inverter Http
  • Deselect All
  • Select the following bundles (note that we are using Eclipse Oxygen, in previous Eclipse versions org.apache.felix.scr and org.eclipse.osgi.util are not required)
    • Application bundles
      • org.fipro.inverter.api
      • org.fipro.inverter.http
      • org.fipro.inverter.provider
    • Console bundles
      • org.apache.felix.gogo.command
      • org.apache.felix.gogo.runtime
      • org.apache.felix.gogo.shell
      • org.eclipse.equinox.console
    • OSGi framework and DS bundles
      • org.apache.felix.scr
      • org.eclipse.equinox.ds
      • org.eclipse.osgi
      • org.eclipse.osgi.services
      • org.eclipse.osgi.util
    • Equinox Http Service and Http Whiteboard
      • org.eclipse.equinox.http.jetty
      • org.eclipse.equinox.http.servlet
    • Jetty
      • javax.servlet
      • org.eclipse.jetty.continuation
      • org.eclipse.jetty.http
      • org.eclipse.jetty.io
      • org.eclipse.jetty.security
      • org.eclipse.jetty.server
      • org.eclipse.jetty.servlet
      • org.eclipse.jetty.util
  • Ensure that Default Auto-Start is set to true
  • Switch to the Arguments tab
    • Add -Dorg.osgi.service.http.port=8080 to the VM arguments
  • Click Run

Note:
If you include the above bundles in an Eclipse RCP application, ensure that you auto-start the org.eclipse.equinox.http.jetty bundle to automatically start the Jetty server. This can be done on the Configuration tab of the Product Configuration Editor.

If you now open a browser and go to the URL http://localhost:8080/invert?value=Eclipse you should get a response with the inverted output.

Bndtools - Launch the example

  • Open the launch.bndrun file in the org.fipro.inverter.http project
  • On the Run tab add the following bundles to the Run Requirements
    • org.fipro.inverter.http
    • org.fipro.inverter.provider
    • org.apache.felix.http.jetty
  • Click Resolve to ensure all required bundles are added to the Run Bundles via auto-resolve
  • Add -Dorg.osgi.service.http.port=8080 to the JVM Arguments
  • Click Run OSGi

Http Service & Http Whiteboard

Now why is this simply working? We only implemented a servlet and provided it as OSGi DS. And it is “magically” available via web interface. The answer to this is the OSGi Http Service Specification and the Http Whiteboard Specification. The OSGi Compendium Specification contains the Http Service Specification Version 1.2 and the Http Whiteboard Specification Version 1.0.

The purpose of the Http Service is to provide access to services on the internet or other networks for example by using a standard web browser. This can be done by registering servlets or resources to the Http Service. Without going too much into detail, the implementation is similar to an embedded web server, which is the reason why the default implementations in Equinox and Felix are based on Jetty.

To register servlets and resources to the Http Service you know the Http Service API very well and you need to retrieve the Http Service and directly operate on it. As this is not every convenient, the Http Whiteboard Specification was introduced. This allows to register servlets and resources via the Whiteboard Pattern, without the need to know the Http Service API in detail. I always think about the whiteboard pattern as a “don’t call us, we will call you” pattern. That means you don’t need to register servlets on the Http Service directly, you will provide it as a service to the service registry, and the Http Whiteboard implementation will take it and register it to the Http Service.

Via Http Whiteboard it is possible to register:

  • Servlets
  • Servlet Filters
  • Resources
  • Servlet Listeners

I will show some examples to be able to play around with the Http Whiteboard service.

Register Servlets

An example on how to register a servlet via Http Whiteboard is shown above. The main points are:

  • The servlet needs to be registered as OSGi service of type javax.servlet.Servlet.
  • The component property osgi.http.whiteboard.servlet.pattern needs to be set to specify the request mappings.
  • The service scope should be PROTOTYPE.

For registering servlets the following component properties are supported. (see OSGi Compendium Specification - Table 140.4):

Component Property Description
osgi.http.whiteboard.servlet.asyncSupported Declares whether the servlet supports the asynchronous operation mode. Allowed values are true and false independent of case. Defaults to false.
osgi.http.whiteboard.servlet.errorPage Register the servlet as an error page for the error code and/or exception specified; the value may be a fully qualified exception type name or a three-digit HTTP status code in the range 400-599. Special values 4xx and 5xx can be used to match value ranges. Any value not being a three-digit number is assumed to be a fully qualified exception class name.
osgi.http.whiteboard.servlet.name The name of the servlet. This name is used as the value of the javax.servlet.ServletConfig.getServletName() method and defaults to the fully qualified class name of the service object.
osgi.http.whiteboard.servlet.pattern Registration pattern(s) for the servlet.
servlet.init.* Properties starting with this prefix are provided as init parameters to the javax.servlet.Servlet.init(ServletConfig) method. The servlet.init. prefix is removed from the parameter name.

The Http Whiteboard service needs to call javax.servlet.Servlet.init(ServletConfig) to initialize the servlet before it starts to serve requests, and when it is not needed anymore javax.servlet.Servlet.destroy() to shut down the servlet. If more than one Http Whiteboard implementation is available in a runtime, the init() and destroy() calls would be executed multiple times, which violates the Servlet specification. It is therefore recommended to use the PROTOTYPE scope for servlets to ensure that every Http Whiteboard implementation gets its own service instance.

Note:
In a controlled runtime, like an RCP application that is delivered with one Http Whiteboard implementation and that does not support installing bundles at runtime, the usage of the PROTOTYPE scope is not required. Actually such a runtime ensures that the servlet is only instantiated and initialized once. But if possible it is recommended that the PROTOTYPE scope is used.

To register a servlet as an error page, the service property osgi.http.whiteboard.servlet.errorPage needs to be set. The value can be either a three-digit  HTTP error code, the special codes 4xx or 5xx to specify a range or error codes, or a fully qualified exception class name. The service property osgi.http.whiteboard.servlet.pattern is not required for servlets that provide error pages.

The following snippet shows an error page servlet that deals with IllegalArgumentExceptions and the HTTP error code 500. It can be tested by calling the inverter servlet without a query parameter.

@Component(
    service=Servlet.class,
    property= {
        "osgi.http.whiteboard.servlet.errorPage=java.lang.IllegalArgumentException",
        "osgi.http.whiteboard.servlet.errorPage=500"
    },
    scope=ServiceScope.PROTOTYPE) 
public class ErrorServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        resp.setContentType("text/html"); 
        resp.getWriter().write( "<html><body>You need to provide an input!</body></html>"); 
    } 
}

Register Filters

Via servlet filters it is possible to intercept servlet invocations. They are used to modify the ServletRequest and ServletResponse to perform common tasks before and after the servlet invocation.

The example below shows a servlet filter that adds a simple header and footer on each request to the servlet with the /invert pattern:

@Component(
    property = "osgi.http.whiteboard.filter.pattern=/invert",
    scope=ServiceScope.PROTOTYPE) 
public class SimpleServletFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException { }

    @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        response.setContentType("text/html");
        response.getWriter().write("<b>Inverter Servlet</b><p>");
        chain.doFilter(request, response);
        response.getWriter().write("</p><i>Powered by fipro</i>");
    }

    @Override
    public void destroy() { }

}

To register a servlet filter the following criteria must match:

  • It needs to be registered as OSGi service of type javax.servlet.Filter.
  • One of the given component properties needs to be set:
    • osgi.http.whiteboard.filter.pattern
    • osgi.http.whiteboard.filter.regex
    • osgi.http.whiteboard.filter.servlet
  • The service scope should be PROTOTYPE.

For registering servlet filters the following service properties are supported. (see OSGi Compendium Specification - Table 140.5):

Service Property Description
osgi.http.whiteboard.filter.asyncSupported Declares whether the servlet filter supports asynchronous operation mode. Allowed values are true and false independent of case. Defaults to false.
osgi.http.whiteboard.filter.dispatcher Select the dispatcher configuration when the servlet filter should be called. Allowed string values are REQUEST, ASYNC, ERROR, INCLUDE, and FORWARD. The default for a filter is REQUEST.
osgi.http.whiteboard.filter.name The name of a servlet filter. This name is used as the value of the FilterConfig.getFilterName() method and defaults to the fully qualified class name of the service object.
osgi.http.whiteboard.filter.pattern Apply this servlet filter to the specified URL path patterns. The format of the patterns is specified in the servlet specification.
osgi.http.whiteboard.filter.regex Apply this servlet filter to the specified URL paths. The paths are specified as regular expressions following the syntax defined in the java.util.regex.Pattern class.
osgi.http.whiteboard.filter.servlet Apply this servlet filter to the referenced servlet(s) by name.
filter.init.* Properties starting with this prefix are passed as init parameters to the Filter.init() method. The filter.init. prefix is removed from the parameter name.

Register Resources

It is also possible to register a service that informs the Http Whiteboard service about static resources like HTML files, images, CSS- or Javascript-files. For this a simple service can be registered that only needs to have the following two mandatory service properties set:

Service Property Description
osgi.http.whiteboard.resource.pattern The pattern(s) to be used to serve resources. As defined by the [4] Java Servlet 3.1 Specification in section 12.2, Specification of Mappings.This property marks the service as a resource service.
osgi.http.whiteboard.resource.prefix The prefix used to map a requested resource to the bundle’s entries. If the request’s path info is not null, it is appended to this prefix. The resulting string is passed to the getResource(String) method of the associated Servlet Context Helper.

The service does not need to implement any specific interface or function. All required information is provided via the component properties.

To create a resource service follow these steps:

  • Create a folder resources in the project org.fipro.inverter.http
  • Add an image in that folder, e.g. eclipse_logo.png
  • PDE
    • Add the resources folder in the build.properties
  • Bndtools
    • Add the following line to the bnd.bnd file on the Source tab
      -includeresource: resources=resources
  • Create resource service
@Component(
    service = ResourceService.class,
    property = {
        "osgi.http.whiteboard.resource.pattern=/files/*",
        "osgi.http.whiteboard.resource.prefix=/resources"
    }) 
public class ResourceService { }

After starting the application the static resources located in the resources folder are available via the /files path in the URL, e.g. http://localhost:8080/files/eclipse_logo.png

Note:
While writing this blog post I came across a very nasty issue. Because I initially registered the servlet filter for the /* pattern, the simple header and footer where always added. This also caused setting the content type, that didn’t match the content type of the image of course. And so the static content was never shown correctly. So if you want to use servlet filters to add common headers and footers, you need to take care of the pattern so the servlet filter is not applied to static resources.

Register Servlet Listeners

It is also possible to register different servlet listeners as whiteboard services. The following listeners are supported according to the servlet specification:

  • ServletContextListener
    Receive notifications when Servlet Contexts are initialized and destroyed.
  • ServletContextAttributeListener
    Receive notifications for Servlet Context attribute changes.
  • ServletRequestListener
    Receive notifications for servlet requests coming in and being destroyed.
  • ServletRequestAttributeListener
    Receive notifications when servlet Request attributes change.
  • HttpSessionListener
    Receive notifications when Http Sessions are created or destroyed.
  • HttpSessionAttributeListener
    Receive notifications when Http Session attributes change.
  • HttpSessionIdListener
    Receive notifications when Http Session ID changes.

There is only one component property needed to be set so the Http Whiteboard implementation is handling the listener.

Service Property Description
osgi.http.whiteboard.listener When set to true this listener service is handled by the Http Whiteboard implementation. When not set or set to false the service is ignored. Any other value is invalid.

The following example shows a simple ServletRequestListener that prints out the client address on the console for each request (borrowed from the OSGi Compendium Specification):

@Component(property = "osgi.http.whiteboard.listener=true") 
public class SimpleServletRequestListener implements ServletRequestListener {

    public void requestInitialized(ServletRequestEvent sre) { 
        System.out.println("Request initialized for client: " + sre.getServletRequest().getRemoteAddr()); 
    }

    public void requestDestroyed(ServletRequestEvent sre) { 
        System.out.println("Request destroyed for client: " + sre.getServletRequest().getRemoteAddr()); 
    }
}

Servlet Context and Common Whiteboard Properties

The ServletContext is specified in the servlet specification and provided to the servlets at runtime by the container. By default there is one ServletContext and without additional information the servlets are registered to that default ServletContext via the Http Whiteboard implementation. This could lead to scenarios where different bundles provide servlets for the same request mapping. In that case the service.ranking will be inspected to decide which servlet should be delivered. If the servlets belong to different applications, it is possible to specify different contexts. This can be done by registering a custom ServletContextHelper as whiteboard service and associate the servlets to the corresponding context. The ServletContextHelper can be used to customize the behavior of the ServletContext (e.g. handle security, provide resources, …) and to support multiple web-applications via different context paths.

A custom ServletContextHelper it needs to be registered as service of type ServletContextHelper and needs to have the following two service properties set:

  • osgi.http.whiteboard.context.name
  • osgi.http.whiteboard.context.path
Service Property Description
osgi.http.whiteboard.context.name Name of the Servlet Context Helper. This name can be referred to by Whiteboard services via the osgi.http.whiteboard.context.select property. The syntax of the name is the same as the syntax for a Bundle Symbolic Name. The default Servlet Context Helper is named default. To override the default, register a custom ServletContextHelper service with the name default. If multiple Servlet Context Helper services are registered with the same name, the one with the highest Service Ranking is used. In case of a tie, the service with the lowest service ID wins. In other words, the normal OSGi service ranking applies.
osgi.http.whiteboard.context.path Additional prefix to the context path for servlets. This property is mandatory. Valid characters are specified in IETF RFC 3986, section 3.3. The context path of the default Servlet Context Helper is /. A custom default Servlet Context Helper may use an alternative path.
context.init.* Properties starting with this prefix are provided as init parameters through the ServletContext.getInitParameter() and ServletContext.getInitParameterNames() methods. The context.init. prefix is removed from the parameter name.

The following example will register a ServletContextHelper for the context path /eclipse and will retrieve resources from http://www.eclipse.org. It is registered with BUNDLE service scope to ensure that every bundle gets its own instance, which is for example important to resolve resources from the correct bundle.

Note:
Create it in a new package org.fipro.inverter.http.eclipse within the org.fipro.inverter.http project, as we will need to create some additional resources to show how this example actually works.

@Component(
    service = ServletContextHelper.class,
    scope = ServiceScope.BUNDLE,
    property = {
        "osgi.http.whiteboard.context.name=eclipse",
        "osgi.http.whiteboard.context.path=/eclipse"
    }) 
public class EclipseServletContextHelper extends ServletContextHelper {

    public URL getResource(String name) {
        // remove the path from the name 
        name = name.replace("/eclipse", ""); 
        try { 
            return new URL("http://www.eclipse.org/" + name); 
        } catch (MalformedURLException e) { 
            return null; 
        } 
    } 
}

Note:
With PDE remember to add org.osgi.service.http.context to the Imported Packages. With Bndtools remember to add the new package to the Private Packages in the bnd.bnd file on the Contents tab.

To associate servlets, servlet filter, resources and listeners to a ServletContextHelper, they share common service properties (see OSGi Compendium Specification - Table 140.3) additional to the service specific properties:

Service Property Description
osgi.http.whiteboard.context.select An LDAP-style filter to select the associated ServletContextHelper service to use. Any service property of the Servlet Context Helper can be filtered on. If this property is missing the default Servlet Context Helper is used. For example, to select a Servlet Context Helper with name myCTX provide the following value: (osgi.http.whiteboard.context.name=myCTX)To select all Servlet Context Helpers provide the following value: (osgi.http.whiteboard.context.name=*)
osgi.http.whiteboard.target The value of this service property is an LDAP style filter expression to select the Http Whiteboard implementation(s) to handle this Whiteboard service. The LDAP filter is used to match HttpServiceRuntime services. Each Http Whiteboard implementation exposes exactly one HttpServiceRuntime service. This property is used to associate the Whiteboard service with the Http Whiteboard implementation that registered the HttpServiceRuntime service. If this property is not specified, all Http Whiteboard implementations can handle the service.

The following example will register a servlet only for the introduced /eclipse context:

@Component(
    service=Servlet.class,
    property= {
        "osgi.http.whiteboard.servlet.pattern=/image",
        "osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.name=eclipse)"
    },
    scope=ServiceScope.PROTOTYPE) 
public class ImageServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        resp.setContentType("text/html"); 
        resp.getWriter().write("Show an image from www.eclipse.org"); 
        resp.getWriter().write( "<p><img src='img/nattable/images/FeatureScreenShot.png'/></p>"); 
    }
}

And to make this work in combination with the introduced ServletContextHelper we need to additionally register the resources for the /img context, which is also only assigned to the /eclipse context:

@Component(
    service = EclipseImageResourceService.class,
    property = {
        "osgi.http.whiteboard.resource.pattern=/img/*",
        "osgi.http.whiteboard.resource.prefix=/eclipse",
        "osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.name=eclipse)"
    }) 
public class EclipseImageResourceService { }

If you start the application and browse to http://localhost:8080/eclipse/image you will see an output from the servlet together with an image that is loaded from http://www.eclipse.org.

Note:
The component properties and predefined values are available via org.osgi.service.http.whiteboard.HttpWhiteboardConstants. So you don’t need to remember them all and can also retrieve some additional information about the properties via the corresponding Javadoc.

The sources for this tutorial are hosted on GitHub in the already existing projects:

Updated: