Getting Started with OSGi Remote Services - enRoute Maven Archetype Edition

46 minute read

At the EclipseCon Europe 2016 I held a tutorial together with Peter Kirschner named Building Nano Services with OSGi Declarative Services. The final exercise should have been the demonstration of OSGi Remote Services. It actually did not really happen because of the lack of time and networking issues. The next year at the EclipseCon Europe 2017 we joined forces again and gave a talk with the name Microservices with OSGi. In that talk we focused on OSGi Remote Services, but we again failed with the demo at the end because of networking issues. At the EclipseCon Europe 2018 I gave a talk on how to use different OSGi specifications for connecting services remotely titled How to connect your OSGi application. Of course I mentioned OSGi Remote Services there, and of course the demonstration failed again because of networking issues.

In the last years I published several blog posts and gave several talks related to OSGi, and often the topic OSGi Remote Services was raised, but never really covered in detail. Scott Lewis, the project lead of the Eclipse Communication Framework, was really helpful whenever I encountered issues with Remote Services. I promised to write a blog post about that topic as a favour for all the support. And with this blog post I finally want to keep my promise. That said, let’s start with OSGi Remote Services.

Motivation

First I want to explain the motivation for having a closer look at OSGi Remote Services. Looking at general software architecture discussions in the past, service oriented architectures and microservices are a huge topic. Per definition the idea of a microservices architecture is to have

  • a suite of small services
  • each running in its own process
  • communicating with a lightweight mechanism, e.g. HTTP
  • independently deployable
  • easy to replace

While new frameworks and tools came up over the years, the OSGi specification(s) covers these topics for a long time. Via the service registry and the service dynamics you can build up very small modules. Those modules can then be integrated into small runtimes and deployed in different environments (despite the needed JVM or a database if needed). The services in those small independent deployments can then be accessed in different ways, like using the HTTP Whiteboard or JAX-RS Whiteboard. This satisfies the aspect of a communication between services via lightweight mechanisms. For inhomogeneous environments the usage of those specifications is a good match. But it means that you need to implement the access layer on the provider side (e.g. the JAX-RS wrapper to access the service via REST) and you need to implement the service access on the consumer side by using a corresponding framework to execute the REST calls.

Ideally the developer of the service as well as the developer of the service consumer should not need to think about the infrastructure of the whole application. Well, it is always good that everybody in a project knows about everything, but the idea is to not making your code dependent on infrastructure. And this is where OSGi Remote Services come in. You develop the service and the service consumer as if they would be executed in the same runtime. In the deployment the lightweight communication will be added to support service communication over a network.

And as initially mentioned, I want to have a look at ways how to probably get rid of the networking issues I faced in the presentations in the past.

Introduction

To understand this blog post you should be familiar with OSGi services and ideally with OSGi Declarative Services. If you are not familiar with OSGi DS, you can get an introduction by reading my blog post Getting Started with OSGi Declarative Services.

In short, the OSGi Service Layer specifies a Service Producer that publishes a service, and a Service Consumer that listens and retrieves a service. This is shown in the following picture:

With OSGi Remote Services this picture is basically the same. The difference is that the services are registered and consumed across network boundaries. For OSGi Remote Services the above picture could be extended to look like the following:

Glossary

To understand the above picture and the following blog post better, here is a short glossary for the used terms:

  • Remote Service (Distributed Service)
    Basic specification to describe how OSGi services can be exported and imported to be available across network boundaries.
  • Distribution Provider
    Exports services by creating endpoints on the producer side, imports services by creating proxies to access endpoints on the consumer side, manages policies around the topology and discovers remote services.
  • Endpoint
    Communication access mechanism to a remote service that requires some protocol for communications.
  • Topology
    Mapping between services and endpoints as well as their communication characteristics.
  • Remote Service Admin (RSA)
    Provides the mechanisms to import and export services through a set of configuration types. It is a passive Distribution Provider, not taking any action to export or import itself.
  • Topology Manager
    Provides the policy for importing and exporting services via RSA and implements a Topology.
  • Discovery
    Discover / announce Endpoint Descriptions via some discovery protocol.
  • Endpoint Description
    A properties based description of an Endpoint that can be exchanged between different frameworks to create connections to each other’s services.

To get a slightly better understanding, the following picture shows some more details inside the Remote Service Implementation block.

Note:
Actually this picture is still a simplified version, as internally there are Endpoint Event Listener and Remote Service Admin Listener that are needed to trigger all the necessary actions. But to get an idea how things play together this picture should be sufficient.

Now let’s explain the picture in more detail:

Service Provider Runtime

  • A service is marked to be exported. This is done via service properties.
  • The Distribution Provider creates an endpoint for the exported service:
    • The Topology Manager gets informed about the exported service.
    • If the export configuration matches the Topology it instructs the Remote Service Admin to create an Endpoint.
    • The Remote Service Admin creates the Endpoint.
  • The Discovery gets informed via Endpoint Event Listener and announces the Endpoint to other systems via Endpoint Description.

Service Consumer Runtime

  • The Discovery discovers an Endpoint via Endpoint Description that was announced in the network.
  • The Distribution Provider creates a proxy for the service.
    • The Topology Manager learns from the Discovery about the newly discovered service (via Endpoint Event Listener), which then instructs the Remote Service Admin to import the service.
    • The Remote Service Admin then creates a local service proxy that is registered as service in the local OSGi runtime. This proxy is mapped to the remote service (or an alternative like a webservice).
  • The service proxy is used for wiring.

To simplify the picture again, the important takeaways are the Distribution Provider and the Discovery. The Distribution Provider is responsible for exporting and importing the service, the Discovery is responsible for announcing and discovering the service. The other terms are needed for a deeper understanding, but for a high level understanding of OSGi Remote Services, these two are sufficient.

Tutorial

Now it is time to get our hands dirty and play with OSGi Remote Services. This tutorial has several steps:

  1. Project Setup
  2. Service Implementation (API & Impl)
  3. Service Provider Runtime
  4. Service Consumer Implementation
  5. Service Consumer Runtime

There are different ways and tools available for OSGi development. In this tutorial I will use the OSGi enRoute Maven Archetypes. I also published this tutorial with other toolings if you don’t want to use enRoute:

ECF - Remote Service Runtime

While the implementation and export of an OSGi service as a Remote Service is trivial in first place, the definition of the runtime can become quite complicated. Especially collecting the necessary bundles is not that easy without some guidance.

As a reference, with Equinox as underlying OSGi framework the following bundles need to be part of the runtime as a basis:

  • Equinox OSGi
    • org.eclipse.osgi
    • org.eclipse.osgi.services
    • org.eclipse.equinox.common
    • org.eclipse.equinox.event
    • org.eclipse.osgi.util
    • org.apache.felix.scr
  • Equinox Console
    • org.apache.felix.gogo.command
    • org.apache.felix.gogo.runtime
    • org.apache.felix.gogo.shell
    • org.eclipse.equinox.console
  • ECF and dependencies
    • org.eclipse.core.jobs
    • org.eclipse.ecf
    • org.eclipse.ecf.discovery
    • org.eclipse.ecf.identity
    • org.eclipse.ecf.osgi.services.distribution
    • org.eclipse.ecf.osgi.services.remoteserviceadmin
    • org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy
    • org.eclipse.ecf.remoteservice
    • org.eclipse.ecf.remoteservice.asyncproxy
    • org.eclipse.ecf.sharedobject
    • org.eclipse.equinox.concurrent
    • org.eclipse.osgi.services.remoteserviceadmin

With the above basic runtime configuration the Remote Services will not yet work. There are still two things missing, the Discovery and the Distribution Provider. ECF provides different implementations for both. Which implementations to use needs to be defined by the project. In this tutorial we will use Zeroconf/JmDNS for the Discovery and the Generic Distribution Provider:

  • ECF Discovery - Zeroconf
    • org.eclipse.ecf.provider.jmdns
  • ECF Distribution Provider - Generic
    • org.eclipse.ecf.provider
    • org.eclipse.ecf.provider.remoteservice

Note:
You can find the list of different implementations with the documentation about the bundles, configuration types and intents in the ECF Wiki:

Project Setup

By using Maven and the OSGi enRoute archetypes you create plain Maven-Java projects. This way you can use any IDE if you are not comfortable with Eclipse and Bndtools. The first step is to create the projects from command line.

Workspace

Switch to a folder in which you want to create the projects. Create a minimal enRoute OSGi workspace by using the project-bare archetype:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=project-bare \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = enroute
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier
  • After accepting the inserted values with ‘y’ a subfolder named by the artifactId enroute is created that contains a basic minimal pom.xml file.

Service Interface

Change into the newly created folder enroute and create the Service API project by using the api archetype:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=api \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = api
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier.api
  • After accepting the inserted values with ‘y’ a subfolder named api is created that contains the api project structure and the api project is added as module to the parent pom.xml file.

Service Implementation

Create the Service Implementation project by using the ds-component archetype:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=ds-component \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = inverter
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier.inverter
  • After accepting the inserted values with ‘y’ a subfolder named inverter is created that contains the service implementation project structure and the inverter project is added as module to the parent pom.xml file.

Service Provider Runtime

With the OSGi enRoute Archetypes we create a composite application to put the modules together. This is done via the application archetype. Execute the following command in the enroute folder:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=application \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = inverter-app
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier
    • impl-artifactId = inverter
    • impl-groupId = org.fipro.modifier
    • impl-version = 1.0-SNAPSHOT
    • target-java-version = 11
  • First you need to decline the properties configuration, as by default target-java-version = 8 will be used. After setting the correct values and accepting them with ‘y’ a subfolder named inverter-app is created that contains .bndrun files and preparations for configuring the application.

Service Consumer

To be able to test the Remote Service, we directly create the Service Consumer project by again using the ds-component archetype:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=ds-component \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = client
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier.client
  • After accepting the inserted values with ‘y’ a subfolder named inverter is created that contains the service implementation project structure and the inverter project is added as module to the parent pom.xml file.

Service Consumer Runtime

The consumer will be a command line application. Therefore create an application project with the application archetype similar to creating the service application:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=application \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = client-app
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier
    • impl-artifactId = client
    • impl-groupId = org.fipro.modifier
    • impl-version = 1.0-SNAPSHOT
    • target-java-version = 11
  • First you need to decline the properties configuration, as by default target-java-version = 8 will be used. After setting the correct values and accepting them with ‘y’ a subfolder named client-app is created that contains .bndrun files and preparations for configuring the application.

Import, modify and implement

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 still my choice is Eclipse with bndtools.

  • Import the created projects via File - Import… - Maven - Existing Maven Projects
  • Click Next
  • Select the created enroute directory
  • Click Finish

Unfortunately the archetypes are some years old and were not updated since then. Using the enRoute OSGi Maven Archetypes you get project skeletons that are configured for Java 8, Bndtools 4.1.0 and OSGi R7. For this tutorial it is sufficient to use OSGi R7, but let’s update to Java 11 and the current Bndtools 6.2.0.

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:

  • Open the file enroute/pom.xml
  • Locate the properties section
    • Update bnd.version from 4.1.0 to 6.2.0
    • Remove maven.compiler.source
    • Remove maven.compiler.target
  • Locate the dependencyManagement section
    • Add the ECF dependencies configured like below
<!-- ECF dependencies -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.core.jobs</artifactId>
  <version>3.12.0</version>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.concurrent</artifactId>
  <version>1.2.100</version>
</dependency>

<!-- ECF -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf</artifactId>
  <version>3.10.0</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.discovery</artifactId>
  <version>5.1.1</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.identity</artifactId>
  <version>3.9.402</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.distribution</artifactId>
  <version>2.1.600</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin</artifactId>
  <version>4.9.3</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy</artifactId>
  <version>1.0.101</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice.asyncproxy</artifactId>
  <version>2.1.200</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice</artifactId>
  <version>8.14.0</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.sharedobject</artifactId>
  <version>2.6.200</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.osgi.services.remoteserviceadmin</artifactId>
  <version>1.6.300</version>
</dependency>

<!-- ECF Discovery - Zeroconf -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jmdns</artifactId>
  <version>4.3.301</version>
</dependency>

<!-- ECF Distribution Provider - Generic -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider</artifactId>
  <version>4.9.1</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.remoteservice</artifactId>
  <version>4.6.1</version>
</dependency>

Note:
Unfortunately the ECF project does not have the dependencies configured in the pom.xml files, so there automated resolving of transitive dependencies in Maven is not working. The reason is obviously the usage of Tycho and the resolving of dependencies based on the MANIFEST file. While the MANIFEST-first approach is nice at development time, it makes you a bad Maven citizen by default. If you as a project want to be also a good Maven citizen, you have to maintain the dependencies twice, in the MANIFEST for PDE based development and Tycho builds, and in the pom.xml file in the dependencies section, that is actually not used in the build and creates Warnings in the Tycho build.

For this example simply use the snippet above, which should help in managing the dependencies. But keep in mind that by the time the versions might have increased and need to be updated.

  • Locate the pluginManagement section
    • Add the maven-compiler-plugin configured like below
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.8.1</version>
      <configuration>
        <release>11</release>
      </configuration>
    </plugin>
  • Add the api project to the dependencies of the inverter project
    • Open the file inverter/pom.xml
    • Add the following block to the dependencies section
    <dependency>
        <groupId>org.fipro.modifier</groupId>
        <artifactId>api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
  • Add the api project to the dependencies of the client project
    • Open the file client/pom.xml
    • Add the following block to the dependencies section
    <dependency>
        <groupId>org.fipro.modifier</groupId>
        <artifactId>api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
  • Right click on the enroute project - Maven - Update Project…
    • Have all projects checked
    • Click OK

Service Interface

Modify the api project:

  • Delete the ConsumerInterface and the ProviderInterface
  • Copy the following interface StringModifier into the api project
package org.fipro.modifier.api;

public interface StringModifier {
    String modify(String input);
}

Service Implementation

Modify the inverter project:

  • Delete the ComponentImpl class
  • Copy the following class StringInverter into the inverter project
package org.fipro.modifier.inverter;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;

@Component(property= {
    "service.exported.interfaces=*",
    "service.exported.configs=ecf.generic.server" }
)
public class StringInverter implements StringModifier {

    @Override
    public String modify(String input) {
        return (input != null)
            ? new StringBuilder(input).reverse().toString()
            : "No input given";
    }
}

The only thing that needs to be done additionally in comparison to creating a local OSGi service, is to configure that the service should be exported as Remote Service. This is done by setting the component property service.exported.interfaces. The value of this property needs to be a list of types for which the class is registered as a service. For a simple use case like the above, the asterisk can be used, which means to export the service for all interfaces under which it is registered, but to ignore the classes. For more detailed information have a look at the Remote Service Properties section of the OSGi Compendium Specification.

The other component property used in the above example is service.exported.configs. This property is used to specify the configuration types, for which the Distribution Provider should create Endpoints. If it is not specified, the Distribution Provider is free to choose the default configuration type for the service.

Note:
In the above example we use the ECF Generic Provider. This one by default chooses a SSL configuration type, so without additional configuration the example would not work if we don’t specify the configuration type.

Additionally you can specify Intents via the service.exported.intents component property to constrain the possible communication mechanisms that a distribution provider can choose to distribute a service. An example will be provided at a later step.

Service Consumer

The implementation of a Remote Service Consumer also quite simple. From the development perspective there is nothing to consider. The service consumer is implemented without any additions. Only the runtime needs to be extended to contain the necessary bundles for Discovery and Distribution.

The simplest way of implementing a service consumer is a Gogo Shell command.

Modify the client project:

  • Delete the ComponentImpl class
  • Copy the following class ModifyCommand into the client project
package org.fipro.modifier.client;

import java.util.List;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=modify"},
    service=ModifyCommand.class
)
public class ModifyCommand {

    @Reference
    volatile List<StringModifier> modifier;

    public void modify(String input) {
        if (modifier.isEmpty()) {
            System.out.println("No StringModifier registered");
        } else {
            modifier.forEach(m -> System.out.println(m.modify(input)));
        }
    }
}

Now the ECF bundles need to be added to the dependencies section of the service-app/pom.xml. You can find the ECF bundles on Maven Central.

After the Maven Dependencies are updated, the .bndrun configuration can be updated to include the necessary bundles:

Service Provider Runtime

  • Open the inverter-app/pom.xml file
    • Add the dependencies to the ECF bundles as shown below (the versions are already configured in the parent pom.xml dependencyManagement section)
<!-- ECF dependencies -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.core.jobs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.concurrent</artifactId>
</dependency>

<!-- ECF -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.identity</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.distribution</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice.asyncproxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.sharedobject</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.osgi.services.remoteserviceadmin</artifactId>
</dependency>

<!-- ECF Discovery - Zeroconf -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jmdns</artifactId>
</dependency>

<!-- ECF Distribution Provider - Generic -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.remoteservice</artifactId>
</dependency>
  • Open the inverter-app/inverter-app.bndrun file
  • Add the following bundles to the Run Requirements
    • org.fipro.modifier.inverter
    • org.eclipse.ecf.osgi.services.distribution
    • org.eclipse.ecf.provider.jmdns
    • org.eclipse.ecf.provider.remoteservice
  • Save the changes
  • Click on Resolve
  • Accept the result in the opening dialog via Update

Now you can start the inverter-app via the Run OSGi button in the upper right corner of the editor. As there is nothing included in the runtime that would show up somewhere, you won’t see anything now.

Service Consumer Runtime

The client app is a simple command line application that uses the Gogo Shell. To get the Gogo Shell up and running some additional steps need to be performed in the client-app. By default the Gogo Shell bundles are included in the project setup for the test scope and for debugging. To make them available in the compile scope:

  • Open the client-app/pom.xml file
    • Add the following block in the dependencies section for the ECF bundles (the versions are already configured in the parent pom.xml dependencyManagement section)
<!-- ECF dependencies -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.core.jobs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.concurrent</artifactId>
</dependency>

<!-- ECF -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.identity</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.distribution</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice.asyncproxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.sharedobject</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.osgi.services.remoteserviceadmin</artifactId>
</dependency>

<!-- ECF Discovery - Zeroconf -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jmdns</artifactId>
</dependency>

<!-- ECF Distribution Provider - Generic -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.remoteservice</artifactId>
</dependency>
  • Add the following block in the dependencies section for the Gogo Shell bundles (actually copied from the org.osgi.enroute:debug-bundles, so the versions are probably outdated, but for the example sufficient).
<!-- The Gogo Shell -->
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.gogo.shell</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.gogo.runtime</artifactId>
    <version>1.0.10</version>
</dependency>
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.gogo.command</artifactId>
    <version>1.0.2</version>
    <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>
  • Open the client-app/client-app.bndrun file
  • Add the following bundles to the Run Requirements
    • org.apache.felix.gogo.command
    • org.apache.felix.gogo.shell
    • org.fipro.modifier.client
    • org.eclipse.ecf.osgi.services.distribution
    • org.eclipse.ecf.provider.jmdns
    • org.eclipse.ecf.provider.remoteservice
  • Save the changes
  • Click on Resolve
  • Accept the result in the opening dialog via Update
  • Switch to the Source tab of the .bndrun file editor and add the following section to start the console in interactive mode
-runproperties: \
    osgi.console=,\
    osgi.console.enable.builtin=false

If you now click on Run OSGi on the Run tab of the editor, the Gogo Shell becomes available in the Console view of the IDE. Once the application is started you can execute the created Gogo Shell command via

modify <input>

If services are available, it will print out the modified results. Otherwise the message “No StringModifier registered” will be printed.

Remote Service Admin Events

There are several events with regards to importing and exporting Remote Services, that are fired by the Remote Service Admin synchronously once they happen. These events are posted asynchronously via the OSGi Event Admin under the topic

org/osgi/service/remoteserviceadmin/<type>

Where <type> can be one of the following:

  • EXPORT_ERROR
  • EXPORT_REGISTRATION
  • EXPORT_UNREGISTRATION
  • EXPORT_UPDATE
  • EXPORT_WARNING
  • IMPORT_ERROR
  • IMPORT_REGISTRATION
  • IMPORT_UNREGISTRATION
  • IMPORT_UPDATE
  • IMPORT_WARNING

A simple event listener that prints to the console on any Remote Service Admin Event could look like this:

@Component(property = EventConstants.EVENT_TOPIC + "=org/osgi/service/remoteserviceadmin/*")
public class RemoteServiceEventListener implements EventHandler {

    @Override
    public void handleEvent(Event event) {
        System.out.println(event.getTopic());
        for (String objectClass :  ((String[])event.getProperty("objectClass"))) {
            System.out.println("\t"+objectClass);
        }
    }

}

For further details on the Remote Service Admin Events have a look at the OSGi Compendium Specification Chapter 122.7.

If you need to react synchronously on these events, you can implement a RemoteServiceAdminListener. I typically would not recommend this, unless you really want blocking calls on import/export events. Typically it is intended to be used internally by the Remote Service Admin. But for debugging purposes the ECF project also provides a DebugRemoteServiceAdminListener. It writes the endpoint description via a Writer to support debugging of Remote Services. Via the following class you could easily register a DebugRemoteServiceAdminListener via OSGi DS that prints the information on the console.

@Component
public class DebugListener
    extends DebugRemoteServiceAdminListener
    implements RemoteServiceAdminListener {
	// register the DebugRemoteServiceAdminListener via DS
}

To test this you can either add the above components to one of the existing bundles, or create a new bundle and add that bundle to the runtimes.

Runtime Debugging

The ECF project provides several ways for runtime inspection and runtime debugging. This is mainly done Gogo Shell commands provided via separate bundles. To enable the OSGi console and the ECF console commands, you need to add the following bundles to your runtime:

  • OSGi Console
    • org.apache.felix.gogo.command
    • org.apache.felix.gogo.runtime
    • org.apache.felix.gogo.shell
  • ECF Console
    • org.eclipse.ecf.console
    • org.eclipse.ecf.osgi.services.remoteserviceadmin.console

With the ECF Console bundles added to the runtime, there are several commands to inspect and interact with the Remote Service Admin. As an overview the available commands are listed in the wiki: Gogo Commands for Remote Services Development

Additionally the DebugRemoteServiceAdminListener described above is activated by default with the ECF Console bundles. It can be activated or deactivated in the runtime via the command

ecf:rsadebug <true/false>

To add the ECF Console bundles to the project, add the following snippet to the dependencyManagement section of the enroute/pom.xml file:

<dependency>
    <groupId>org.eclipse.ecf</groupId>
    <artifactId>org.eclipse.ecf.console</artifactId>
    <version>1.3.100</version>
</dependency>
<dependency>
    <groupId>org.eclipse.ecf</groupId>
    <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.console</artifactId>
    <version>1.3.0</version>
</dependency>

JAX-RS Distribution Provider

One of the biggest issues I faced when working with Remote Services is networking as mentioned in the introduction. In the above example the ECF Generic Distribution Provider is used for a simpler setup. But for example in a corporate network with enabled firewalls somewhere in the network setup, the example will probably not work. As said before, the ECF project provides multiple Distribution Provider implementations, which gives the opportunity to configure the setup to match the project needs. One interesting implementation in that area is the JAX-RS Distribution Provider. Using that one could probably help solving several of the networking issues related to firewalls. But as with the whole Remote Service topic, the complexity in the setup is quite high because of the increased number of dependencies that need to be resolved.

The JAX-RS Distribution Provider implementation is available for Eclipse Jersey and Apache CXF. It uses the OSGi HttpService to register the JAX-RS resource, and of course it then also needs a Servlet container like Eclipse Jetty to provide the JAX-RS resource. I will show the usage of the Jersey based implementation in the following sections.

Project Setup

Unfortunately the JAX-RS Distribution Provider is not available via Maven Central. The only way to get the project setup done is to install the artifacts in the local repository. This can be done by installing the artifacts locally via mvn clean install. Alternatively you can use the maven-install-plugin, which can even be integrated into your Maven build if you add the artifact to install to the source code repository. For this tutorial we use the manual installation of artifacts, as it is the easier approach for now.

Note:
The artifact versions in the below snippets rely on the JAX-RS Distribution Provider 1.14.6 which was the most current version at the time this tutorial was written. If there is a newer version available in the meantime you need to update the snippets.

  • Download the JaxRSProviders archive from GitHub
  • Extract the following artifacts in a temporary directory (located in build/plugins)
    • org.eclipse.ecf.provider.jaxrs.client_1.8.1.202202112253.jar
    • org.eclipse.ecf.provider.jaxrs.server_1.11.1.202202112253.jar
    • org.eclipse.ecf.provider.jaxrs_1.7.1.202202112253.jar
    • org.eclipse.ecf.provider.jersey.client_1.8.2.202202112253.jar
    • org.eclipse.ecf.provider.jersey.server_1.11.1.202202112253.jar
  • Open a shell and execute the following commands to install the artifacts to the local Maven repository
mvn install:install-file \
  -Dfile=org.eclipse.ecf.provider.jaxrs\_1.7.1.202202112253.jar \
  -DgroupId=org.eclipse.ecf \
  -DartifactId=org.eclipse.ecf.provider.jaxrs \
  -Dversion=1.7.1 \
  -Dpackaging=jar

mvn install:install-file \
  -Dfile=org.eclipse.ecf.provider.jaxrs.server\_1.11.1.202202112253.jar \
  -DgroupId=org.eclipse.ecf \
  -DartifactId=org.eclipse.ecf.provider.jaxrs.server \
  -Dversion=1.11.1 \
  -Dpackaging=jar

mvn install:install-file \
  -Dfile=org.eclipse.ecf.provider.jersey.server\_1.11.1.202202112253.jar \
  -DgroupId=org.eclipse.ecf \
  -DartifactId=org.eclipse.ecf.provider.jersey.server \
  -Dversion=1.11.1 \
  -Dpackaging=jar

mvn install:install-file \
  -Dfile=org.eclipse.ecf.provider.jaxrs.client\_1.8.1.202202112253.jar \
  -DgroupId=org.eclipse.ecf \
  -DartifactId=org.eclipse.ecf.provider.jaxrs.client \
  -Dversion=1.8.1 \
  -Dpackaging=jar

mvn install:install-file \
  -Dfile=org.eclipse.ecf.provider.jersey.client\_1.8.2.202202112253.jar \
  -DgroupId=org.eclipse.ecf \
  -DartifactId=org.eclipse.ecf.provider.jersey.client \
  -Dversion=1.8.2 \
  -Dpackaging=jar
  • Open the file enroute/pom.xml
    • Locate the dependencyManagement section
    • Add the JAX-RS Distribution Provider dependencies configured like below
<!-- ECF JAX-RS Distribution Provider -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs</artifactId>
  <version>1.7.1</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs.server</artifactId>
  <version>1.11.1</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jersey.server</artifactId>
  <version>1.11.1</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs.client</artifactId>
  <version>1.8.1</version>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jersey.client</artifactId>
  <version>1.8.2</version>
</dependency>

<!-- ECF JAX-RS Distribution Provider Dependencies -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.10.1</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-json-provider</artifactId>
  <version>2.10.1</version>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
  <version>2.30.1</version>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet-core</artifactId>
  <version>2.30.1</version>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
  <version>2.30.1</version>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
  <version>2.30.1</version>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
  <version>2.30.1</version>
</dependency>

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

Note:
I have chosen the same versions for the dependencies as the JAX-RS Distribution Provider has. There are already newer versions available, so you can check if newer versions would work. Also note that the above snippet is the minimal necessary configuration. All other dependencies are resolved transitively. I have chosen this approach to minimize the snippet.

JAX-RS Remote Service Implementation

The implementation of the service already looks different compared to what you have seen so far. Instead of only adding the necessary Component Properties to configure the service as a Remote Service, the service implementation does directly contain the JAX-RS annotations. That of course also means that the annotations need to be available.

  • Create the Service Implementation project by using the ds-component archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=ds-component \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = uppercase
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier.uppercase
  • After accepting the inserted values with ‘y’ a subfolder named uppercase is created that contains the service implementation project structure and the uppercase project is added as module to the parent pom.xml file.

  • Import the created project via
    File - Import… - Maven - Existing Maven Projects
  • Click Next
  • Select the enroute directory
  • Select the created uppercase project
  • Click Finish

  • Add the api project and jakarta.ws.rs-api to the dependencies of the uppercase project
    • Open the file uppercase/pom.xml
    • Add the following block to the dependencies section
    <dependency>
        <groupId>org.fipro.modifier</groupId>
        <artifactId>api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>jakarta.ws.rs</groupId>
        <artifactId>jakarta.ws.rs-api</artifactId>
        <version>2.1.6</version>
    </dependency>
  • Delete the ComponentImpl class
  • Copy the following class UppercaseModifier into the uppercase project
package org.fipro.modifier.uppercase;

import java.util.Locale;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

//The JAX-RS path annotation for this service
@Path("/modify")
//The OSGi DS component annotation
@Component(
    immediate = true,
    property = {
        "service.exported.interfaces=*",
        "service.exported.intents=jaxrs"})
public class UppercaseModifier implements StringModifier {

    @GET
    // The JAX-RS annotation to specify the result type
    @Produces(MediaType.TEXT_PLAIN)
    // The JAX-RS annotation to specify that the last part
    // of the URL is used as method parameter
    @Path("/{value}")
    @Override
    public String modify(@PathParam("value") String input) {
        return (input != null)
            ? input.toUpperCase(Locale.getDefault())
            : "No input given";
    }
}

For the JAX-RS annotations, please have a look at various existing tutorials and blog posts in the internet, for example

About the OSGi DS configuration:

  • The service is an Immediate Compontent, so it is consumed by the OSGi Http Whiteboard on startup
  • Export all interfaces as Remote Service via service.exported.interfaces=*
  • Configure that JAX-RS is used as communication mechanism by the distribution provider via service.exported.intents=jaxrs

Note:
As mentioned earlier there is a bug in ECF 3.14.26 which is integrated in the Eclipse 2021-21 SimRel repo. The service.exported.intents property is not enough to get the JAX-RS resource registered. Additionally it is necessary to set service.exported.configs=ecf.jaxrs.jersey.server to make it work. This was fixed shortly after I reported it and is included with the current ECF 3.14.31 release. The basic idea of the intent configuration is to make the service independent of the underlying JAX-RS Distribution Provider implementation (Jersey vs. Apache CXF).

JAX-RS Jersey Distribution Provider Dependencies

For the JAX-RS Distribution Provider Runtime a lot more dependencies are required. The following list should cover the additional necessary base dependencies:

  • Jackson
    • com.fasterxml.jackson.core.jackson-annotations
    • com.fasterxml.jackson.core.jackson-core
    • com.fasterxml.jackson.core.jackson-databind
    • com.fasterxml.jackson.jaxrs.jackson-jaxrs-base
    • com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider
    • com.fasterxml.jackson.module.jackson-module-jaxb-annotations
  • Jersey / Glassfish / Dependencies
    • org.glassfish.hk2.api
    • org.glassfish.hk2.external.aopalliance-repackaged
    • org.glassfish.hk2.external.jakarta.inject
    • org.glassfish.hk2.locator
    • org.glassfish.hk2.osgi-resource-locator
    • org.glassfish.hk2.utils
    • org.glassfish.jersey.containers.jersey-container-servlet
    • org.glassfish.jersey.containers.jersey-container-servlet-core
    • org.glassfish.jersey.core.jersey-client
    • org.glassfish.jersey.core.jersey-common
    • org.glassfish.jersey.core.jersey-server
    • org.glassfish.jersey.ext.jersey-entity-filtering
    • org.glassfish.jersey.inject.jersey-hk2
    • org.glassfish.jersey.media.jersey-media-jaxb
    • org.glassfish.jersey.media.jersey-media-json-jackson
    • com.sun.activation.javax.activation
    • jakarta.annotation-api
    • javax.ws.rs-api
    • jakarta.xml.bind-api
    • javassist
    • javax.validation.api
    • org.slf4j.api

For the Service Provider we need the following dependencies, which are the JAX-RS Jersey Distribution Provider Server bundles, the Jetty as embedded server and the HTTP Whiteboard:

  • ECF Distribution Provider - JAX-RS Jersey
    • org.eclipse.ecf.provider.jaxrs
    • org.eclipse.ecf.provider.jaxrs.server
    • org.eclipse.ecf.provider.jersey.server
  • Jetty / Http Whiteboard / Http Service
    • org.apache.felix.http.jetty
    • org.apache.felix.http.servlet-api

For the Service Consumer we need the following dependencies, which are the JAX-RS Jersey Distribution Provider Client bundles and the HttpClient to be able to access the JAX-RS resource:

  • ECF Distribution Provider - JAX-RS Jersey
    • org.eclipse.ecf.provider.jaxrs
    • org.eclipse.ecf.provider.jaxrs.client
    • org.eclipse.ecf.provider.jersey.client

Service Provider Runtime

  • Create the Service Provider Runtime project using the application archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=application \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = uppercase-app
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier
    • impl-artifactId = uppercase
    • impl-groupId = org.fipro.modifier
    • impl-version = 1.0-SNAPSHOT
    • target-java-version = 11
  • First you need to decline the properties configuration, as by default target-java-version = 8 will be used. After setting the correct values and accepting them with ‘y’ a subfolder named uppercase-app is created that contains .bndrun files and preparations for configuring the application.
  • Import the created project via File - Import… - Maven - Existing Maven Projects
  • Click Next
  • Select the enroute directory
  • Select the created uppercase-app project
  • Click Finish
  • Open the uppercase-app/pom.xml file
    • Add the dependencies to the ECF bundles as shown below (the versions are already configured in the parent pom.xml dependencyManagement section)
<!-- ECF dependencies -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.core.jobs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.concurrent</artifactId>
</dependency>

<!-- ECF -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.identity</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.distribution</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice.asyncproxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.sharedobject</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.osgi.services.remoteserviceadmin</artifactId>
</dependency>

<!-- ECF Discovery - Zeroconf -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jmdns</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs.server</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jersey.server</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider Dependencies -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
</dependency>
  • Open the uppercase-app/uppercase-app.bndrun file

    • Add the following bundles to the Run Requirements
      • org.fipro.modifier.uppercase
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jmdns
      • org.eclipse.ecf.provider.jersey.server
      • org.apache.felix.http.jetty
      • org.eclipse.equinox.event
    • Add the following property to the OSGi Framework properties:
      • org.osgi.service.http.port=8181
    • Save the changes
    • Click on Resolve
    • Accept the result in the opening dialog via Update

Note:
With the latest version of the JAX-RS Distribution Provider, the .bndrun configuration is much more comfortable than before. There were several improvements to make the definition of a runtime more user friendly, so if you are already familiar with the JAX-RS Distribution Provider and used it in the past, be sure to update it to the latest version to benefit from the latest modifications.

Now you can start the Uppercase JAX-RS Service Runtime from the Overview tab via Launch an Eclipse application. After the runtime is started the service will be available as JAX-RS resource and can be accessed in a browser, e.g. http://localhost:8181/modify/remoteservice

Note:
Unfortunately with the above setup, you will see a 404 instead of the service result. It seems that using Jetty 9 the usage of the base URL is not working for Remote Services. Maybe it is only a configuration issue that I was not able to solve as part of this tutorial. There are two options to handle this issue, either configure additional path segments or use Jetty 10.

Note:
Don’t worry if you see a SelectContainerException in the console. It is only an information that tells that the service from the first part of the tutorial can not be imported in the runtime of this part of the tutorial and vice versa. The first service is distributed via the Generic Provider, while the second service is distributed by the JAX-RS Provider. But both are using the JmDNS Discovery Provider.

The URL path is defined via the JAX-RS annotations, “modify” via @Path("/modify") on the class, “remoteservice” is the path parameter defined via @Path("/{value}") on the method (if you change that value, the result will change accordingly). You can extend the URL via configurations shown below:

  • Add a prefix URL path segment on runtime level: Add the following property to the OSGi Framework properties ecf.jaxrs.server.pathPrefix=<value> (e.g. ecf.jaxrs.server.pathPrefix=/services)
  • Add a leading URL path segment on service level: Add the following component property to the @Component annotation ecf.jaxrs.server.pathPrefix=<value> e.g.
    @Component(
      immediate = true,
      property = {
          "service.exported.interfaces=*",
          "service.exported.configs=ecf.jaxrs.jersey.server",
          "service.exported.intents=jaxrs",
          "ecf.jaxrs.server.pathPrefix=/upper"})
    

    If all of the above configurations are added, the new URL to the service is, e.g. http://localhost:8181/services/upper/modify/remoteservice

Additional information about available component properties can be found here: Jersey Service Properties

Service Provider Runtime - Jetty 10

With the above setup the bundle org.apache.felix.http.jetty is integrated in the runtime. That bundle combines the following:

  • OSGi Http Service
  • OSGi Http Whiteboard
  • Jetty 9

This makes the integration very easy. If you want to update to Jetty 10 the setup is more complicated, as that is not available as combined Felix bundle. In that case you need the following bundles:

  • Jetty 10
    • org.eclipse.jetty.http
    • org.eclipse.jetty.io
    • org.eclipse.jetty.security
    • org.eclipse.jetty.server
    • org.eclipse.jetty.servlet
    • org.eclipse.jetty.util
    • org.eclipse.jetty.util.ajax
  • OSGi Http Service and Http Whiteboard (Equinox / Jetty)
    • org.eclipse.equinox.http.jetty
    • org.eclipse.equinox.http.servlet
  • OSGi Service Interfaces
    • org.eclipse.osgi.services

First you create a new Service Provider Runtime project that includes Jetty 10:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=application \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = uppercase-app-jetty10
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier
    • impl-artifactId = uppercase
    • impl-groupId = org.fipro.modifier
    • impl-version = 1.0-SNAPSHOT
    • target-java-version = 11
  • First you need to decline the properties configuration, as by default target-java-version = 8 will be used. After setting the correct values and accepting them with ‘y’ a subfolder named uppercase-app is created that contains .bndrun files and preparations for configuring the application.

  • Import the created project via File - Import… - Maven - Existing Maven Projects
  • Click Next
  • Select the enroute directory
  • Select the created uppercase-app-jetty10 project
  • Click Finish
  • Open the uppercase-app-jetty10/pom.xml file
    • Add the dependencies to ECF, Jetty 10 and Equinox Http as shown below (of course you can also configure the versions for Jetty 10 etc. in the enroute/pom.xml dependencyManagement section as described before)
<!-- ECF dependencies -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.core.jobs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.concurrent</artifactId>
</dependency>

<!-- ECF -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.identity</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.distribution</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice.asyncproxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.sharedobject</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.osgi.services.remoteserviceadmin</artifactId>
</dependency>

<!-- ECF Discovery - Zeroconf -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jmdns</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs.server</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jersey.server</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider Dependencies -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
</dependency>

<!-- Equinox OSGi Http Service and Http Whiteboard -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.osgi.services</artifactId>
  <version>3.10.200</version>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.http.jetty</artifactId>
  <version>3.8.100</version>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.http.servlet</artifactId>
  <version>1.7.200</version>
</dependency>

<!-- Jetty 10 -->
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-http</artifactId>
  <version>10.0.8</version>
</dependency>
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-io</artifactId>
  <version>10.0.8</version>
</dependency>
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-security</artifactId>
  <version>10.0.8</version>
</dependency>
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-server</artifactId>
  <version>10.0.8</version>
</dependency>
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-servlet</artifactId>
  <version>10.0.8</version>
</dependency>
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-util</artifactId>
  <version>10.0.8</version>
</dependency>
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-util-ajax</artifactId>
  <version>10.0.8</version>
</dependency>

<!-- Jetty 10 Dependencies -->
<dependency>
  <groupId>jakarta.servlet</groupId>
  <artifactId>jakarta.servlet-api</artifactId>
  <version>4.0.4</version>
</dependency>
<dependency>
  <groupId>jakarta.xml.bind</groupId>
  <artifactId>jakarta.xml.bind-api</artifactId>
  <version>2.3.3</version>
</dependency>

<!-- Gogo Shell & ECF Console - optionally -->
<dependency>
  <groupId>org.apache.felix</groupId>
  <artifactId>org.apache.felix.gogo.shell</artifactId>
  <version>1.0.0</version>
</dependency>
<dependency>
  <groupId>org.apache.felix</groupId>
  <artifactId>org.apache.felix.gogo.runtime</artifactId>
  <version>1.0.10</version>
</dependency>
<dependency>
  <groupId>org.apache.felix</groupId>
  <artifactId>org.apache.felix.gogo.command</artifactId>
  <version>1.0.2</version>
  <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>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.console</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.console</artifactId>
</dependency>
  • Open the file uppercase-app-jetty10.bndrun
    • Add the following bundles to the Run Requirements
      • org.fipro.modifier.uppercase
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jmdns
      • org.eclipse.ecf.provider.jersey.server
      • org.eclipse.equinox.http.jetty
      • org.eclipse.equinox.http.servlet
      • org.eclipse.jetty.http
      • org.eclipse.jetty.io
      • org.eclipse.jetty.security
      • org.eclipse.jetty.server
      • org.eclipse.jetty.servlet
      • org.eclipse.jetty.util
      • org.eclipse.jetty.util.ajax
      • org.eclipse.equinox.event
    • Add org.apache.felix.http.jetty to the Run Blacklist
      (this is necessary to avoid that this bundle is used by the resolve step)
    • Optional: Add the following console bundles for debugging and inspection
      • org.apache.felix.gogo.command
      • org.apache.felix.gogo.shell
      • org.eclipse.ecf.osgi.services.remoteserviceadmin.console
    • Set Execution Env.: JavaSE-11
    • Add the following properties to the OSGi Framework properties:
      • org.osgi.service.http.port=8181
      • launch.activation.eager=true
    • Optional: Add the following properties to the OSGi Framework properties for the interactive console:
      • osgi.console=
      • osgi.console.enable.builtin=false
    • Save the changes
    • Click on Resolve
    • Accept the result in the opening dialog via Update

Note:
The OSGi Framework property launch.activation.eager=true is necessary because of the activation policy set in the Equinox Jetty Http Service bundle. It is configured to be activated lazy, which means it will only be activated if someone requests something from that bundle. But as Equinox does collect all OSGi service interfaces in org.eclipse.osgi.services, actually nobody ever will request something from that bundle, which leaves it in the STARTING state forever. With launch.activation.eager property the lazy activation will be ignored and all bundles will be simply started. Bug 530076 was created to discuss if the lazy activation could be dropped.

Note:
Unfortunately you can not include the org.apache.felix.webconsole in a Jetty 10 runtime. The reason is the Servlet API version dependency of webconsole. org.apache.felix.webconsole requires javax.servlet;version="[2.4,4)" even in its latest version, while org.eclipse.jetty.servlet requires javax.servlet;version="[4.0.0,5)". So if you want to use the webconsole in your JAX-RS Remote Service, you need to stick with Jetty 9.

Note:
It is currently not possible to use Jetty 11 for OSGi development, as the OSGi implementations are not updated to the jakarta namespace.

For an overview on the Jetty versions and dependencies, have a look at the Jetty Downloads page.

Service Consumer Runtime

To consume the Remote Service provided via JAX-RS Distribution Provider, the runtime needs to be extended to include the additional dependencies:

  • Open the file client-app/pom.xml
    • Add the following snippet to the dependencies section
<!-- ECF JAX-RS Distribution Provider -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs.client</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jersey.client</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider Dependencies -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
</dependency>

<!-- ECF Console -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.console</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.console</artifactId>
</dependency>
  • Open the file client-app/client-app.bndrun
    • Add the following bundle to the Run Requirements
      • org.eclipse.ecf.provider.jersey.client
    • Save the changes
    • Click on Resolve to update the Run Bundles

If you now start the Service Consumer Runtime and have the Service Provider Runtime also running, you can execute the following command

modify jax

This will actually lead to an error if you followed my tutorial step by step:

ServiceException: Service exception on remote service proxy

The reason is that the Service Interface does not contain the JAX-RS annotations as the service actually does, and therefore the mapping is working. So while for providing the service the interface does not need to be modified, it has to for the consumer side.

Extend the Service Interface

  • Open the file api/pom.xml
  • Add the following snippet to the dependencies section
      <dependency>
          <groupId>jakarta.ws.rs</groupId>
          <artifactId>jakarta.ws.rs-api</artifactId>
          <version>2.1.6</version>
      </dependency>
    
  • Open the StringModifier class and add the JAX-RS annotations to be exactly the same as for the Service Implementation
package org.fipro.modifier.api;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/modify")
public interface StringModifier {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{value}")
    String modify(@PathParam("value") String input);
}

If you now start the Uppercase Service Provider Runtime and the Service Consumer Runtime again, the error should be gone and you should see the expected result.

Update the “Inverter” Service Provider Runtime

After the Service Interface was extended to include the JAX-RS annotations, the first Service Provider Runtime will not resolve anymore because of missing dependencies. To fix this:

  • Open the file inverter-app/inverter-app.bndrun
    • Click on Resolve to update the Run Bundles

Now you can start that Service Provider Runtime again. If the other Service Provider and the Service Consumer are also active, executing the modify command will now output the result of both services.

Endpoint Description Extender Format (EDEF)

In the tutorial we used JmDNS/Zeroconf as Discovery Provider. This way there is not much we have to do as a developer or administrator despite adding the according bundle to the runtime. This kind of Discovery is using a broadcast mechanism to announce the service in the network. In cases this doesn’t work, e.g. firewall rules that block broadcasting, it is also possible that you use a static file-based discovery. This can be done using the Endpoint Description Extender Format (EDEF) and is also supported by ECF.

Let’s create an additional service that is distributed via JAX-RS. But this time we exclude the org.eclipse.ecf.provider.jmdns bundle, so there is no additional discovery inside the Service Provider Runtime. We also add the console bundles to be able to inspect the runtime.

Note:
If you don’t want to create another service, you can also modify the previous uppercase service. In that case remove the org.eclipse.ecf.provider.jmdns bundle from the product configuration and ensure that the console bundles are added to be able to inspect the remote service runtime via the OSGi Console.

  • Create the Service Implementation project by using the ds-component archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=ds-component \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = camelcase
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier.camelcase
  • After accepting the inserted values with ‘y’ a subfolder named camelcase is created that contains the service implementation project structure and the camelcase project is added as module to the parent pom.xml file.

  • Create the Service Provider Runtime project using the application archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=application \
    -DarchetypeVersion=7.0.0
  • The execution will interactively ask you for some values to be used by the project creation. Use the following values or adjust them to your personal needs:
    • groupId = org.fipro.modifier
    • artifactId = camelcase-app
    • version = 1.0-SNAPSHOT
    • package = org.fipro.modifier
    • impl-artifactId = camelcase
    • impl-groupId = org.fipro.modifier
    • impl-version = 1.0-SNAPSHOT
    • target-java-version = 11
  • First you need to decline the properties configuration, as by default target-java-version = 8 will be used. After setting the correct values and accepting them with ‘y’ a subfolder named camelcase-app is created that contains .bndrun files and preparations for configuring the application.

  • Import the created projecst via File - Import… - Maven - Existing Maven Projects
  • Click Next
  • Select the enroute directory
  • Select the created camelcase and camelcase-app projects
  • Click Finish

  • Add the api project and jakarta.ws.rs-api to the dependencies of the camelcase project
    • Open the file camelcase/pom.xml
    • Add the following block to the dependencies section
    <dependency>
        <groupId>org.fipro.modifier</groupId>
        <artifactId>api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>jakarta.ws.rs</groupId>
        <artifactId>jakarta.ws.rs-api</artifactId>
        <version>2.1.6</version>
    </dependency>
  • Delete the ComponentImpl class
  • Copy the following class CamelCaseModifier into the camelcase project
package org.fipro.modifier.camelcase;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;

@Path("/modify")
@Component(
    immediate = true,
    property = {
        "service.exported.interfaces=*",
        "service.exported.intents=jaxrs",
        "ecf.jaxrs.server.pathPrefix=/camelcase"})
public class CamelCaseModifier implements StringModifier {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{value}")
    @Override
    public String modify(@PathParam("value") String input) {
        StringBuilder builder = new StringBuilder();
        if (input != null) {
            for (int i = 0; i < input.length(); i++) {
                char currentChar = input.charAt(i);
                if (i % 2 == 0) {
                    builder.append(Character.toUpperCase(currentChar));
                } else {
                    builder.append(Character.toLowerCase(currentChar));
                }
            }
        }
        else {
            builder.append("No input given");
        }
        return builder.toString();
    }
}
  • Open the camelcase-app/pom.xml file
    • Add the dependencies to the ECF bundles as shown below (the versions are already configured in the parent pom.xml dependencyManagement section)
<!-- ECF dependencies -->
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.core.jobs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.equinox.concurrent</artifactId>
</dependency>

<!-- ECF -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.identity</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.distribution</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice.asyncproxy</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.remoteservice</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.sharedobject</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.osgi.services.remoteserviceadmin</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jaxrs.server</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.provider.jersey.server</artifactId>
</dependency>

<!-- ECF JAX-RS Distribution Provider Dependencies -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
</dependency>

<!-- The Gogo Shell -->
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.gogo.shell</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.gogo.runtime</artifactId>
    <version>1.0.10</version>
</dependency>
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.gogo.command</artifactId>
    <version>1.0.2</version>
    <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>

<!-- ECF Console -->
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.console</artifactId>
</dependency>
<dependency>
  <groupId>org.eclipse.ecf</groupId>
  <artifactId>org.eclipse.ecf.osgi.services.remoteserviceadmin.console</artifactId>
</dependency>
  • Open the camelcase-app/camelcase-app.bndrun file
    • Add the following bundles to the Run Requirements
      • org.fipro.modifier.camelcase
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jersey.server
      • org.apache.felix.http.jetty
      • org.eclipse.equinox.event
      • org.apache.felix.gogo.command
      • org.apache.felix.gogo.shell
      • org.eclipse.ecf.osgi.services.remoteserviceadmin.console
    • Add the following property to the OSGi Framework properties:
      • osgi.console=
      • osgi.console.enable.builtin=false
      • org.osgi.service.http.port=8282
      • ecf.jaxrs.server.pathPrefix=/services
    • Save the changes
    • Click on Resolve
    • Accept the result in the opening dialog via Update

Once the runtime is started via Run OSGi the service should be available via http://localhost:8282/services/camelcase/modify/remoteservice

You probably noticed a console output on startup that shows the Endpoint Description XML. This is actually what we need for the EDEF file. You can also get the endpoint description at runtime via the ECF Gogo Command listexports <endpoint.id>:

osgi> listexports
endpoint.id                          |Exporting Container ID                       |Exported Service Id
5918da3a-a971-429f-9ff6-87abc70d4742 |http://localhost:8282/services/camelcase     |38

osgi> listexports 5918da3a-a971-429f-9ff6-87abc70d4742
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
    <property name="ecf.endpoint.id" value-type="String" value="http://localhost:8282/services/camelcase"/>
    <property name="ecf.endpoint.id.ns" value-type="String" value="ecf.namespace.jaxrs"/>
    <property name="ecf.endpoint.ts" value-type="Long" value="1642667915518"/>
    <property name="ecf.jaxrs.server.pathPrefix" value-type="String" value="/camelcase"/>
    <property name="ecf.rsvc.id" value-type="Long" value="1"/>
    <property name="endpoint.framework.uuid" value-type="String" value="80778aff-63c7-448d-92a5-7902eb6782ae"/>
    <property name="endpoint.id" value-type="String" value="5918da3a-a971-429f-9ff6-87abc70d4742"/>
    <property name="endpoint.package.version.org.fipro.modifier" value-type="String" value="1.0.0"/>
    <property name="endpoint.service.id" value-type="Long" value="38"/>
    <property name="objectClass" value-type="String">
      <array>
        <value>org.fipro.modifier.StringModifier</value>
      </array>
    </property>
    <property name="remote.configs.supported" value-type="String">
      <array>
        <value>ecf.jaxrs.jersey.server</value>
      </array>
    </property>
    <property name="remote.intents.supported" value-type="String">
      <array>
        <value>passByValue</value>
        <value>exactlyOnce</value>
        <value>ordered</value>
        <value>osgi.async</value>
        <value>osgi.private</value>
        <value>osgi.confidential</value>
        <value>jaxrs</value>
      </array>
    </property>
    <property name="service.imported" value-type="String" value="true"/>
    <property name="service.imported.configs" value-type="String">
      <array>
        <value>ecf.jaxrs.jersey.server</value>
      </array>
    </property>
    <property name="service.intents" value-type="String">
      <array>
        <value>jaxrs</value>
      </array>
    </property>
  </endpoint-description>
</endpoint-descriptions>

The endpoint description is needed by the Service Consumer to discover the new service. Without a Discovery that is broadcasting, the service needs to be discovered statically via an EDEF file. As the EDEF file is registered via manifest header, we create a new bundle. You could also place it in an existing bundle like org.fipro.modifier.client, but for some more OSGi dynamics fun, let’s create a new bundle.

  • Create the EDEF configuration bundle project
    • File -> New -> Other… -> Maven -> Maven Module
    • Click Next
    • Check Create a simple project
    • Set Module Name: to client-edef
    • Select Parent Project: enroute
    • Click Next
    • Click Finish
  • Create a new folder edef
    • Right click on the source folder src/main/resources -> New -> Folder
    • Set Folder name to edef
    • Click Finish
  • Create a new file camelcase.xml in that folder
    • Right click on the edef folder -> New -> File
    • Set File name to camelcase.xml
    • Copy the Endpoint Description XML from the previous console command execution into that file
  • Create a new package edef
    • Right click on the source folder src/main/java -> New -> Package
    • Set Name to edef
    • Check Create package-info.java
    • Click Finish
  • Open src/main/java/edef/package-info.java
    • Add the Header OSGi Bundle Annotation to add the Remote-Service header to the OSGi metadata
@org.osgi.annotation.bundle.Header(name="Remote-Service", value="edef/camelcase.xml")
package edef;
  • Open the file client-edef/pom.xml
    • Add the following fragment after the artifactId
  <dependencies>
    <dependency>
      <groupId>org.osgi.enroute</groupId>
      <artifactId>osgi-api</artifactId>
      <type>pom</type>
    </dependency>
  </dependencies>

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

Note:
If you see an error on the project after the modification on the pom.xml file, execute a right-click on the project -> Maven -> Update Project… -> select the client-edef project or even all projects in the dialog and click Update.

  • Open the file client-app/pom.xml
    • Add the following snippet to the dependencies section
    <dependency>
        <groupId>org.fipro.modifier</groupId>
        <artifactId>client-edef</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
  • Open the file client-app/client-app.bndrun
    • Add org.fipro.modifier.client-edef to the Run Requirements
    • Save the changes
    • Click on Resolve to update the Run Bundles

If you start the Service Consumer Runtime, the service will directly be available. This is because the new org.fipro.modifier.client-edef bundle is activated automatically by the bnd launcher (a big difference compared to Equinox). Let’s deactivate it via the console. First we need to find the bundle-id via lb and then stop it via stop <bundle-id>. The output should look similar to the following snippet:

g! lb edef
START LEVEL 1
   ID|State      |Level|Name
   49|Active     |    1|client-edef (1.0.0.202202180929)|1.0.0.202202180929

g! stop 49

Now the service becomes unavailable via the modify command. If you start the bundle, the service becomes available again.

ECF Extensions to EDEF

The EDEF specification itself would not be sufficient for productive usage. For example, the values of the endpoint description properties need to match. For the endpoint.id this would be really problematic, as that value is a random generated uuid and changes on each runtime start. So if the Service Provider Runtime is restarted there is a new endpoint.id value. ECF includes a mechanism to support the discovery and the distribution even if the endpoint.id of the importer and the exporter do not match. This actually makes the EDEF file support work in productive environments.

ECF also provides a mechanism to create an endpoint description using a properties file. All the necessary endpoint description properties need to be included as properties with the respective types and values. The following example shows the properties representation for the EDEF XML of the above example. Note that for endpoint.id and endpoint.framework.uuid the type is set to uuid and the value is 0. This way ECF will generate a random UUID and the matching feature will ensure that the distribution will work even without matching id values.

ecf.endpoint.id=http://localhost:8282/services/camelcase
ecf.endpoint.id.ns=ecf.namespace.jaxrs
ecf.endpoint.ts:Long=1642761763599
ecf.jaxrs.server.pathPrefix=/camelcase
ecf.rsvc.id:Long=1
endpoint.framework.uuid:uuid=0
endpoint.id:uuid=0
endpoint.package.version.org.fipro.modifier.api=1.0.0
endpoint.service.id:Long=38
objectClass:array=org.fipro.modifier.api.StringModifier
remote.configs.supported:array=ecf.jaxrs.jersey.server
remote.intents.supported:array=passByValue,exactlyOnce,ordered,osgi.async,osgi.private,osgi.confidential,jaxrs
service.imported:boolean=true
service.imported.configs:array=ecf.jaxrs.jersey.server
service.intents:array=jaxrs

Properties files can be used to override values in an underlying XML EDEF file and even as an alternative, so the XML file is not needed anymore. It is even possible to override properties values for different environments, which makes it very interesting in a productive environment. So there can be a default Properties file for the basic endpoint description, then an endpoint description per service that derives from the basic settings, and even profile specific settings that changes for example the ecf.endpoint.id URLs per profile (DEV/INT/PROD). More details on that topic can be found in the ECF Wiki.

Alternatively you can also trigger a remote service import via EDEF programmatically using classes from the org.osgi.service.remoteserviceadmin package (see below). This way it is possible to dynamically import and close remote service registrations at runtime (without operating via low level OSGi bundle operations). The following snippet is an example for the programmatic registration of the service above:

Map<String, Object> properties = new HashMap<>();

properties.put("ecf.endpoint.id", "http://localhost:8282/services/camelcase");
properties.put("ecf.endpoint.id.ns", "ecf.namespace.jaxrs");
properties.put("ecf.endpoint.ts", 1642489801532l);
properties.put("ecf.jaxrs.server.pathPrefix", "/camelcase");
properties.put("ecf.rsvc.id", 1l);
properties.put("endpoint.framework.uuid", "0");
properties.put("endpoint.id", "0");
properties.put("endpoint.package.version.org.fipro.modifier.api", "1.0.0");
properties.put("endpoint.service.id", 38l);
properties.put("objectClass", new String[] { "org.fipro.modifier.api.StringModifier" });
properties.put("remote.configs.supported", new String[] { "ecf.jaxrs.jersey.server" });
properties.put("remote.intents.supported", new String[] { "passByValue", "exactlyOnce", "ordered", "osgi.async", "osgi.private", "osgi.confidential", "jaxrs" });
properties.put("service.imported", "true");
properties.put("service.intents", new String[] { "jaxrs" });
properties.put("service.imported.configs", new String[] { "ecf.jaxrs.jersey.server" });

EndpointDescription desc = new EndpointDescription(properties);
ImportRegistration importRegistration = admin.importService(desc);

Conclusion

The OSGi specification has several chapters and implementations to support a microservice architecture. The Remote Service and Remote Service Admin specifications are one of these and probably the most complicated ones, which was confirmed by several OSGi experts I talked with at conferences. Also the specification itself is not easy to understand, but I hope that this blog post helps to get a better understanding.

While Remote Services are pretty easy to implement, the complicated steps are in the setup of the runtime by collecting all necessary bundles. While the ECF project provides several examples and also tries to provide support for better bundle resolving, it is still not a trivial task. I hope this tutorial helps also in solving that topic a little bit.

Of course at runtime you might face networking issues, as I did in every talk for example. The typical fallacies are even referred in the Remote Service Specification. With the usage of JAX-RS and HTTP for the distribution of services and EDEF for a static file-based discovery, this might be less problematic. Give them a try if you are running into troubles.

At the end I again want to thank Scott Lewis for his continuous work on ECF and his support whenever I faced issues with my examples and had questions on some details. If you need an extension or if you have other requests regarding ECF or the JAX-RS Distribution Provider, like publishing the JAX-RS Distribution Provider on Maven Central and providing dependencies via pom.xml, please get in touch with him.

References

Updated: