Build REST services with the OSGi Whiteboard Specification for Jakarta™ RESTful Web Services

49 minute read

Several years ago I wrote a blog post about creating a REST service out of an OSGi service. At that time I used the OSGi R7 JAX-RS Whiteboard Specification and the Aries JAX-RS Whiteboard reference implementation. Since then several things happened:

Because of the above reasons, the OSGi Compendium Release 8.1 contains the Whiteboard Specification for Jakarta™ RESTful Web Services. And additionally the OSGi Technology Whiteboard Implementation for Jakarta RESTful Web Services reference implementation is available in the Eclipse namespace.

The following tutorial is actually an update to the old one, using the new specification and reference implementation, to be able to create RESTful Web Services using OSGi.

Create the project structure

Currently there are no Maven archetypes for OSGi that would be really helpful. The enRoute archetypes are outdated and only generate skeletons for OSGi R7 projects. The org.eclipse.osgitech.rest.archetype generates the skeleton for a single project, which is helpful to identify the dependencies, but not helpful in a multi-module project.

If you want to try out the org.eclipse.osgitech.rest.archetype provided by the reference implementation, you can use the following command:

mvn archetype:generate \
-DarchetypeGroupId=org.eclipse.osgi-technology.rest \
-DarchetypeArtifactId=org.eclipse.osgitech.rest.archetype \
-DarchetypeVersion=1.2.2 \
-DgroupId=org.fipro.modifier \
-DartifactId=jakartars \
-Dversion=1.0.0-SNAPSHOT \
-Dpackage=org.fipro.modifier.jakartars

As mentioned this creates a single jakartars project. Unfortunately with the above command, the generated project structure is also not valid and shows up with compile errors, as explained in this GitHub Issue.

We will not use the mentioned archetype, as we want to build a multi-module project. So let’s start to create the project structure using the default Maven archetypes similar to Multi-Module Project with Maven:

Create the parent project

mvn archetype:generate \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DgroupId=org.fipro.service.modifier \
-DartifactId=jakartars \
-Dversion=1.0.0-SNAPSHOT \
-DinteractiveMode=false
  • Switch to the created jakartars folder and
  • Delete the src folder
    cd jakartars
    rmdir src /s ... rm -r src
    
  • Open the pom.xml file and set the packaging to pom

Create the child modules for Service API, Service Implementation, the REST Service and the application

  mvn archetype:generate \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DgroupId=org.fipro.service.modifier \
  -DartifactId=api \
  -Dversion=1.0.0-SNAPSHOT \
  -Dpackage=org.fipro.service.modifier.api \
  -DinteractiveMode=false

  mvn archetype:generate \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DgroupId=org.fipro.service.modifier \
  -DartifactId=impl \
  -Dversion=1.0.0-SNAPSHOT \
  -Dpackage=org.fipro.service.modifier.impl \
  -DinteractiveMode=false

  mvn archetype:generate \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DgroupId=org.fipro.service.modifier \
  -DartifactId=rest \
  -Dversion=1.0.0-SNAPSHOT \
  -Dpackage=org.fipro.service.modifier.rest \
  -DinteractiveMode=false

  mvn archetype:generate \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DgroupId=org.fipro.service.modifier \
  -DartifactId=app \
  -Dversion=1.0.0-SNAPSHOT \
  -Dpackage=org.fipro.service.modifier.app \
  -DinteractiveMode=false

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 jakartars directory

Open the jakartars/pom.xml parent pom file and add the following configurations:

<properties>
  <java.version>17</java.version>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <bnd.version>7.0.0</bnd.version>
  <jakartars.whiteboard.version>1.2.2</jakartars.whiteboard.version>
  <jersey.version>3.1.5</jersey.version>
</properties>

Note:
We will use the newest Bndtools 7.0.0 which requires Java 17 for the execution. If you need to use Java 11 in your setup, use Bndtools 6.4.0.

At the time writing this blog post, the current released version of the org.eclipse.osgi-technology.rest artefacts is 1.2.2. Double check if in the meanwhile a newer version was published. In case you want to test a SNAPSHOT version, you need to add the following snippet to your Maven settings.xml:

<profiles>
	<profile>
		<id>oss-sonatype-snapshots</id>
		<repositories>
			<repository>
				 <id>OSSRH</id>
				 <name>Maven OSSRH Snapshots</name>
				 <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
				 <snapshots>
					 <enabled>true</enabled>
				 </snapshots>
				 <releases>
					 <enabled>true</enabled>
				 </releases>
			</repository>
		</repositories>
	</profile>
</profiles>
<activeProfiles>
	<activeProfile>oss-sonatype-snapshots</activeProfile>
</activeProfiles>

Note:
On Windows there is some formatting issue when using the archetypes. For every additional module you create, an empty line with some spaces is added between the content lines. If you followed the tutorial and created 5 modules, you will see 5 empty lines between every content line. To clean this up and make the enroute/pom.xml file readable again, you can do a search and replace via regular expression in an editor of your choice. Use the following regex and replace it with nothing

^(?:[\t ]*(?:\r?\n|\r))+

The following screenshot shows the settings in the Find/Replace dialog that can be used to cleanup:

  • Remove the dependencies section from the jakartars/pom.xml
  • Add the dependencyManagement section similar to the following snippet
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>osgi.core</artifactId>
      <version>8.0.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>osgi.annotation</artifactId>
      <version>8.1.0</version>
      <scope>provided</scope>
    </dependency>

    <!--  The OSGi framework RI is Equinox  -->
    <dependency>
      <groupId>org.eclipse.platform</groupId>
      <artifactId>org.eclipse.osgi</artifactId>
      <version>3.18.600</version>
      <scope>runtime</scope>
    </dependency>

    <!--  Declarative Services  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.component</artifactId>
      <version>1.5.1</version>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.component.annotations</artifactId>
      <version>1.5.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.scr</artifactId>
      <version>2.2.6</version>
      <scope>runtime</scope>
      <exclusions>
        <exclusion>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>animal-sniffer-annotations</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

    <!--  Configuration Admin  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.cm</artifactId>
      <version>1.6.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.configadmin</artifactId>
      <version>1.9.26</version>
      <scope>runtime</scope>
    </dependency>

    <!--  OSGi Configurator  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.configurator</artifactId>
      <version>1.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.configurator</artifactId>
      <version>1.0.18</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.cm.json</artifactId>
      <version>2.0.2</version>
      <scope>runtime</scope>
    </dependency>

    <!--  Event Admin  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.event</artifactId>
      <version>1.4.1</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.platform</groupId>
      <artifactId>org.eclipse.equinox.event</artifactId>
      <version>1.6.200</version>
      <scope>runtime</scope>
    </dependency>

    <!--  Log Stream Service  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.log</artifactId>
      <version>1.5.0</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.eclipse.platform</groupId>
      <artifactId>org.eclipse.equinox.log.stream</artifactId>
      <version>1.1.100</version>
      <scope>runtime</scope>
    </dependency>

    <!--  Metatype  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.metatype</artifactId>
      <version>1.4.1</version>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.metatype.annotations</artifactId>
      <version>1.4.1</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.platform</groupId>
      <artifactId>org.eclipse.equinox.metatype</artifactId>
      <version>1.6.300</version>
      <scope>runtime</scope>
    </dependency>

    <!--  OSGi Converter  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.util.converter</artifactId>
      <version>1.0.9</version>
      <scope>runtime</scope>
    </dependency>

    <!--  OSGi Function  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.util.function</artifactId>
      <version>1.2.0</version>
      <scope>runtime</scope>
    </dependency>

    <!--  OSGi Promise  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.util.promise</artifactId>
      <version>1.3.0</version>
      <scope>runtime</scope>
    </dependency>

    <!--  OSGi PushStream  -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.util.pushstream</artifactId>
      <version>1.1.0</version>
      <scope>runtime</scope>
    </dependency>

    <!-- Jakarta Servlet Whiteboard -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.servlet</artifactId>
      <version>2.0.0</version>
    </dependency>

    <!-- Jakarta RESTful Web Services Whiteboard -->
    <dependency>
      <groupId>jakarta.ws.rs</groupId>
      <artifactId>jakarta.ws.rs-api</artifactId>
      <version>3.1.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.jakartars</artifactId>
      <version>2.0.0</version>
    </dependency>
    <!-- The whiteboard implementation -->
    <dependency>
      <groupId>org.eclipse.osgi-technology.rest</groupId>
      <artifactId>org.eclipse.osgitech.rest</artifactId>
      <version>${jakartars.whiteboard.version}</version>
      <scope>runtime</scope>
    </dependency>
    <!-- The whiteboard implementation default configuration, when you want to use it -->
    <dependency>
      <groupId>org.eclipse.osgi-technology.rest</groupId>
      <artifactId>org.eclipse.osgitech.rest.config</artifactId>
      <version>${jakartars.whiteboard.version}</version>
      <scope>runtime</scope>
    </dependency>
    <!-- An optional fragment for the use of server sent events -->
    <dependency>
      <groupId>org.eclipse.osgi-technology.rest</groupId>
      <artifactId>org.eclipse.osgitech.rest.sse</artifactId>
      <version>${jakartars.whiteboard.version}</version>
      <scope>runtime</scope>
    </dependency>
    <!-- The adapter to run the implementation with Jetty -->
    <dependency>
      <groupId>org.eclipse.osgi-technology.rest</groupId>
      <artifactId>org.eclipse.osgitech.rest.jetty</artifactId>
      <version>${jakartars.whiteboard.version}</version>
      <scope>runtime</scope>
    </dependency>
    <!-- The adapter to run the implementation with the OSGi Servlet Whiteboard -->
    <dependency>
      <groupId>org.eclipse.osgi-technology.rest</groupId>
      <artifactId>org.eclipse.osgitech.rest.servlet.whiteboard</artifactId>
      <version>${jakartars.whiteboard.version}</version>
      <scope>runtime</scope>
    </dependency>

    <!-- Jersey - explicitly added to be able to update the dependency that is provided by org.eclipse.osgi-technology.rest -->
    <dependency>
      <groupId>org.glassfish.jersey</groupId>
      <artifactId>jersey-bom</artifactId>
      <version>${jersey.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <!-- Condition Service -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.service.condition</artifactId>
      <version>1.0.0</version>
    </dependency>

    <!-- Tracker -->
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.util.tracker</artifactId>
      <version>1.5.4</version>
    </dependency>

    <!--  Jetty  -->
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-bom</artifactId>
      <version>11.0.20</version>
      <type>pom</type>
    </dependency>

    <!-- 
      org.apache.felix.http.jetty:
      - implementation of the R8.1 OSGi Servlet Service, the R7 OSGi Http Service and the R7 OSGi Http Whiteboard Specification 
      - has itself the dependencies to Eclipse Jetty, which makes those bundles transitively available in our setup
    -->
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.http.jetty</artifactId>
      <version>5.1.8</version>
      <scope>runtime</scope>
    </dependency>

    <!--  Http Servlet 3.1 API with contract  -->
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.http.servlet-api</artifactId>
      <version>2.1.0</version>
      <!-- <version>3.0.0</version> -->
      <scope>runtime</scope>
    </dependency>

    <!-- Java XML -->
    <dependency>
      <groupId>jakarta.xml.bind</groupId>
      <artifactId>jakarta.xml.bind-api</artifactId>
      <version>4.0.1</version>
    </dependency>
    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-osgi</artifactId>
      <version>4.0.4</version>
      <scope>runtime</scope>
    </dependency>

    <!-- JSON Support -->
    <dependency>
      <groupId>jakarta.json</groupId>
      <artifactId>jakarta.json-api</artifactId>
      <version>2.1.3</version>
    </dependency>
    <dependency>
      <groupId>jakarta.json.bind</groupId>
      <artifactId>jakarta.json.bind-api</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.16.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
      <artifactId>jackson-jakarta-rs-json-provider</artifactId>
      <version>2.16.0</version>
    </dependency>

    <dependency>
      <groupId>org.eclipse.parsson</groupId>
      <artifactId>jakarta.json</artifactId>
      <version>1.1.5</version>
    </dependency>

    <!-- extender that facilitates the use of JRE SPI providers -->
    <dependency>
      <groupId>org.apache.aries.spifly</groupId>
      <artifactId>org.apache.aries.spifly.dynamic.framework.extension</artifactId>
      <version>1.3.7</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.hk2</groupId>
      <artifactId>osgi-resource-locator</artifactId>
      <version>1.0.3</version>
      <scope>runtime</scope>
    </dependency>

    <!--  Several implementations need to log using SLF4J  -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.36</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.12</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>1.2.12</version>
      <scope>runtime</scope>
    </dependency>

    <!--  The Web Console  -->
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.webconsole</artifactId>
      <version>4.8.8</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.webconsole.plugins.ds</artifactId>
      <version>2.2.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.inventory</artifactId>
      <version>1.1.0</version>
      <scope>test</scope>
    </dependency>

    <!--  The Gogo Shell  -->
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.gogo.shell</artifactId>
      <version>1.1.4</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.gogo.runtime</artifactId>
      <version>1.1.6</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.gogo.command</artifactId>
      <version>1.1.2</version>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.osgi</groupId>
          <artifactId>org.osgi.core</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.osgi</groupId>
          <artifactId>org.osgi.compendium</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</dependencyManagement>
  • Add the build section similar to the following snippet
<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.11.0</version>
        <configuration>
          <release>${java.version}</release>
        </configuration>
      </plugin>

      <!-- Use the bnd-maven-plugin and assemble the symbolic names -->
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-maven-plugin</artifactId>
        <version>${bnd.version}</version>
        <configuration>
          <bnd>
            <![CDATA[
Bundle-SymbolicName: ${project.groupId}.${project.artifactId}
-sources: true
-contract: *
]]>
          </bnd>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>bnd-process</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- Required to make the maven-jar-plugin pick up the bnd 
                  generated manifest. Also avoid packaging empty Jars -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
          <archive>
            <manifestFile>
              ${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
          </archive>
          <skipIfEmpty>true</skipIfEmpty>
        </configuration>
      </plugin>
      <!-- Setup the indexer for running and testing -->
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-indexer-maven-plugin</artifactId>
        <version>${bnd.version}</version>
        <configuration>
          <localURLs>REQUIRED</localURLs>
          <attach>false</attach>
        </configuration>
        <executions>
          <execution>
            <id>index</id>
            <goals>
              <goal>index</goal>
            </goals>
            <configuration>
              <indexName>${project.artifactId}</indexName>
            </configuration>
          </execution>
          <execution>
            <id>test-index</id>
            <goals>
              <goal>index</goal>
            </goals>
            <configuration>
              <indexName>${project.artifactId} Test</indexName>
              <outputFile>${project.build.directory}/test-index.xml</outputFile>
              <scopes>
                <scope>test</scope>
              </scopes>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <!-- Define the version of the resolver plugin we use -->
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-resolver-maven-plugin</artifactId>
        <version>${bnd.version}</version>
        <configuration>
          <failOnChanges>false</failOnChanges>
          <bndruns></bndruns>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>resolve</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- Define the version of the export plugin we use -->
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-export-maven-plugin</artifactId>
        <version>${bnd.version}</version>
        <configuration>
          <resolve>true</resolve>
          <failOnChanges>false</failOnChanges>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>export</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- Define the version of the testing plugin that we use -->
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-testing-maven-plugin</artifactId>
        <version>${bnd.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>testing</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- Define the version of the baseline plugin we use and 
                  avoid failing when no baseline jar exists. (for example before the first 
                  release) -->
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-baseline-maven-plugin</artifactId>
        <version>${bnd.version}</version>
        <configuration>
          <failOnMissing>false</failOnMissing>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>baseline</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

Troubleshooting

  • If you face issues with regards to dependency resolution, a good first option to solve is to Right Click on jakartars - Maven - Update Project…, select all projects and click OK.
  • If you see the Conflicting lifecycle mapping… error, you probably have the bndtools.m2e connector as well as the m2e.pde.connector in your workspace enabled. To solve this open the pom.xml files with the error, set the cursor to the line with the error, press CTRL+1 (Quick Fix) and select Ignore M2E PDE Connector…

Service interface

  • In the Bndtools Explorer locate the api module
  • Open the pom.xml file of the api module and replace the dependencies section with the following one:
<dependencies>
  <dependency>
    <groupId>org.osgi</groupId>
    <artifactId>osgi.core</artifactId>
  </dependency>
  <dependency>
    <groupId>org.osgi</groupId>
    <artifactId>osgi.annotation</artifactId>
  </dependency>
</dependencies>
  • Add the following build section:
<build>
  <plugins>
    <plugin>
      <groupId>biz.aQute.bnd</groupId>
      <artifactId>bnd-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>biz.aQute.bnd</groupId>
      <artifactId>bnd-baseline-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
  • Expand to the package org.fipro.service.modifier.api
  • Implement the StringModifier interface:
public interface StringModifier {
    String modify(String input);
}
  • You can delete the App.java file which was created by the archetype.
  • Create the package-info.java file in the org.fipro.service.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.
@org.osgi.annotation.bundle.Export
@org.osgi.annotation.versioning.Version("1.0.0")
package org.fipro.service.modifier.api;
  • Delete the package under src/test/java

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 impl module.
  • Open the pom.xml file and add the dependency to the api module in the dependencies section.
<dependency>
  <groupId>org.fipro.service.modifier</groupId>
  <artifactId>api</artifactId>
  <version>${project.version}</version>
</dependency>
  • Also add the necessary OSGi dependencies:
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>osgi.core</artifactId>
</dependency>
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>osgi.annotation</artifactId>
</dependency>
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>org.osgi.service.component.annotations</artifactId>
</dependency>
  • Add the following build section:
<build>
  <plugins>
    <plugin>
      <groupId>biz.aQute.bnd</groupId>
      <artifactId>bnd-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
  • Expand to the package org.fipro.service.modifier.impl
  • 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 App 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 / Jakarta REST Web Resource

A Jakarta RESTful Web Services Resource can be registered with the Jakarta RESTful Web Services Whiteboard by registering them as Whiteboard services. In other words, the Jakarta REST Resource can simply be registered with the Jakarta REST Whiteboard if it is implemented as a OSGi 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 rest module.
  • Open the pom.xml file and add the dependency to the api module in the dependencies section.
<dependency>
  <groupId>org.fipro.service.modifier</groupId>
  <artifactId>api</artifactId>
  <version>${project.version}</version>
</dependency>
  • Add the necessary basic OSGi dependencies
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>osgi.core</artifactId>
</dependency>
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>osgi.annotation</artifactId>
</dependency>
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>org.osgi.service.component.annotations</artifactId>
</dependency>
  • Add the necessary Jakarta-RS dependencies
<dependency>
  <groupId>jakarta.ws.rs</groupId>
  <artifactId>jakarta.ws.rs-api</artifactId>
</dependency>
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>org.osgi.service.jakartars</artifactId>
</dependency>
  • Add the following build section:
<build>
  <plugins>
    <plugin>
      <groupId>biz.aQute.bnd</groupId>
      <artifactId>bnd-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
  • Expand to the package org.fipro.service.modifier.rest
  • Implement the ModifierRestService:
    • 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 @JakartarsResource annotation to the class definition to mark it as a Jakarta-RS whiteboard resource.
      This will add the service property osgi.jakartars.resource=true which means this service must be processed by the Jakarta RS whiteboard. @JakartarsResource itself has the @RequireJakartarsWhiteboard annotation which adds the requirement for a Jakarta RESTful Web Services Whiteboard implementation. Therefore it is not needed to use the @RequireJakartarsWhiteboard annotation on your REST service implementation.
    • Optional: Add the @JakartarsName annotation.
      This will add the service property osgi.jakartars.name, which defines a user defined name that can be used to identify a Jakarta RESTful Web Services whiteboard service.
    • Add the Jakarta-RS Path annotation on class level to specify the URI path that the resource class will serve requests for.
    • Get a StringModifier injected using the @Reference annotation.
    • Implement a Jakarta-RS resource method that uses the StringModifier.
@JakartarsResource
@JakartarsName("modifier")
@Component(service=ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
public class ModifierRestService {

    @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 Jakarta-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 Jakarta-RS resource methods, you can avoid the PROTOTYPE scope. If you have a stateful implementation, that for example gets Jakarta-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 Jakarta-RS design, as Jakarta-RS natively expects to re-create resources on every request.

Prepare the application project

There are currently two adapters you can choose from to run the OSGi Technology Whiteboard Implementation for Jakarta RESTful Web Services:

  • Jetty
  • OSGi Servlet Whiteboard (e.g. with Jetty)

Deployment on Jetty

The following section describes how to run directly on a Jetty server.

In the application project we need to ensure that our service is available. In case the StringInverter from above was implemented, the impl 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
  • Add all dependencies defined in the dependencyManagement section of the parent pom.xml. Remember to remove the version, as it is defined in the parent pom.xml
  • Add the dependency to the impl and the rest module in the dependencies section.
<dependency>
  <groupId>org.fipro.service.modifier</groupId>
  <artifactId>impl</artifactId>
  <version>${project.version}</version>
</dependency>
<dependency>
  <groupId>org.fipro.service.modifier</groupId>
  <artifactId>rest</artifactId>
  <version>${project.version}</version>
</dependency>
  • Check that only the relevant dependencies for the Jetty deployment are included
<!-- The whiteboard implementation -->
<dependency>
  <groupId>org.eclipse.osgi-technology.rest</groupId>
  <artifactId>org.eclipse.osgitech.rest</artifactId>
</dependency>
<!-- The whiteboard implementation default configuration, when you want to use it -->
<dependency>
  <groupId>org.eclipse.osgi-technology.rest</groupId>
  <artifactId>org.eclipse.osgitech.rest.config</artifactId>
</dependency>
<!-- An optional fragment for the use of server sent events -->
<dependency>
  <groupId>org.eclipse.osgi-technology.rest</groupId>
  <artifactId>org.eclipse.osgitech.rest.sse</artifactId>
</dependency>
<!-- The adapter to run the implementation with Jetty -->
<dependency>
  <groupId>org.eclipse.osgi-technology.rest</groupId>
  <artifactId>org.eclipse.osgitech.rest.jetty</artifactId>
</dependency>
  • Add the dependency to slf4j-simple to at least see the log statements on the console
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.36</version>
</dependency>
  • Add the following build section:
  <build>
    <plugins>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-indexer-maven-plugin</artifactId>
        <configuration>
          <includeJar>true</includeJar>
        </configuration>
      </plugin>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-export-maven-plugin</artifactId>
        <configuration>
          <bndruns>
            <bndrun>app.bndrun</bndrun>
          </bndruns>
        </configuration>
      </plugin>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-resolver-maven-plugin</artifactId>
        <configuration>
          <bndruns>
            <bndrun>app.bndrun</bndrun>
          </bndruns>
        </configuration>
      </plugin>
    </plugins>
  </build>
  • Remove the junit dependency from the pom.xml
  • Remove src/test/java from the build path via Right Click -> Build Path -> Remove from Build Path
  • Delete the folder src/test/java
  • Delete the App and the package under src/main/java
  • Create a folder src/main/resources/OSGI-INF/configurator
  • Create a configurator.json file in that folder with the following content:
{
  ":configurator:resource-version": 1,
  "JakartarsWhiteboardComponent": {
    "jersey.port": 8080,
    "jersey.jakartars.whiteboard.name" : "Jetty REST",
    "jersey.context.path" : ""
  }
}

The following properties are supported for configuring the Whiteboard on Jersey:

Parameter Description Default
jersey.schema The schema under which the services should be available. http
jersey.host The host under which the services should be available. localhost
jersey.port The port under which the services should be available. 8181
jersey.context.path The base context path of the whiteboard. /rest
jersey.jakartars.whiteboard.name The name of the whiteboard Jersey REST
jersey.disable.sessions Enable/disable session handling in Jetty.
Disabled by default as REST services are stateless.
true

The definition of these properties is located in JerseyConstants.

Note:
The default value for jersey.context.path is /rest. So if you don’t configure a value via the configurator.json file, your services will be available via the rest context path. This is also the case for a custom Jakarta-RS application. If you don’t want to use a context path, you explicitly have to set it to an empty value, as in the example above.

  • Create the folder config in src/main/java
  • Create a package-info.java in the folder src/main/java/config with the following content:
@RequireConfigurator
@RequireConfigurationAdmin
package config;

import org.osgi.service.configurator.annotations.RequireConfigurator;
import org.osgi.service.cm.annotations.RequireConfigurationAdmin;

By using these annotations you declare that the Configurator extender and a Configuration Admin implementation are required. Further information about the Configurator can be found in the OSGi Compendium Configurator Specification.

  • Create the file app/app.bndrun
  • Open app/app.bndrun
  • Switch to the Source tab and add the following content:
index: target/index.xml;name="app"

-standalone: ${index}

-runrequires: \
	bnd.identity;id='org.fipro.service.modifier.rest',\
	bnd.identity;id='org.fipro.service.modifier.app',\
	bnd.identity;id='org.eclipse.parsson.jakarta.json',\
	bnd.identity;id='slf4j.simple'
	
-runfw: org.eclipse.osgi
-runee: JavaSE-17
-resolve.effective: active

-runblacklist: bnd.identity;id='org.apache.felix.http.jetty'

Note:
We add the bundle org.apache.felix.http.jetty to the Run Blacklist to avoid that this bundle is used in the resolve process. This is necessary as we explicitly want to use the default Jetty bundles instead of the repackaged Felix Jetty bundle.

  • Switch back to the Run tab and click on Resolve
  • Double check that the modules api, app, impl and rest are part of the Run Bundles

Note:
If the Run Bundles stay empty, or you see the bundles and shortly afterwards they are gone again, try to set the Resolution to Auto and save the file. This should then solve the issue afterwards.

Note:
Eclipse Parsson provides an implementation of Jakarta JSON Processing Specification. It is required by the Jakarta RESTful Web Services implementation if you configure it via the OSGi Compendium Configurator Specification, but unfortunately there is no direct requirement to an implementation. Therefore it is not resolved automatically and needs to be specified as Run Requirement explicitly.

Note:
If you see the following warning and want to get rid of it, you need to add com.sun.xml.bind.jaxb-osgi to the Run Requirements and Resolve again.

JAXBContext implementation could not be found. WADL feature is disabled.

Deployment on OSGi Servlet Whiteboard

The following section describes how to run using the OSGi Servlet Whiteboard.

If you want to try out both variants, I suggest to create a new module app-http. This will be helpful later on to test and compare the differences.

  • Switch to the jakartars folder on a console
  • Create the child module for the additional application
mvn archetype:generate \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DgroupId=org.fipro.service.modifier \
-DartifactId=app-http \
-Dversion=1.0.0-SNAPSHOT \
-Dpackage=org.fipro.service.modifier.app-http \
-DinteractiveMode=false
  • Import the newly created project via File - Import… - Maven - Existing Maven Projects

In the application project we need to ensure that our service is available. In case the StringInverter from above was implemented, the impl module needs to be added to the dependencies section of the application 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-http module.
  • Open the pom.xml file
  • Add all dependencies defined in the dependencyManagement section of the parent pom.xml. Remember to remove the version, as it is defined in the parent pom.xml
  • Add the dependency to the impl and the rest module in the dependencies section.
<dependency>
  <groupId>org.fipro.service.modifier</groupId>
  <artifactId>impl</artifactId>
  <version>${project.version}</version>
</dependency>
<dependency>
  <groupId>org.fipro.service.modifier</groupId>
  <artifactId>rest</artifactId>
  <version>${project.version}</version>
</dependency>
  • Check that only the relevant dependencies for the OSGi Servlet Whiteboard deployment are included
<!-- The whiteboard implementation -->
<dependency>
  <groupId>org.eclipse.osgi-technology.rest</groupId>
  <artifactId>org.eclipse.osgitech.rest</artifactId>
</dependency>
<!-- The adapter to run the implementation with the OSGi Servlet Whiteboard -->
<dependency>
  <groupId>org.eclipse.osgi-technology.rest</groupId>
  <artifactId>org.eclipse.osgitech.rest.servlet.whiteboard</artifactId>
</dependency>
  • Add the dependency to slf4j-simple to at least see the log statements on the console
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.36</version>
</dependency>
  • Add the following build section:
  <build>
    <plugins>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-indexer-maven-plugin</artifactId>
        <configuration>
          <includeJar>true</includeJar>
        </configuration>
      </plugin>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-export-maven-plugin</artifactId>
        <configuration>
          <bndruns>
            <bndrun>app.bndrun</bndrun>
          </bndruns>
        </configuration>
      </plugin>
      <plugin>
        <groupId>biz.aQute.bnd</groupId>
        <artifactId>bnd-resolver-maven-plugin</artifactId>
        <configuration>
          <bndruns>
            <bndrun>app.bndrun</bndrun>
          </bndruns>
        </configuration>
      </plugin>
    </plugins>
  </build>
  • Remove the junit dependency from the pom.xml
  • Remove src/test/java from the build path via Right Click -> Build Path -> Remove from Build Path
  • Delete the folder src/test/java
  • Delete the App and the package under src/main/java
  • Create a folder src/main/resources/OSGI-INF/configurator
  • Create a configurator.json file in that folder with the following content:
{
  ":configurator:resource-version": 1,
  
  "org.apache.felix.http~modifier":
  {
    "org.osgi.service.http.port": "8080",
    "org.osgi.service.http.host": "localhost",
    "org.apache.felix.http.context_path": "",
    "org.apache.felix.http.name": "Modify REST Service",
    "org.apache.felix.http.runtime.init.id": "modify"
  },
  "JakartarsServletWhiteboardRuntimeComponent~modifier":
  {
    "jersey.context.path" : "",
    "jersey.jakartars.whiteboard.name" : "Servlet REST",
    "osgi.http.whiteboard.target" : "(id=modify)"
  }
}

The first block org.apache.felix.http~modifier is used to configure the Apache Felix HTTP Service service factory. Details about the configuration options are available in the Apache Felix HTTP Service Wiki.

The second block JakartarsServletWhiteboardRuntimeComponent~modifier is used to configure the whiteboard service factory with the Servlet Whiteboard. The following properties are supported for configuring the Whiteboard on Servlet Whiteboard:

Parameter Description Default
jersey.context.path The base context path of the whiteboard. /
jersey.jakartars.whiteboard.name The name of the whiteboard Jersey REST
osgi.http.whiteboard.target Service property specifying the target filter to select the Http Whiteboard implementation to process the service.
The value is an LDAP style filter that points to the id defined in org.apache.felix.http.runtime.init.id.
-

The definition of these properties is located in JerseyConstants.

  • Create the folder config in src/main/java
  • Create a package-info.java in the folder src/main/java/config with the following content:
@RequireConfigurator
@RequireConfigurationAdmin
package config;

import org.osgi.service.configurator.annotations.RequireConfigurator;
import org.osgi.service.cm.annotations.RequireConfigurationAdmin;

By using these annotations you declare that the Configurator extender and a Configuration Admin implementation are required. Further information about the Configurator can be found in the OSGi Compendium Configurator Specification.

  • Create the file app-http/app.bndrun
  • Open app-http/app.bndrun
  • Switch to the Source tab and add the following content:
index: target/index.xml;name="app-http"

-standalone: ${index}

-runrequires: \
	bnd.identity;id='org.fipro.service.modifier.rest',\
	bnd.identity;id='org.fipro.service.modifier.app-http',\
	bnd.identity;id='org.eclipse.parsson.jakarta.json',\
	bnd.identity;id='slf4j.simple',\
	bnd.identity;id='org.apache.felix.http.jetty'

-runfw: org.eclipse.osgi
-runee: JavaSE-17
-resolve.effective: active

# Avoid to have the default Jetty run at port 8080  
-runproperties: \
    org.osgi.service.http.port=-1
  • Deactivate the HTTP Service under port 8080. This is necessary because the Felix Jetty implementation runs the OSGi HTTP Service by default at port 8080.
-runproperties: \
    org.osgi.service.http.port=-1
  • Switch back to the Run tab
  • Click on Resolve and double check that the modules api, app-http, impl and rest are part of the Run Bundles

Note:
If the Run Bundles stay empty, or you see the bundles and shortly afterwards they are gone again, try to set the Resolution to Auto and save the file. This should then solve the issue afterwards.

Note:
Eclipse Parsson provides an implementation of Jakarta JSON Processing Specification. It is required by the Jakarta RESTful Web Services implementation if you configure it via the OSGi Compendium Configurator Specification, but unfortunately there is no direct requirement to an implementation. Therefore it is not resolved automatically and needs to be specified as Run Requirement explicitly.

Note:
Compared to the Jetty usage, the default value for jersey.context.path with the Servlet Whiteboard is /. So if you don’t want to use a context path, you can simply do not configure a value via the configurator.json file.

If you specify org.apache.felix.http.context_path and jersey.context.path, the path to the service is combined, e.g.

"org.apache.felix.http.context_path": "http"
...
"jersey.context.path" : "demo"

Would result in the path http://localhost:8080/http/demo/modify/fubar

It is also possible to register the Jakarta-RS Whiteboard Service with the default Jetty. In this case the configuration is much simpler:

{
  ":configurator:resource-version": 1,
  
  "JakartarsServletWhiteboardRuntimeComponent":
  {
    "jersey.jakartars.whiteboard.name" : "Servlet REST",
    "jersey.context.path" : "rest"
  }
}

And of course you need to remove org.osgi.service.http.port=-1 from the runproperties, otherwise the default Jetty instance doesn’t start. It is important that you provide a configuration, either via Configurator or even manually via ConfigurationAdmin, as the JakartarsServletWhiteboardRuntimeComponent requires a configuration.

The following snippet shows how you could provide a configuration programmatically via Immediate Component:

@Component
public class JakartaRsConfiguration {

  @Reference
  ConfigurationAdmin admin;
  
  @Activate
  void activate() throws IOException {
    Dictionary<String, String> properties = new Hashtable<>();
    properties.put("jersey.jakartars.whiteboard.name", "Servlet REST");
    properties.put("jersey.context.path", "rest");
 
    Configuration config = 
        admin.getConfiguration("JakartarsServletWhiteboardRuntimeComponent", "?");
    config.update(properties);
  }
}

Note:
If you see the following warning and want to get rid of it

JAXBContext implementation could not be found. WADL feature is disabled.

you need to add com.sun.xml.bind.jaxb-osgi to the Run Requirements and Resolve again.

Jakarta RESTful Web Services Extensions

In Jakarta RESTful Web Services you can add Providers that are responsible for various cross-cutting concerns such as filtering requests, converting representations into Java objects, mapping exceptions to responses, etc. Such Jakarta RESTful Web Services Extensions can be registered with the Jakarta RESTful Web Services Whiteboard by registering them as Whiteboard services. This is explained in more detail in the OSGi Compendium Specification Jakarta RESTful Web Services Whiteboard.

The following interfaces are supported by the specification:

  • ContainerRequestFilter and ContainerResponseFilter extensions are used to alter the HTTP request and response parameters.
  • ReaderInterceptor and WriterInterceptor extensions are used to alter the incoming or outgoing objects for the call.
  • MessageBodyReader and MessageBodyWriter extensions are used to deserialize/serialize objects to the wire for a given media type, for example application/json.
  • ContextResolver extensions are used to provide objects for injection into other Jakarta RESTful Web Services resources and extensions.
  • ExceptionMapper extensions are used to map exceptions thrown by Jakarta RESTful Web Services resources into responses.
  • ParamConverterProvider extensions are used to map rich parameter types to and from String values.
  • Feature and DynamicFeature extensions are used as a way to register multiple extension types with the Jakarta RESTful Web Services container. Dynamic Features further allow the extensions to be targeted to specific resources within the Jakarta RESTful Web Services container.

For a Jakarta-RS Extension Whiteboard Service, there are basically two important annotations:

  • @JakartarsExtension
    Mark the service as a Jakarta-RS Whiteboard Extension type that should be processed by the Jakarta-RS Whiteboard.
  • @JakartarsExtensionSelect
    Express dependencies on one or more extension services. Typically used for extension ordering, as extensions are by default applied to every request and response.

As an example we will implement a WriterInterceptor. This tutorial contains further examples in the following chapters.

  • Implement a HtmlWriterInterceptor class in the rest module
    • Add the @Component annotation to the class definition to specify it as a service.
    • Add the @JakartarsExtension annotation to the class definition to mark the service as a Jakarta-RS Whiteboard Extension type that should be processed by the Jakarta-RS Whiteboard.
    • Implement the WriterInterceptor interface and wrap the String in the entity reference with HTML tags.
@Component
@JakartarsExtension
public class HtmlWriterInterceptor implements WriterInterceptor {

	public void aroundWriteTo(WriterInterceptorContext ctx) 
    		throws WebApplicationException, IOException {
        
    	Object entity = ctx.getEntity();
        
        if (entity instanceof String result) {
        	String html = "<html><head></head><body><ul>";
        	String[] split = result.split(";");
        	for (String string : split) {
        		html += "<li>" + string + "</li>";
			}
        	html += "</ul></body>";
        	ctx.setEntity(html);
        }
        ctx.proceed();
    }
}

Note:
We use the list markup processing already as a preparation for later steps.

By default Jakarta-RS Extensions are applied to every request and response. In cases where this should be not the case for every Jakarta-RS Resource, but only a subset, it is possible to limit the usage via name binding. Let’s evaluate this with the following modifications:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@NameBinding
public @interface HtmlModification{}
  • Update the HtmlWriterInterceptor to add the name binding annotation to the class definition
@Component
@JakartarsExtension
@HtmlModification
public class HtmlWriterInterceptor implements WriterInterceptor { ... }
  • Update the ModifierRestService and add a new resource method that uses the name binding annotation and explicitly returns the media type text/html
@JakartarsResource
@JakartarsName("modifier")
@Component(service=ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
public class ModifierRestService {

    @Reference
    StringModifier modifier;

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

    @GET
    @Path("modifyhtml/{input}")
    @Produces(MediaType.TEXT_HTML)
    @HtmlModification
    public String modifyHtml(@PathParam("input") String input) {
        return modifier.modify(input);
    }
}

Jakarta RESTful Web Services Application

The Jakarta RESTful Web Services Whiteboard registers a default Jakarta REST Web Service Application with the name .default. Typically it is sufficient to register Jakarta-RS Resources and Jakarta-RS Extensions as Whiteboard Services and implicitly use the default application. There are two use cases where it makes sense to register a Jakarta-RS Application as Whiteboard Service:

  • To support the use of legacy Jakarta RESTful Web Services applications
  • To provide simple scoping of Jakarta RESTful Web Services resources and extensions within a whiteboard

For a Jakarta-RS Application Whiteboard Service, there are basically two important annotations:

  • @JakartarsApplicationBase
    Mark the service as a Jakarta-RS Whiteboard Application type that should be processed by the Jakarta-RS Whiteboard. Also defines the URI, relative to the root context of the whiteboard, at which the Application should be registered.
  • @JakartarsApplicationSelect
    Select the Jakarta RESTful Web Services Application with which this Whiteboard service should be associated.
@JakartarsApplicationBase("mod")
@JakartarsName("modifyApplication")
@Component(service=Application.class)
public class ModifyApplication extends Application { ... }
@Component(service=ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@JakartarsResource
@JakartarsApplicationSelect("(osgi.jakartars.name=modifyApplication)")
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class ModifierRestService { ... }

If you need to use the @JakartarsApplicationSelect annotation on multiple Jakarta-RS Resources and Jakarta-RS Extensions, it is helpful to define a Custom Component Property Annotation.

@ComponentPropertyType
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface TargetModifyApp {
    String osgi_jakartars_application_select() default "(osgi.jakartars.name=modifyApplication)";
}

You can then use the @TargetModifyApp annotation instead:

@JakartarsResource
@Component(service=ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@TargetModifyApp
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class ModifierRestService { ... }

Note:
By default Jakarta-RS Resource Whiteboard Services and Jakarta-RS Extension Whiteboard Services are registered with the .default Jakarta-RS Application provided by the Whiteboard implementation. They are not automatically assigned to all published Applications. This means, if you have a custom Application in your runtime and want to add Resources and Extensions to that Application, you need to target them via @JakartarsApplicationSelect.

An example to follow is shown later in this tutorial, to have a better idea on how it could look like.

Further information is available in the OSGi Compendium Spec Registering RESTful Web Service Applications.

JSON Support

There are use cases where returning a plain String as result of a web service is not sufficient. In the following section we extend our setup to return the result as JSON. We will use Jackson for this.

Update the ModifierRestService

First we configure the Jakarta-RS Resource so it produces JSON.

  • Add the Jakarta-RS @Produces(MediaType.APPLICATION_JSON) annotation to the ModifierRestService class definition to specify that JSON responses are created.
  • Optional:
    Get multiple StringModifier injected and return a List of Strings as a result of the REST resource.
@JakartarsResource
@JakartarsName("modifier")
@Component(service=ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class ModifierRestService {

  @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());
  }
    
  @GET
  @Path("modifyhtml/{input}")
  @Produces(MediaType.TEXT_HTML)
  @HtmlModification
  public String modifyHtml(@PathParam("input") String input) {
    return modifier.stream()
      .map(mod -> mod.modify(input))
      .collect(Collectors.joining(";"));
  }
}

Note:
If you change the return value to List without further configuration, you will see an error like this:

MessageBodyWriter not found for media type=text/html, type=class java.util.ArrayList, genericType=java.util.List<java.lang.String>
  • Optional:
    Implement an additional StringModifier in the impl module.
@Component
public class Upper implements StringModifier {

  @Override
  public String modify(String input) {
    return input.toUpperCase();
  }
}

Use the Jersey Jackson JSON Provider

Jersey provides support for common media type representations, e.g. Jersey - JSON - Jackson (2.x). By adding the bundle org.glassfish.jersey.media.jersey-media-json-jackson to the runtime, the necessary providers are automatically registered to all Jakarta-RS Applications in the runtime.

  • Open the rest/pom.xml
  • Add the following dependency in the dependencies section
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
  • Open the app/app.bndrun
  • If you changed the implementation of the ModifierRestService to consume a collection of StringModifier, you need to add the bundle org.fipro.service.modifier.impl explicitly to the Run Requirements
  • Add the org.glassfish.jersey.media.jersey-media-json-jackson bundle to the Run Requirements
-runrequires: \
	bnd.identity;id='org.fipro.service.modifier.impl',\
	bnd.identity;id='org.fipro.service.modifier.rest',\
	bnd.identity;id='org.fipro.service.modifier.app',\
	bnd.identity;id='org.eclipse.parsson.jakarta.json',\
	bnd.identity;id='slf4j.simple',\
	bnd.identity;id='com.sun.xml.bind.jaxb-osgi',\
	bnd.identity;id='org.glassfish.jersey.media.jersey-media-json-jackson'
  • Click on Resolve to ensure that the Jackson libraries and the impl bundle 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.

If you have also created the app-http module, perform the above modifications also in the app-http/app.bndrun

-runrequires: \
    bnd.identity;id='org.fipro.service.modifier.impl',\
    bnd.identity;id='org.fipro.service.modifier.rest',\
    bnd.identity;id='org.fipro.service.modifier.app-http',\
    bnd.identity;id='org.eclipse.parsson.jakarta.json',\
    bnd.identity;id='slf4j.simple',\
    bnd.identity;id='org.apache.felix.http.jetty',\
    bnd.identity;id='com.sun.xml.bind.jaxb-osgi',\
    bnd.identity;id='org.glassfish.jersey.media.jersey-media-json-jackson'

Note:
If the execution of Resolve does not take the new changes into account, you need to execute a Maven build mvn clean verify, update the projects via Right Click -> Maven -> Update Project…, and then trigger Resolve from the Bnd Run File Editor again.

Note:
The org.glassfish.jersey.jackson.JacksonFeature is automatically registered with all applications in the server. This way the OSGi requirement on the JSON media type via osgi.jakartars.media.type=application/json service property is not satisfied. If you want to use the org.glassfish.jersey.jackson.JacksonFeature and use the OSGi capability mechanism, you could register it via Jakarta-RS Feature Whiteboard Extension (see below).

Use the JacksonJsonProvider via Jakarta-RS Feature Whiteboard Extension

A Jakarta RESTful Web Service Feature is a special type of Jakarta RESTful Web Service Provider, that implements the Feature interface and can be used to configure a Jakarta-RS implementation. They are useful for grouping sets of properties and providers (including other features) that are logically related and must be enabled as a unit (see Configurable Types).

  • Open the rest/pom.xml
  • Add the following dependency in the dependencies section
<dependency>
  <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
  <artifactId>jackson-jakarta-rs-json-provider</artifactId>
</dependency>
  • Implement the JacksonJsonFeature:
    • Add the @Component annotation to the class definition.
    • Add the @JakartarsExtension annotation to the class definition to mark the service as a Jakarta-RS Whiteboard Extension type that should be processed by the Jakarta-RS Whiteboard.
    • Add the @JakartarsMediaType(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.
    • Register the com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider in the configure(FeatureContext) method.
@Component
@JakartarsExtension
@JakartarsMediaType(MediaType.APPLICATION_JSON)
public class JacksonJsonFeature implements Feature {

  @Override
  public boolean configure(FeatureContext context) {
    context.register(JacksonJsonProvider.class);
    return true;
  }
}
  • Open the file app/app.bndrun
  • Remove org.glassfish.jersey.media.jersey-media-json-jackson from the Run Requirements
  • Click Resolve and verify that the bundle is not part of the Run Bundles anymore

As our Feature provides the capability to support the media type JSON via the @JakartarsMediaType(APPLICATION_JSON), we can configure our service to require that capability via the @JSONRequired annotation.

  • Add the @JSONRequired annotation to the ModifierRestService class definition to mark this class to require JSON media type support. In OSGi terms it means that a service is available that provides the service property osgi.jakartars.media.type=application/json, which we provided in our custom entity provider via @JakartarsMediaType(MediaType.APPLICATION_JSON). This way our Jakarta REST Resource will only be available if the media type support service is available in the runtime.
@JakartarsResource
@JakartarsName("modifier")
@Component(service = ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@JSONRequired
public class ModifierRestService { ... }

Alternative: Custom Entity Provider

In this section we will implement a Custom Entity Provider and use Jackson for this. We will first register it directly as a Jakarta-RS Whiteboard Extension.

  • In the Bndtools Explorer locate the rest module.
  • Open the pom.xml file and add the dependency to Jackson and to the OSGi Converter in the dependencies section.
<dependency>
  <groupId>org.osgi</groupId>
  <artifactId>org.osgi.util.converter</artifactId>
  <scope>compile</scope>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

Note:
Remember to remove org.glassfish.jersey.media.jersey-media-json-jackson from the Run Requirements in the app/app.bndrun and Resolve in case you haven’t done so already in a previous section.

  • 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 @JakartarsExtension annotation to the class definition to mark the service as a Jakarta-RS Whiteboard Extension type that should be processed by the Jakarta-RS Whiteboard.
    • Add the @JakartarsMediaType(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.
    • Add the Jakarta-RS @Consumes(MediaType.WILDCARD) annotation, to define the media types the jakarta.ws.rs.ext.MessageBodyReader can accept. In this case */* to also support “non-standard” JSON variants as input.
    • Add the Jakarta-RS @Produces(MediaType.APPLICATION_JSON) annotation, to define the media type the jakarta.ws.rs.ext.MessageBodyWriter can produce. In this case application/json.
    • Add the Jakarta-RS @Provider annotation, to support automatic discovery of the provider class by the Jakarta-RS runtime.
    • Internally make use of the OSGi Converter Specification for the implementation.
package org.fipro.service.modifier.rest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.jakartars.whiteboard.propertytypes.JakartarsExtension;
import org.osgi.service.jakartars.whiteboard.propertytypes.JakartarsMediaType;
import org.osgi.service.log.Logger;
import org.osgi.service.log.LoggerFactory;
import org.osgi.util.converter.Converter;
import org.osgi.util.converter.ConverterFunction;
import org.osgi.util.converter.Converters;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.Provider;

@JakartarsExtension
@JakartarsMediaType(MediaType.APPLICATION_JSON)
@Component(scope = ServiceScope.PROTOTYPE)
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Provider
public class JacksonJsonConverter implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

  @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 ConverterFunction.CANNOT_HANDLE;
  }

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

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

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

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

  @Override
  public void writeTo(
      Object o, Class<?> type, Type genericType, 
      Annotation[] annotations, MediaType mediaType,
      MultivaluedMap<String, Object> httpHeaders, OutputStream out) 
          throws IOException, WebApplicationException {

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

  @Override
  public Object readFrom(
      Class<Object> type, Type genericType, 
      Annotation[] annotations, MediaType mediaType,
      MultivaluedMap<String, String> httpHeaders, InputStream in) 
          throws IOException, WebApplicationException {

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

Register Custom Entity Provider via Jakarta-RS Whiteboard Extension Feature

In the previous section we created a Custom Entity Provider to return the media type JSON as service response. And we registered it via the OSGi Jakarta RESTful Web Service Extension mechanism. Basically this means, the Custom Entity Provider needs to be an OSGi service itself. But what about cases where the Custom Entity Provider already exists and is maintained by a project that is not OSGi aware?

How can you make use of Jakarta Extensions that are not whiteboard enabled?

The easiest approach is to create a Jakarta Feature as a Jakarta RESTful Web Service Extension. Similar to what we have done to register the JacksonJsonProvider (see above).

To show how this works, modify the JacksonJsonConverter so it is no whiteboard service anymore:

  • Remove the OSGi annotations from the class definition
  • Change the logger from org.osgi.service.log.Logger to org.slf4j.Logger
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Provider
public class JacksonJsonConverter implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

  private Logger logger = LoggerFactory.getLogger(JacksonJsonConverter.class);

  ...
} 

You need to add the slf4j-api to the dependencies of the rest/pom.xml to get rid of the compile errors:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <scope>compile</scope>
</dependency>
  • Implement/Update the JacksonJsonFeature:
    • Add the @Component annotation to the class definition.
    • Add the @JakartarsExtension annotation to the class definition to mark the service as a Jakarta-RS Whiteboard Extension type that should be processed by the Jakarta-RS Whiteboard.
    • Add the @JakartarsMediaType(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.
    • Register the JacksonJsonConverter in the configure(FeatureContext) method.
@Component
@JakartarsExtension
@JakartarsMediaType(MediaType.APPLICATION_JSON)
public class JacksonJsonFeature implements Feature {

	@Override
	public boolean configure(FeatureContext context) {
		context.register(JacksonJsonConverter.class);
		return true;
	}
}

If you start the application again with the JacksonJsonFeature, the service should work again as expected.

Register Custom Entity Provider via Jakarta-RS Application Whiteboard Service

In case you want to have a more static definition of the Jakarta-RS Resources and Jakarta-RS Extensions, and for example you also want to add Extensions that are not whiteboard enabled, you can also use a custom Jakarta-RS Application and register it as a Whiteboard Service.

  • Implement the ModifyApplication:
    • Add the @Component annotation to the class definition to mark it as an OSGi DS.
    • Add the @JakartarsApplicationBase annotation to the class definition to mark the service as a Jakarta-RS Whiteboard Application type that should be processed by the Jakarta-RS Whiteboard. Also defines the URI, relative to the root context of the whiteboard, at which the Application should be registered.
    • Add the @JakartarsName annotation to the class definition to specify a user defined name that can be used to identify a Jakarta RESTful Web Services whiteboard service.
    • Return the JacksonJsonConverter in the getClasses() method.
@JakartarsApplicationBase("mod")
@JakartarsName("modifyApplication")
@Component(service=Application.class)
public class ModifyApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(JacksonJsonConverter.class);
    }
}
  • Update the ModifierRestService
    • Add the @JakartarsApplicationSelect annotation to select the Jakarta RESTful Web Services Application with which this Whiteboard service should be associated.
    • Remove the @JSONRequired annotation, as the converter does not provide the necessary capability.
@JakartarsResource
@JakartarsName("modifier")
@Component(service = ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@JakartarsApplicationSelect("(osgi.jakartars.name=modifyApplication)")
public class ModifierRestService { ... }

After this change you will notice that the REST resource is not available anymore with the default application. This is because we selected the modifyApplication as the application where the resource should be available. To register the resource with the default application and the modifyApplication, you can either configure to select all Applications in the whiteboard

@JakartarsApplicationSelect("(osgi.jakartars.name=*)")

or provide a LDAP filter that selects the two explicitly

@JakartarsApplicationSelect("(|(osgi.jakartars.name=.default)(osgi.jakartars.name=modifyApplication))")

If you need to use the @JakartarsApplicationSelect annotation on multiple Jakarta-RS Resources and Jakarta-RS Extensions, it is helpful to define a Custom Component Property Annotation.

@ComponentPropertyType
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface TargetModifyApp {
    String osgi_jakartars_application_select() default "(osgi.jakartars.name=modifyApplication)";
}

You can then use the @TargetModifyApp instead in the ModifierRestService:

@JakartarsResource
@JakartarsName("modifier")
@Component(service = ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@TargetModifyApp
public class ModifierRestService { ... }

As you can see, you have multiple ways to register a Jakarta-RS Extension:

  • Register directly as whiteboard enabled OSGi Declarative Service
    Very simple in a plain OSGi environment, but requires OSGi dependencies in the Jakarta-RS Provider classes.
  • Register via Jakarta-RS Extension Feature Whiteboard Service
    Enables the usage of Jakarta-RS Extensions which are not whiteboard enabled. Good in cases where you might need to target multiple applications, or need a more dynamic approach.
  • Register via Jakarta-RS Application Whiteboard Service
    Enables the usage of Jakarta-RS Extensions which are not whiteboard enabled. Good if you have a fixed use case with a number of static extensions that must always be present.

Note:
Many thanks to Tim Ward who helped me in understanding the Jakarta RESTful Web Services Whiteboard and especially the Extension mechanisms better!

Control the JSON output format

With Jackson you can control the format of the JSON structure via an ObjectMapper. In case of a Custom Entity Provider like the one above, you are in full control of the ObjectMapper instance. To make this more dynamic you could also provide an ObjectMapper via dependency injection. For this need a Jakarta Extension ContextResolver for an ObjectMapper.

To make the effect visible, let’s first extend the ModifierRestService with a resource method that returns a more complex data structure:

@JakartarsResource
@JakartarsName("modifier")
@Component(service = ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@JakartarsApplicationSelect("(osgi.jakartars.name=*)")
public class ModifierRestService {

    @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());
  }
    
  @GET
  @Path("modifyhtml/{input}")
  @Produces(MediaType.TEXT_HTML)
  @HtmlModification
  public String modifyHtml(@PathParam("input") String input) {
    return modifier.stream()
        .map(mod -> mod.modify(input))
        .collect(Collectors.joining(";"));
   }

  @GET
  @Path("pretty/{input}")
  public Result pretty(@PathParam("input") String input) {
    List<String> result = modifier.stream()
        .map(mod -> mod.modify(input))
        .collect(Collectors.toList());

    return new Result(input, result);
  }
    
  public static record Result(String input, List<String> result) {};
}
{"input":"fubar","result":["FUBAR","rabuf"]}
  • Implement a ContextResolver for ObjectMapper in the rest module
    • Add the @Component annotation to the class definition to mark it as an OSGi DS.
    • Add the @JakartarsExtension annotation to the class definition to mark the service as a Jakarta-RS Whiteboard Extension type that should be processed by the Jakarta-RS Whiteboard.
    • Add the @Provider annotation to mark the implementation of an extension interface that should be discoverable by Jakarta-RS runtime during a provider scanning phase.
@JakartarsExtension
@Component
@Provider
public class CustomObjectMapperProvider implements ContextResolver<ObjectMapper> {

  private ObjectMapper mapper;

  public CustomObjectMapperProvider() {
    this.mapper = new ObjectMapper();
    this.mapper.enable(SerializationFeature.INDENT_OUTPUT);
  }

	public ObjectMapper getContext(Class<?> clazz) {
    return mapper;
  }
}

If the Custom Entity Provider JacksonJsonConverter is still in place in your setup, you need to modify it to get the ObjectMapper injected. This can be done by using the jakarta.ws.rs.ext.Providers:

@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Provider
public class JacksonJsonConverter implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

  private Logger logger = LoggerFactory.getLogger(JacksonJsonConverter.class);

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

  @Context
  private Providers providers;

  private ObjectMapper mapper;

  private ObjectMapper getObjectMapper() {
    if (this.mapper == null) {
	    if (providers != null) {
        this.mapper = providers
            .getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE)
            .getContext(ObjectMapper.class);
      } else {
        this.mapper = new ObjectMapper();
      }
    }

    return this.mapper;
  }

  private String toJson(Object value, Type targetType) {
    try {
      return getObjectMapper().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 getObjectMapper().readValue((String) o, List.class);
      }
      return getObjectMapper().readValue((String) o, String.class);
    } catch (IOException e) {
      logger.error("error on JSON parsing", e);
    }
    return ConverterFunction.CANNOT_HANDLE;
  }

  ...

}
{
  "input" : "fubar",
  "result" : [ "FUBAR", "rabuf" ]
}

If you also have the custom Application deployed, try to navigate to http://localhost:8080/mod/pretty/fubar. Here you will now see an error, because the ContextResolver by default is only registered with the .default application. This can be solved by either adding the @JakartaApplicationSelect annotation to the CustomObjectMapperProvider, or simply by adding the class to ModifyApplication#getClasses(). If you have a custom Application, this is probably the better fitting way of solving this.

@JakartarsApplicationBase("mod")
@JakartarsName("modifyApplication")
@Component(service=Application.class)
public class ModifyApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(
        		CustomObjectMapperProvider.class, 
        		JacksonJsonConverter.class);
    }
}

Now also http://localhost:8080/pretty/fubar should produce the correct output without an error.

As explained before with the Custom Entity Provider, you can register the Jakarta Extension as a Whiteboard Extension as above, or as plain Jakarta Extension via a Feature or an Application. This depends on the use case you want to solve. Also note that the CustomObjectMapperProvider registered as Whiteboard Extension Service, is also resolved by the com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider or the org.glassfish.jersey.media.jersey-media-json-jackson module. To verify this, change the JacksonJsonFeature back to return the com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider instead of your custom JacksonJsonConverter. Or even disable the JacksonJsonFeature and add the org.glassfish.jersey.media.jersey-media-json-jackson bundle back to the Run Requirements of the app module.

For simple use cases like the one in this tutorial, registering Jakarta Extensions as a Whiteboard Extension is the easiest approach. In more advanced setups, or if you need to consume Jakarta Extensions that are provided by non-OSGi environments, the usage of a Feature as Whiteboard Extension or a whiteboard enabled Jakarta Application is usually more efficient.

Multipart file upload

In the past I had to implement file processing services as part of the API. This means you upload a file, process it and download the result. This way you can for example migrate model files to a newer version, perform a static analysis of a model and even transform a model to some executable format and execute the result for simulation scenarios.

Using the Jakarta RESTful Web Service Specification and Jersey as implementation, this becomes quite easy. The multipart support is provided via a Jersey Module.

  • Open the app/pom.xml
  • Add the following dependency in the dependencies section
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-multipart</artifactId>
</dependency>

Note:
From Jersey 3.1.0 on, the MultiPartFeature is no longer required to be registered and it is registered automatically. So there is no need for an additional Jakarta-RS Feature or the registration via a Jakarta-RS Application. See Jersey Documentation - Multipart for further information.

  • Update the ModifierRestService and add a Resource method that supports a file upload
    • @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.
    • @FormParam("file")
      Specify on the method parameter to receive the form data as EntityPart, InputStream or String data-types, or a List<EntityPart>.
// get the EntityPart and the InputStream form parameter with name "file"
// received by a multipart/form-data POST request
@POST
@Path("modify/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)
public Response upload(
    @FormParam("file") EntityPart part,
    @FormParam("file") InputStream input) throws IOException {
  
  
    if (part != null
            && part.getFileName().isPresent()) {

        StringBuilder inputBuilder = new StringBuilder();
        try (InputStream is = input;
                BufferedReader br =
                    new BufferedReader(new InputStreamReader(is))) {

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

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

        String resultString = part.getFileName().get() + "\n\n";
        resultString += String.join("\n", modified);
        
        return Response.ok(resultString).build();
    }

    return Response.status(Status.PRECONDITION_FAILED).build();
}
  • Open the app/app.bndrun
  • Add the org.glassfish.jersey.media.jersey-media-multipart bundle to the Run Requirements
-runrequires: \
	bnd.identity;id='org.fipro.service.modifier.impl',\
	bnd.identity;id='org.fipro.service.modifier.rest',\
	bnd.identity;id='org.fipro.service.modifier.app',\
	bnd.identity;id='org.eclipse.parsson.jakarta.json',\
	bnd.identity;id='slf4j.simple',\
	bnd.identity;id='com.sun.xml.bind.jaxb-osgi',\
	bnd.identity;id='org.glassfish.jersey.media.jersey-media-multipart'
  • Click on Resolve to ensure that the Jackson libraries are part of the Run Bundles
  • Click on Run OSGi

If you are using a tool like Postman, you can test if the multipart upload is working by executing a POST request on http://localhost:8080/modify/upload

  • On the Body tab, select form-data
  • Set the Key to file and check that it is a File and not a Text
  • Select a text file to upload as Value

Note:
To return a file instead of plain text, you can return an EntityPart and change the @Produces annotation to return MediaType.MULTIPART_FORM_DATA.

@POST
@Path("modify/change")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.MULTIPART_FORM_DATA)
public Response change(
    @FormParam("file") EntityPart part,
    @FormParam("file") InputStream input) throws IOException {
  
  
  if (part != null
      && part.getFileName().isPresent()) {
    
    StringBuilder inputBuilder = new StringBuilder();
    try (InputStream is = input;
        BufferedReader br =
            new BufferedReader(new InputStreamReader(is))) {
      
      String line;
      while ((line = br.readLine()) != null) {
        inputBuilder.append(line).append("\n");
      }
    }
    
    // modify file content
    String inputString = inputBuilder.toString();
    List<String> modified = modifier.stream()
        .map(mod -> mod.modify(inputString))
        .collect(Collectors.toList());
    
    String resultString = String.join("\n", modified);
    
    return Response
        .ok(EntityPart
            .withFileName("changed.txt")
            .content(resultString)
            .build())
        .build();
  }
  
  return Response.status(Status.PRECONDITION_FAILED).build();
}

Interlude: Static Resources

If you asked yourself before, when to use a deployment on Jetty and when to use the OSGi Servlet Whiteboard, you get an answer in this part of the tutorial. We will publish a simple form as a static resource in our application. Doing this we are able to test the file upload even without additional tools.

To register a HTML form as static resource with our REST service, we use the Whiteboard Specification for Jakarta™ Servlet (formerly known as Http Whiteboard Specification).

  • Open the rest/pom.xml
  • Add the following dependency in the dependencies section ```xml
org.osgi org.osgi.service.servlet

- Add the `@HttpWhiteboardResource` annotation to the `ModifierRestService` class definition

```java
@JakartarsResource
@JakartarsName("modifier")
@Component(service = ModifierRestService.class, scope = ServiceScope.PROTOTYPE)
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@JakartarsApplicationSelect("(osgi.jakartars.name=*)")
@HttpWhiteboardResource(pattern = "/files/*", prefix = "static")
public class ModifierRestService { ... }

Important:
Once you add the @HttpWhiteboardResource annotation, your application won’t be able to resolve anymore with the deployment on Jetty setup. The reason is that the @HttpWhiteboardResource annotation itself uses the @RequireHttpWhiteboard, which means, an implementation of the Jakarta Servlet Whiteboard is required. Either replace the annotation with the corresponding Component Properties to avoid the additional requirement (see below), or ensure to run the example on the OSGi Jakarta Servlet Whiteboard.

@JakartarsResource
@JakartarsName("modifier")
@Component(
    service = ModifierRestService.class, 
    scope = ServiceScope.PROTOTYPE,
    // use component properties instead of component property type annotation
    // this way we avoid the requirement on the Servlet Whiteboard and the service also works on a Jetty
    property = {
        "osgi.http.whiteboard.resource.pattern=/files/*",
        "osgi.http.whiteboard.resource.prefix=static"
    })
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@JakartarsApplicationSelect("(osgi.jakartars.name=*)")
public class ModifierRestService { ... }

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 rest module.
  • Right click src/main/java - New - Folder
  • Select the main folder in the tree
  • Add resources/static in the Folder name field
  • Finish
  • Ensure that the resources folder is configured as source folder. If not:
    • 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 to Jakarta RESTful Web Service</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>
  • Open the app-http/app.bndrun
  • Resolve again to reflect the latest changes to the Run Bundles

After starting the app via app-http/app.bndrun you can open a browser and navigate to http://localhost:8080/files/upload.html. If you decided to modify the app project for deployment via OSGi Servlet Whiteboard, you of course need to start the application via app/app.bndrun. 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. In such situations you usually use an OSGi console to inspect the runtime. There are two types of OSGi consoles to inspect the OSGi runtime provided by Apache Felix:

Note:
The Webconsole only works on a deployment via OSGi Servlet Whiteboard. An alternative for OSGi Runtime Inspection would be OSGi.fx, which I plan to cover in an upcoming blog post.

To clearly separate the target application runtime from a debug runtime, it is best practice to create an additional .bndrun file. This file includes the app.bndrun and extends it with configurations to enable the inspection capabilities.

  • Locate the app project in the Bndtools Explorer
  • Create a new file debug.bndrun next to the app.bndrun file and add the following content in the Source tab
-include: ~app.bndrun

test-index: target/test-index.xml;name="app Test"

-standalone: ${index},${test-index}

-runproperties: \
    osgi.console=,\
    osgi.console.enable.builtin=false

-runrequires.debug: osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.shell)',\
 osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.runtime)',\
 osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.command)'

-resolve: manual

The -runproperties configuration will start the console in interactive mode. The -runrequires.debug configuration adds the necessary console bundles to the runtime.

  • Switch to the Run tab
  • Click on Resolve
  • Click on Run OSGi

The Gogo Shell becomes available in the Console View of the IDE. You can now interact with the runtime in the Console View, e.g. list all bundles in the runtime via

lb

As mentioned before, with the OSGi Jakarta Servlet Whiteboard, you also have the option to use the Felix Webconsole. To demonstrate this, we create a debug configuration in the app-http module.

  • Locate the app-http project in the Bndtools Explorer
  • Create a new file debug.bndrun next to the app.bndrun file and add the following content in the Source tab
-include: ~app.bndrun

test-index: target/test-index.xml;name="app Test"

-standalone: ${index},${test-index}

-runproperties: \
    osgi.console=,\
    osgi.console.enable.builtin=false,\
    org.osgi.service.http.port=-1

-runrequires.debug: osgi.identity;filter:='(osgi.identity=org.apache.felix.webconsole)',\
 osgi.identity;filter:='(osgi.identity=org.apache.felix.webconsole.plugins.ds)',\
 osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.shell)',\
 osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.runtime)',\
 osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.command)'

-resolve: manual

Compared to the app/debug.bndrun file, we include the Felix Webconsole bundles and ensure that the default Jetty in the Felix Jetty bundle is not started.

  • Switch to the Run tab
  • Click on Resolve
  • Click on Run OSGi

Now you can 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 jakartars 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 jakartars 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. This could be also avoided by configuring the bnd-export-maven-plugin setting the failOnChanges parameter to false.

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 jakartars/app/target. Execute the following line to start the self-executable jar from the command line if you are located in the jakartars 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 and/or the app-http module:

  • In the Bndtools Explorer locate the app / app-http module.
  • Open pom.xml
  • In the build/plugins section update the bnd-resolver-maven-plugin and 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>
<plugin>
  <groupId>biz.aQute.bnd</groupId>
  <artifactId>bnd-resolver-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

Implementing Jakarta™ RESTful Web Services with the OSGi Compendium Specification Release 8.1 and the corresponding reference implemenations is similar to approaches with other frameworks like Spring Boot, Quarkus 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.

With this tutorial I hope I can help developers to get started with the OSGi Jakarta™ RESTful Web Services Whiteboard and getting a better understanding of the mechanisms. Writing it at least helped me a lot in that area.

Further information:

Updated: