Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "Tutorial: Creating a RESTful Remote Service Provider"

(Step 4: Using the New Provider)
(Provider Step 2: Creating the Distribution Provider for the Consumer)
 
(24 intermediate revisions by the same user not shown)
Line 88: Line 88:
 
<source lang="java">
 
<source lang="java">
 
Dictionary<String, String> props = new Hashtable<String, String>();
 
Dictionary<String, String> props = new Hashtable<String, String>();
// OSGi Standard Property - indicates which of the interfaces of the service will be exported.  '*' means 'all'.
 
 
props.put("service.exported.interfaces", "*");
 
props.put("service.exported.interfaces", "*");
// OSGi Standard Property (optional) - indicates which provider config(s) will be used to export the service
 
// (If not explicitly given here, the provider is free to choose a default configuration for the service)
 
 
props.put("service.exported.configs","ecf.generic.server");
 
props.put("service.exported.configs","ecf.generic.server");
// Register a new TimeServiceImpl with the above props
 
 
bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);  
 
bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);  
 
</source>
 
</source>
  
Note the line:
+
Note specifically the line
  
 
<source lang="java">
 
<source lang="java">
Line 103: Line 99:
 
</source>
 
</source>
  
This is one of the ECF-provided distribution providers, identified by as the config type "ecf.generic.server".  This provider is a general provider, capable of supporting the export of any service.
+
This is one of the ECF-provided distribution providers, identified by as the config type '''ecf.generic.server'''.  This provider is a general provider, capable of supporting the export of any remote service.
  
It's quite possible, however, to create a replacement provider, based upon our own http+rest+json transport protocol.  The remainder of this tutorial will step through how to create and run your own provider.
+
With ECF's implementation of OSGi Remote Services/RSA, it's quite possible to create a custom/replacement provider, based upon a '''new or existing''' transport.  The remainder of this tutorial will step through how to create and run your own provider using a simple http+rest+json transport.
  
==Provider Step 1: Implement New Provider Namespace==
+
==Step 1: Creating the Distribution Provider for the Remote Service Host==
  
ECF allows providers to create their own namespaces, so that the endpoint id of the remote service can be properly interpreted. Here is a TimeServiceRestNamespace class
+
Since we would like to access this service via http/https+rest+json, we will create a Servlet to actually handle the request...and dynamically register it via the OSGi HttpService.  That way we may implement the remote '''ITimeService.getCurrentTime()'' method by doing the appropriate http GET request which will be handled by our implementation.   Here is the complete implementation of the '''TimeRemoteServiceHttpServlet''' as well as the required ECF remote service container code
  
 
<source lang="java">
 
<source lang="java">
public class TimeServiceRestNamespace extends RestNamespace {
 
 
public static final String NAME = "com.mycorp.examples.timeservice.provider.rest.namespace";
 
 
@Override
 
public ID createInstance(Object[] parameters) throws IDCreateException {
 
return new TimeServiceRestID(this, URI.create((String) parameters[0]));
 
}
 
 
public static class TimeServiceRestID extends RestID {
 
 
public TimeServiceRestID(Namespace namespace, URI uri) {
 
super(namespace, uri);
 
}
 
}
 
}
 
</source>
 
 
As can be seen above, when the createInstance call is made a new instance of TimeServiceRestID is created from the single String parameter[0] passed into the createInstance call.  Along with this class, we can define an extension in the plugin.xml of this bundle to allow this namespace to be used at the appropriate point.
 
 
<source lang="xml">
 
  <extension
 
        point="org.eclipse.ecf.identity.namespace">
 
      <namespace
 
            class="com.mycorp.examples.timeservice.provider.rest.common.TimeServiceRestNamespace"
 
            description="Timeservice Rest Namespace"
 
            name="com.mycorp.examples.timeservice.provider.rest.namespace">
 
      </namespace>
 
  </extension>
 
</source>
 
 
Since this Namespace/ID is needed for both the host and the consumer providers, it should be placed in a separate bundle.  The complete bundle is available via the ECF git repo with project (and symbolic) name [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.common com.mycorp.examples.timeservice.provider.rest.common].
 
 
Note that the package with this new Namespace/ID class must be exported by having this in the manifest.mf:
 
 
<source lang="java">
 
Export-Package: com.mycorp.examples.timeservice.provider.rest.common
 
</source>
 
 
==Provider Step 2: Creating the Provider Host Implementation==
 
 
Since we would like to interact with this via http+rest, we will create a Servlet to actually handle the request...and register it dynamically to the OSGi standard HttpService.  That way we can implement the ITimeService.getCurrentTime() method by implementing the HttpServlet.doGet method.  Here is the complete implementation of the TimeRemoteServiceHttpServlet as well as the necessary ECF container code
 
 
<source lang="java">
 
package com.mycorp.examples.timeservice.internal.provider.rest.host;
 
 
import java.io.IOException;
 
 
import javax.servlet.ServletException;
 
import javax.servlet.http.HttpServletRequest;
 
import javax.servlet.http.HttpServletResponse;
 
 
import org.eclipse.ecf.core.ContainerCreateException;
 
import org.eclipse.ecf.core.identity.IDFactory;
 
import org.eclipse.ecf.core.identity.Namespace;
 
import org.eclipse.ecf.remoteservice.servlet.HttpServiceComponent;
 
import org.eclipse.ecf.remoteservice.servlet.RemoteServiceHttpServlet;
 
import org.eclipse.ecf.remoteservice.servlet.ServletServerContainer;
 
import org.json.JSONException;
 
import org.json.JSONObject;
 
import org.osgi.service.http.HttpService;
 
 
import com.mycorp.examples.timeservice.ITimeService;
 
import com.mycorp.examples.timeservice.provider.rest.common.TimeServiceRestNamespace;
 
 
 
public class TimeServiceServerContainer extends ServletServerContainer {
 
public class TimeServiceServerContainer extends ServletServerContainer {
  
Line 182: Line 113:
 
public static final String TIMESERVICE_SERVLET_NAME = "/" + ITimeService.class.getName();
 
public static final String TIMESERVICE_SERVLET_NAME = "/" + ITimeService.class.getName();
  
private final HttpService httpService;
+
TimeServiceServerContainer(ID id) throws ServletException, NamespaceException {
+
super(id);
TimeServiceServerContainer(String id, HttpService httpService) throws ContainerCreateException {
+
// Register our servlet with the given httpService with the
super(IDFactory.getDefault()
+
// TIMESERVICE_SERVLET_NAME
.createID(TimeServiceRestNamespace.NAME, id));
+
this.httpService = httpService;
+
// Register our servlet with the given httpService with the TIMESERVICE_SERVLET_NAME
+
 
// which is "/com.mycorp.examples.timeservice.ITimeService"
 
// which is "/com.mycorp.examples.timeservice.ITimeService"
try {
+
TimeServiceHttpServiceComponent.getDefault().registerServlet(TIMESERVICE_SERVLET_NAME,
this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
+
 
new TimeRemoteServiceHttpServlet(), null, null);
 
new TimeRemoteServiceHttpServlet(), null, null);
} catch (Exception e) {
 
throw new ContainerCreateException("Could not create Time Service Server Container",e);
 
}
 
 
}
 
}
  
 
@Override
 
@Override
 
public void dispose() {
 
public void dispose() {
httpService.unregister(TIMESERVICE_SERVLET_NAME);
+
TimeServiceHttpServiceComponent.getDefault().unregisterServlet(TIMESERVICE_SERVLET_NAME);
 
super.dispose();
 
super.dispose();
 
}
 
}
Line 206: Line 130:
 
@Override
 
@Override
 
public Namespace getConnectNamespace() {
 
public Namespace getConnectNamespace() {
return IDFactory.getDefault().getNamespaceByName(
+
return RestNamespace.INSTANCE;
TimeServiceRestNamespace.NAME);
+
 
}
 
}
  
Line 213: Line 136:
  
 
private static final long serialVersionUID = 3906126401901826462L;
 
private static final long serialVersionUID = 3906126401901826462L;
 
+
// Handle remote time service get call here.
// Handle get call right here.
+
 
@Override
 
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
throws ServletException, IOException {
+
+
// No arguments to getCurrentTime() method, so
+
// nothing to deserialize from request
+
+
 
// Get local OSGi ITimeService
 
// Get local OSGi ITimeService
ITimeService timeService = HttpServiceComponent.getDefault()
+
ITimeService timeService = HttpServiceComponent.getDefault().getService(ITimeService.class);
.getService(ITimeService.class);
+
// Call local service to get the current time
+
// Call local service to get the time
+
 
Long currentTime = timeService.getCurrentTime();
 
Long currentTime = timeService.getCurrentTime();
+
// Serialize response and send as http response
// Serialize response
+
try {
    try {
+
 
resp.getOutputStream().print(new JSONObject().put("time", currentTime).toString());
 
resp.getOutputStream().print(new JSONObject().put("time", currentTime).toString());
 
} catch (JSONException e) {
 
} catch (JSONException e) {
Line 237: Line 151:
 
}
 
}
 
}
 
}
 +
 +
public static class Instantiator extends RemoteServiceContainerInstantiator {
 +
@Override
 +
public IContainer createInstance(ContainerTypeDescription description, Map<String, ?> parameters)
 +
throws ContainerCreateException {
 +
try {
 +
return new TimeServiceServerContainer(
 +
RestNamespace.INSTANCE.createInstance(new Object[] { (String) parameters.get("id") }));
 +
} catch (Exception e) {
 +
throw new ContainerCreateException("Could not create time service server", e);
 +
}
 +
}
  
 +
public String[] getSupportedConfigs(ContainerTypeDescription description) {
 +
return new String[] { TIMESERVICE_HOST_CONFIG_NAME };
 +
}
 +
}
 
}
 
}
 
</source>
 
</source>
  
In the TimeServiceServerContainer constructor a new instance of the TimeRemoteServiceHttpServlet is created and registered as a servlet with the HttpService
+
In the '''TimeServiceServerContainer''' constructor a new instance of the '''TimeRemoteServiceHttpServlet''' is created and registered as a servlet via an HttpService
  
 
<source lang="java">
 
<source lang="java">
this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
+
TimeServiceHttpServiceComponent.getDefault().registerServlet(TIMESERVICE_SERVLET_NAME,
new TimeRemoteServiceHttpServlet(), null, null);
+
        new TimeRemoteServiceHttpServlet(), null, null);
 
</source>
 
</source>
  
Note that the TIMESERVICE_SERVLET_NAME is defined to be "/com.mycorp.examples.timeservice.ITimeService".  Since this is used to register the servlet this means that this service will have an URL that is (for example):
+
Note that the TIMESERVICE_SERVLET_NAME is defined in this example to be '''/com.mycorp.examples.timeservice.ITimeService'''.  Since this is the path associated with the Servlet this means that the remote service may be accessed via a GET request to the following URL:
  
 
<source lang="java">
 
<source lang="java">
Line 254: Line 184:
 
</source>
 
</source>
  
Other paths can obviously be specified as desired.
+
All of the actual behavior to handle the call of the remote service is in the doGet method implementation in the '''TimeRemoteServiceHttpServlet'''
 
+
Also present are overrides of two ECF container lifecycle methods
+
 
+
<source lang="java">
+
public Namespace getConnectNamespace() {
+
return IDFactory.getDefault().getNamespaceByName(
+
TimeServiceRestNamespace.NAME);
+
}
+
</source>
+
 
+
which references the TimeServiceRestNamespace to associate this remote service container with the TimeServiceRestNamespace we created above.
+
 
+
<source lang="java">
+
public void dispose() {
+
httpService.unregister(TIMESERVICE_SERVLET_NAME);
+
super.dispose();
+
}
+
</source>
+
 
+
which dynamically unregisters the servlet when the container instance is disposed.
+
 
+
All of the actual behavior is given by the doGet method implementation in the TimeRemoteServiceHttpServlet
+
  
 
<source lang="java">
 
<source lang="java">
Line 300: Line 208:
 
</source>
 
</source>
  
All that's happening here is that
+
What's happening
  
#1 The local ITimeService implementation is retrieved
+
# The local ITimeService host implementation is retrieved via HttpServiceComponent.getDefault().getService(ITimeService.class);
#2 The ITimeService.getCurrentTime() method is called to get the local time (Long)
+
# The ITimeService.getCurrentTime() method is called to get the local time, which returns and instance of type Long
#3 The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream.
+
# The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream to complete the HttpResponse.
  
The next thing to do is to implement a ECF container instantiator, which is responsible for creating and using instances of TimeServiceServerContainer under the appropriate OSGi Remote Service conditions.  Here is the container instantiator class:
+
Other than this container class, the only thing left to do is to register a remote service distribution provider as a whiteboard service, which is responsible for creating and using instances of '''TimeServiceServerContainer''' under the appropriate runtime conditions.  Here is the provider registration code (in the bundle Activator class):
  
 
<source lang="java">
 
<source lang="java">
public class TimeServiceServerContainerInstantiator extends
+
public class Activator implements BundleActivator {
ServletServerContainerInstantiator {
+
  
public IContainer createInstance(ContainerTypeDescription description,
+
public void start(final BundleContext context) throws Exception {
Object[] parameters) throws ContainerCreateException {
+
context.registerService(IRemoteServiceDistributionProvider.class,
try {
+
new RemoteServiceDistributionProvider.Builder()
// Get HttpServices
+
.setName(TimeServiceServerContainer.TIMESERVICE_HOST_CONFIG_NAME)
Collection<HttpService> httpServices = TimeServiceHttpServiceComponent.getDefault().getHttpServices();
+
.setInstantiator(new TimeServiceServerContainer.Instantiator()).build(),
if (httpServices == null || httpServices.size() == 0) throw new NullPointerException("Cannot get HttpService for TimeServiceServerContainer creation");
+
null);
// If we've got more than one, then we'll just use the first one
+
HttpService httpService = httpServices.iterator().next();
+
@SuppressWarnings("unchecked")
+
Map<String, Object> map = (Map<String, Object>) parameters[0];
+
String id = (String) map.get("id");
+
return new TimeServiceServerContainer(IDFactory.getDefault()
+
.createID(TimeServiceRestNamespace.NAME, id), httpService);
+
} catch (Exception e) {
+
throw new ContainerCreateException(
+
"Could not create TimeServiceServerContainer", e);
+
}
+
}
+
 
+
public String[] getSupportedAdapterTypes(
+
ContainerTypeDescription description) {
+
return getInterfacesAndAdaptersForClass(TimeServiceServerContainer.class);
+
}
+
 
+
public String[] getSupportedConfigs(ContainerTypeDescription description) {
+
return new String[] { TimeServiceServerContainer.NAME };
+
 
}
 
}
 
}
 
}
 
</source>
 
</source>
  
The createInstance method selects an HttpService to use in the TimeServiceServerContainer constructor, and the "id" parameter is used to define the TimeServiceServerContainer ID. The host container ID corresponds to the '''http://localhost:8080''' in the remote service URL given above.
+
This code registers a new instance of IRemoteServiceDistributionProvider, setting up the relationship between the host config name "com.mycorp.examples.timeservice.rest.host", and the TimeServiceServerContainer.Instantiator(), which will then be called by ECF RSA at export time to create an instance of the TimeServiceServerContainer.
  
Note also the '''getSupportedConfigs''' method.  This method allows providers to define a String[] of config types that they support.  This is used by the ECF RSA implementation to determine at remote service export time exactly which remote service containers to create and use to export the remote service.
+
==Provider Step 2: Creating the Distribution Provider for the Remote Service Consumer==
 
+
This container instantiator class can then be declared as an extension via plugin.xml markup:
+
 
+
<source lang="xml">
+
  <extension
+
        point="org.eclipse.ecf.containerFactory">
+
      <containerFactory
+
            class="com.mycorp.examples.timeservice.internal.provider.rest.host.TimeServiceServerContainerInstantiator"
+
            hidden="false"
+
            name="com.mycorp.examples.timeservice.rest.host"
+
            server="true">
+
      </containerFactory>
+
  </extension>
+
</source>
+
 
+
Note that the 'name' attribute ('''com.mycorp.examples.timeservice.rest.host''') must match the value defined in the code for '''TimeServiceServerContainer.NAME'''.
+
 
+
Finally, the HttpService is injected into the '''TimeServiceHttpServiceComponent''' via OSGi Declarative Services.  See the [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.host/OSGI-INF/httpservicecomponent.xml standard DS xml for that component here].  Here is the entire host provider implementation in [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.host com.mycorp.examples.timeservice.rest.host bundle project].
+
 
+
==Provider Step 3: Creating the Consumer Provider Implementation==
+
  
 
Here is the consumer remote service container provider implementation
 
Here is the consumer remote service container provider implementation
Line 372: Line 239:
  
 
public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
 
public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
 +
private static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
  
 
private IRemoteServiceRegistration reg;
 
private IRemoteServiceRegistration reg;
  
TimeServiceRestClientContainer() {
+
TimeServiceRestClientContainer(RestID id) {
// Create a random ID for the client container
+
super(id);
super((RestID) IDFactory.getDefault().createID(
+
TimeServiceRestNamespace.NAME, "uuid:"
+
+ java.util.UUID.randomUUID().toString()));
+
 
// This sets up the JSON deserialization of the server's response.
 
// This sets up the JSON deserialization of the server's response.
 
// See below for implementation of TimeServiceRestResponseDeserializer
 
// See below for implementation of TimeServiceRestResponseDeserializer
setResponseDeserializer(new TimeServiceRestResponseDeserializer());
+
setResponseDeserializer(new IRemoteResponseDeserializer() {
 +
public Object deserializeResponse(String endpoint, IRemoteCall call, IRemoteCallable callable,
 +
@SuppressWarnings("rawtypes") Map responseHeaders, byte[] responseBody) throws NotSerializableException {
 +
try {
 +
return new JSONObject(new String(responseBody)).get("time");
 +
} catch (JSONException e1) {
 +
NotSerializableException t = new NotSerializableException("Exception serializing response from endpoing="+endpoint);
 +
t.setStackTrace(e1.getStackTrace());
 +
throw t;
 +
}
 +
}});
 
}
 
}
  
public void connect(ID targetID, IConnectContext connectContext1)
+
@Override
throws ContainerConnectException {
+
public void connect(ID targetID, IConnectContext connectContext1) throws ContainerConnectException {
 
super.connect(targetID, connectContext1);
 
super.connect(targetID, connectContext1);
 
// Create the IRemoteCallable to represent
 
// Create the IRemoteCallable to represent
// access to the ITimeService method.
+
// access to the ITimeService method.
IRemoteCallable callable = RestCallableFactory.createCallable(
+
IRemoteCallable callable = RestCallableFactory.createCallable("getCurrentTime", ITimeService.class.getName(),
"getCurrentTime", ITimeService.class.getName(), null,
+
null, new HttpGetRequestType(), 30000);
new HttpGetRequestType(), 30000);
+
 
// Register the callable and associate it with the ITimeService class
 
// Register the callable and associate it with the ITimeService class
 
// name
 
// name
reg = registerCallables(new String[] { ITimeService.class.getName() },
+
reg = registerCallables(new String[] { ITimeService.class.getName() }, new IRemoteCallable[][] { { callable } },
new IRemoteCallable[][] { { callable } }, null);
+
null);
 
}
 
}
  
 +
@Override
 
public void disconnect() {
 
public void disconnect() {
 
super.disconnect();
 
super.disconnect();
Line 407: Line 282:
 
}
 
}
  
class TimeServiceRestResponseDeserializer implements
+
@Override
IRemoteResponseDeserializer {
+
public Namespace getConnectNamespace() {
public Object deserializeResponse(String endpoint, IRemoteCall call,
+
return RestNamespace.INSTANCE;
IRemoteCallable callable,
+
@SuppressWarnings("rawtypes") Map responseHeaders,
+
byte[] responseBody) throws NotSerializableException {
+
// We simply need to read the response body (json String),
+
// And return the value of the "time" field
+
try {
+
return new JSONObject(new String(responseBody)).get("time");
+
} catch (JSONException e1) {
+
throw new NotSerializableException(
+
TimeServiceRestResponseDeserializer.class.getName());
+
}
+
}
+
 
+
 
}
 
}
  
public Namespace getConnectNamespace() {
+
public static class Instantiator extends RemoteServiceContainerInstantiator {
return IDFactory.getDefault().getNamespaceByName(
+
 
TimeServiceRestNamespace.NAME);
+
@Override
 +
public IContainer createInstance(ContainerTypeDescription description, Map<String, ?> parameters)
 +
throws ContainerCreateException {
 +
// Create new container instance with random uuid
 +
return new TimeServiceRestClientContainer((RestID) RestNamespace.INSTANCE
 +
.createInstance(new Object[] { "uuid:" + UUID.randomUUID().toString() }));
 +
}
 +
public String[] getImportedConfigs(ContainerTypeDescription description, String[] exporterSupportedConfigs) {
 +
if (Arrays.asList(exporterSupportedConfigs).contains(TIMESERVICE_HOST_CONFIG_NAME))
 +
return new String[] { TimeServiceRestClientContainer.TIMESERVICE_CONSUMER_CONFIG_NAME };
 +
else
 +
return null;
 +
}
 
}
 
}
 
}
 
}
 
</source>
 
</source>
 
First, as with the host, the getConnectNamespace override uses/refers to the '''TimeServiceRestNamespace.NAME''' definition.  This associates this consumer provider with the host provider.
 
  
 
The '''TimeServiceRestClientContainer''' constructor first creates a unique id for itself and then it sets up a json response deserializer (for handling the json result of getCurrentTime()) with this code:
 
The '''TimeServiceRestClientContainer''' constructor first creates a unique id for itself and then it sets up a json response deserializer (for handling the json result of getCurrentTime()) with this code:
  
 
<source lang="java">
 
<source lang="java">
// This sets up the JSON deserialization of the server's response.
+
setResponseDeserializer(new IRemoteResponseDeserializer() {
// See below for implementation of TimeServiceRestResponseDeserializer
+
public Object deserializeResponse(String endpoint, IRemoteCall call, IRemoteCallable callable,
setResponseDeserializer(new TimeServiceRestResponseDeserializer());
+
@SuppressWarnings("rawtypes") Map responseHeaders, byte[] responseBody) throws NotSerializableException {
 +
try {
 +
return new JSONObject(new String(responseBody)).get("time");
 +
} catch (JSONException e1) {
 +
NotSerializableException t = new NotSerializableException("Exception serializing response from endpoing="+endpoint);
 +
t.setStackTrace(e1.getStackTrace());
 +
throw t;
 +
}
 +
}});
 
</source>
 
</source>
  
When the response is received, the '''TimeServiceRestResponseDeserializer.deserializeResponse''' method will be called, and this code then parses the json from the host
+
When the response is received, the '''TimeServiceRestResponseDeserializer.deserializeResponse''' method will be called, and from above this code then parses the json from the host
  
 
<source lang="java">
 
<source lang="java">
Line 452: Line 332:
 
The disconnect method simply unregisters the '''IRemoteCallable'''.
 
The disconnect method simply unregisters the '''IRemoteCallable'''.
  
As for the host remote service container, a container instantiator must be created for the TimeServiceRestClientContainer
+
As for the host remote service container, a container Instantiator must be created for the TimeServiceRestClientContainer and that is used by ECF RSA to create an instance of the TimeServiceRestClientContainer at the appropriate time.
  
<source lang="java">
+
Note the '''getImportedConfigs''' method in the Instantiator, which is automatically called by the ECF Remote Service implementation in order to allow the provider to convey that the TimeServiceRestClientContainer should be used for importing when the '''TIMESERVICE_HOST_CONFIG_NAME''' i.e. '''com.mycorp.examples.timeservice.rest.host'''.
public class TimeServiceRestClientContainerInstantiator extends
+
RestClientContainerInstantiator {
+
  
private static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
+
This remote service container instantiator is then declared as a IRemoteServiceDistributionProvider in the activator
  
@Override
+
<source lang="xml">
public IContainer createInstance(ContainerTypeDescription description,
+
public class Activator implements BundleActivator {
Object[] parameters) throws ContainerCreateException {
+
 
// Create new container instance
+
public void start(final BundleContext context) throws Exception {
return new TimeServiceRestClientContainer();
+
context.registerService(IRemoteServiceDistributionProvider.class,
 +
new RemoteServiceDistributionProvider.Builder()
 +
.setName(TimeServiceRestClientContainer.TIMESERVICE_CONSUMER_CONFIG_NAME)
 +
.setInstantiator(new TimeServiceRestClientContainer.Instantiator()).build(),
 +
null);
 
}
 
}
  
public String[] getImportedConfigs(ContainerTypeDescription description,
+
public void stop(BundleContext context) throws Exception {
String[] exporterSupportedConfigs) {
+
@SuppressWarnings("rawtypes")
+
List supportedConfigs = Arrays.asList(exporterSupportedConfigs);
+
// If the supportedConfigs contains the timeservice host config,
+
// then we are the client to handle it!
+
if (supportedConfigs.contains(TIMESERVICE_HOST_CONFIG_NAME))
+
return new String[] { TimeServiceRestClientContainer.TIMESERVICE_CONSUMER_CONFIG_NAME };
+
else return null;
+
 
}
 
}
 
 
}
 
}
 
</source>
 
</source>
  
Note the '''getImportedConfigs''' method, which is automatically called by the ECF Remote Service implementation in order to allow the provider to convey that the TimeServiceRestClientContainer should be used for importing when the '''TIMESERVICE_HOST_CONFIG_NAME''' i.e. '''com.mycorp.examples.timeservice.rest.host'''.
+
This completes the consumer provider.  The source for the complete bundle is in the [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.consumer com.mycorp.examples.timeservice.provider.rest.consumer bundle project].
 
+
This remote service container instantiator is then declared using an extension point in the plugin.xml
+
 
+
<source lang="xml">
+
  <extension
+
        point="org.eclipse.ecf.containerFactory">
+
      <containerFactory
+
            class="com.mycorp.examples.timeservice.internal.provider.rest.consumer.TimeServiceRestClientContainerInstantiator"
+
            hidden="false"
+
            name="com.mycorp.examples.timeservice.rest.consumer"
+
            server="false">
+
      </containerFactory>
+
  </extension>
+
</source>
+
 
+
And that completes the consumer provider.  The source for the complete bundle is in the [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.consumer com.mycorp.examples.timeservice.provider.rest.consumer bundle project].
+
  
==Step 4:  Using the New Provider==
+
==Using the New Provider==
  
Now we have a completed host provider, and a completed consumer provider for the restful timeservice.  These two providers are entirely represented by the three bundles
+
Now we have a completed host provider, and a completed consumer provider for the restful timeservice.  These two providers are entirely represented by the two bundles
  
#[http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.common com.mycorp.examples.timeservice.provider.rest.common]
 
 
#[http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.host com.mycorp.examples.timeservice.provider.rest.host]
 
#[http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.host com.mycorp.examples.timeservice.provider.rest.host]
 
#[http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.consumer com.mycorp.examples.timeservice.provider.rest.consumer]
 
#[http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.consumer com.mycorp.examples.timeservice.provider.rest.consumer]
  
With these three bundles and their dependencies present in a runtime, the following may now be used to export a remote service using the host provider
+
With these bundles and their dependencies, the following may now be used to export a remote service using the host provider
  
 
<source lang="java">
 
<source lang="java">
Line 515: Line 371:
 
props.put("service.exported.configs","com.mycorp.examples.timeservice.rest.host");
 
props.put("service.exported.configs","com.mycorp.examples.timeservice.rest.host");
 
// Specify the 'id' parameter for the ID creation of the host (see  
 
// Specify the 'id' parameter for the ID creation of the host (see  
// the TimeServiceServerContainerInstantiator.createInstance method above
+
// the TimeServiceServerContainerInstantiator.createInstance method  
 
props.put("com.mycorp.examples.timeservice.rest.host.id","http://localhost:8181");
 
props.put("com.mycorp.examples.timeservice.rest.host.id","http://localhost:8181");
 
// Register a new TimeServiceImpl with the above props
 
// Register a new TimeServiceImpl with the above props
Line 524: Line 380:
  
 
#Detects that the "service.exported.interfaces" property is set and so the service is to be exported
 
#Detects that the "service.exported.interfaces" property is set and so the service is to be exported
#Detects that the "service.exported.configs" property is set, and selects the container instantiator that returns a matching value from a call to '''getSupportedConfigs'''
+
#Detects that the "service.exported.configs" property is set, and selects the container Instantiator that returns a matching value from a call to '''getSupportedConfigs'''
 
#Creates a new instance of '''TimeServiceServerContainer''' by calling the approprate container instantiator's createInstance method, and passes in a Map of appropriate service properties (i.e. '''com.mycorp.examples.timeservice.rest.host.id''').
 
#Creates a new instance of '''TimeServiceServerContainer''' by calling the approprate container instantiator's createInstance method, and passes in a Map of appropriate service properties (i.e. '''com.mycorp.examples.timeservice.rest.host.id''').
 
#Uses the created remote service container to export the remote service
 
#Uses the created remote service container to export the remote service
Line 574: Line 430:
 
</source>
 
</source>
  
Note that both the consumer code and the host code (except for the service property values on the host) are exactly the same.  This makes it possible to develop and test services independent of the underlying providers being used.
+
Note that both the consumer code and the host code (except for the service property values on the host) are exactly the same.  This makes it possible to develop, test, and deploy remote services independent of the underlying distribution providers being used.
 +
 
 +
==Background and Related Articles==
 +
 
 +
[[Getting Started with ECF's OSGi Remote Services Implementation]]
 +
 
 +
[[OSGi Remote Services and ECF]]
 +
 
 +
[[Asynchronous Proxies for Remote Services]]
 +
 
 +
[[File-based Discovery | Static File-based Discovery of Remote Service Endpoints]]
 +
 
 +
[[EIG:Download|Download ECF Remote Services/RSA Implementation]]
 +
 
 +
[[EIG:Add to Target Platform|How to Add Remote Services/RSA to Your Target Platform]]
 +
 
 +
[[ECF#Customization_of_ECF_Remote_Services_.28new_Discovery_and.2For_Distribution_providers.29 | Customization of ECF Remote Services]]

Latest revision as of 12:28, 8 September 2015


Introduction

In previous tutorials we've focused on how to use OSGi Remote Services to export a simple ITimeService and have a consumer use this service.

This tutorial will focus on customizing ECF's implementation of OSGi Remote Services to use a custom distribution provider...aka a Remote Service provider. Creating a custom Remote Service provider is tantamount to creating your own implementation of the distribution function of OSGi Remote Services.

Why would one want to do this? One reason is interoperability and integration. There are many existing services on the Internet currently...exposed through many different protocols (e.g. http+json, http+xml, JMS+json, etc., etc.). By creating a new remote service provider, it's quite possible to take an existing service, implemented via an existing transport+serialization approach, and easily expose it as an OSGi Remote Service...while allowing existing services to run unmodified.

Another reason is that in the remoting of services, there are frequently non-functional requirements...e.g. for a certain kind of transport-level security, or using an existing protocol (e.g. http), or using a certain kind of serialization (e.g. json, or xml, or protocol buffers). By creating a new distribution/remote service provider, these requirements can be met...simply by using the ECF provider architecture...rather than being required to reimplement all aspects of OSGi Remote Services/RSA.

Exporting Using a Config Type

As shown in Tutorial:_Building_your_first_OSGi_Remote_Service, here is the remote service metadata needed to export a remote service using the ECF generic server distribution provider

Dictionary<String, String> props = new Hashtable<String, String>();
props.put("service.exported.interfaces", "*");
props.put("service.exported.configs","ecf.generic.server");
bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);

Note specifically the line

props.put("service.exported.configs","ecf.generic.server");

This is one of the ECF-provided distribution providers, identified by as the config type ecf.generic.server. This provider is a general provider, capable of supporting the export of any remote service.

With ECF's implementation of OSGi Remote Services/RSA, it's quite possible to create a custom/replacement provider, based upon a new or existing transport. The remainder of this tutorial will step through how to create and run your own provider using a simple http+rest+json transport.

Step 1: Creating the Distribution Provider for the Remote Service Host

Since we would like to access this service via http/https+rest+json, we will create a Servlet to actually handle the request...and dynamically register it via the OSGi HttpService. That way we may implement the remote ITimeService.getCurrentTime() method by doing the appropriate http GET request which will be handled by our implementation. Here is the complete implementation of the TimeRemoteServiceHttpServlet' as well as the required ECF remote service container code

public class TimeServiceServerContainer extends ServletServerContainer {
 
	public static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
	public static final String TIMESERVICE_SERVLET_NAME = "/" + ITimeService.class.getName();
 
	TimeServiceServerContainer(ID id) throws ServletException, NamespaceException {
		super(id);
		// Register our servlet with the given httpService with the
		// TIMESERVICE_SERVLET_NAME
		// which is "/com.mycorp.examples.timeservice.ITimeService"
		TimeServiceHttpServiceComponent.getDefault().registerServlet(TIMESERVICE_SERVLET_NAME,
					new TimeRemoteServiceHttpServlet(), null, null);
	}
 
	@Override
	public void dispose() {
		TimeServiceHttpServiceComponent.getDefault().unregisterServlet(TIMESERVICE_SERVLET_NAME);
		super.dispose();
	}
 
	@Override
	public Namespace getConnectNamespace() {
		return RestNamespace.INSTANCE;
	}
 
	class TimeRemoteServiceHttpServlet extends RemoteServiceHttpServlet {
 
		private static final long serialVersionUID = 3906126401901826462L;
		// Handle remote time service get call here.
		@Override
		protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
			// Get local OSGi ITimeService
			ITimeService timeService = HttpServiceComponent.getDefault().getService(ITimeService.class);
			// Call local service to get the current time
			Long currentTime = timeService.getCurrentTime();
			// Serialize response and send as http response
			try {
				resp.getOutputStream().print(new JSONObject().put("time", currentTime).toString());
			} catch (JSONException e) {
				throw new ServletException("json response object could not be created for time service", e);
			}
		}
	}
 
	public static class Instantiator extends RemoteServiceContainerInstantiator {
		@Override
		public IContainer createInstance(ContainerTypeDescription description, Map<String, ?> parameters)
				throws ContainerCreateException {
			try {
				return new TimeServiceServerContainer(
						RestNamespace.INSTANCE.createInstance(new Object[] { (String) parameters.get("id") }));
			} catch (Exception e) {
				throw new ContainerCreateException("Could not create time service server", e);
			}
		}
 
		public String[] getSupportedConfigs(ContainerTypeDescription description) {
			return new String[] { TIMESERVICE_HOST_CONFIG_NAME };
		}
	}
}

In the TimeServiceServerContainer constructor a new instance of the TimeRemoteServiceHttpServlet is created and registered as a servlet via an HttpService

TimeServiceHttpServiceComponent.getDefault().registerServlet(TIMESERVICE_SERVLET_NAME,
        new TimeRemoteServiceHttpServlet(), null, null);

Note that the TIMESERVICE_SERVLET_NAME is defined in this example to be /com.mycorp.examples.timeservice.ITimeService. Since this is the path associated with the Servlet this means that the remote service may be accessed via a GET request to the following URL:

http://localhost:8080/com.mycorp.examples.timeservice.ITimeService

All of the actual behavior to handle the call of the remote service is in the doGet method implementation in the TimeRemoteServiceHttpServlet

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException {
 
	// 1. Get local OSGi ITimeService
	ITimeService timeService = HttpServiceComponent.getDefault()
			.getService(ITimeService.class);
 
	// 2. Call local service to get the time
	Long currentTime = timeService.getCurrentTime();
 
	// 3. Serialize response
        try {
		resp.getOutputStream().print(new JSONObject().put("time", 
                                              currentTime).toString());
	} catch (JSONException e) {
		throw new ServletException("json response object could not be created for                    
                                            time service", e);
	}
}

What's happening

  1. The local ITimeService host implementation is retrieved via HttpServiceComponent.getDefault().getService(ITimeService.class);
  2. The ITimeService.getCurrentTime() method is called to get the local time, which returns and instance of type Long
  3. The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream to complete the HttpResponse.

Other than this container class, the only thing left to do is to register a remote service distribution provider as a whiteboard service, which is responsible for creating and using instances of TimeServiceServerContainer under the appropriate runtime conditions. Here is the provider registration code (in the bundle Activator class):

public class Activator implements BundleActivator {
 
	public void start(final BundleContext context) throws Exception {
		context.registerService(IRemoteServiceDistributionProvider.class,
				new RemoteServiceDistributionProvider.Builder()
						.setName(TimeServiceServerContainer.TIMESERVICE_HOST_CONFIG_NAME)
						.setInstantiator(new TimeServiceServerContainer.Instantiator()).build(),
				null);
	}
}

This code registers a new instance of IRemoteServiceDistributionProvider, setting up the relationship between the host config name "com.mycorp.examples.timeservice.rest.host", and the TimeServiceServerContainer.Instantiator(), which will then be called by ECF RSA at export time to create an instance of the TimeServiceServerContainer.

Provider Step 2: Creating the Distribution Provider for the Remote Service Consumer

Here is the consumer remote service container provider implementation

public class TimeServiceRestClientContainer extends RestClientContainer {
 
	public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
	private static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
 
	private IRemoteServiceRegistration reg;
 
	TimeServiceRestClientContainer(RestID id) {
		super(id);
		// This sets up the JSON deserialization of the server's response.
		// See below for implementation of TimeServiceRestResponseDeserializer
		setResponseDeserializer(new IRemoteResponseDeserializer() {
			public Object deserializeResponse(String endpoint, IRemoteCall call, IRemoteCallable callable,
					@SuppressWarnings("rawtypes") Map responseHeaders, byte[] responseBody) throws NotSerializableException {
				try {
					return new JSONObject(new String(responseBody)).get("time");
				} catch (JSONException e1) {
					NotSerializableException t = new NotSerializableException("Exception serializing response from endpoing="+endpoint);
					t.setStackTrace(e1.getStackTrace());
					throw t;
				}
			}});
	}
 
	@Override
	public void connect(ID targetID, IConnectContext connectContext1) throws ContainerConnectException {
		super.connect(targetID, connectContext1);
		// Create the IRemoteCallable to represent
		// access to the ITimeService method.
		IRemoteCallable callable = RestCallableFactory.createCallable("getCurrentTime", ITimeService.class.getName(),
				null, new HttpGetRequestType(), 30000);
		// Register the callable and associate it with the ITimeService class
		// name
		reg = registerCallables(new String[] { ITimeService.class.getName() }, new IRemoteCallable[][] { { callable } },
				null);
	}
 
	@Override
	public void disconnect() {
		super.disconnect();
		if (reg != null) {
			reg.unregister();
			reg = null;
		}
	}
 
	@Override
	public Namespace getConnectNamespace() {
		return RestNamespace.INSTANCE;
	}
 
	public static class Instantiator extends RemoteServiceContainerInstantiator {
 
		@Override
		public IContainer createInstance(ContainerTypeDescription description, Map<String, ?> parameters)
				throws ContainerCreateException {
			// Create new container instance with random uuid
			return new TimeServiceRestClientContainer((RestID) RestNamespace.INSTANCE
					.createInstance(new Object[] { "uuid:" + UUID.randomUUID().toString() }));
		}
		public String[] getImportedConfigs(ContainerTypeDescription description, String[] exporterSupportedConfigs) {
			if (Arrays.asList(exporterSupportedConfigs).contains(TIMESERVICE_HOST_CONFIG_NAME))
				return new String[] { TimeServiceRestClientContainer.TIMESERVICE_CONSUMER_CONFIG_NAME };
			else
				return null;
		}
	}
}

The TimeServiceRestClientContainer constructor first creates a unique id for itself and then it sets up a json response deserializer (for handling the json result of getCurrentTime()) with this code:

setResponseDeserializer(new IRemoteResponseDeserializer() {
	public Object deserializeResponse(String endpoint, IRemoteCall call, IRemoteCallable callable,
			@SuppressWarnings("rawtypes") Map responseHeaders, byte[] responseBody) throws NotSerializableException {
		try {
			return new JSONObject(new String(responseBody)).get("time");
		} catch (JSONException e1) {
			NotSerializableException t = new NotSerializableException("Exception serializing response from endpoing="+endpoint);
			t.setStackTrace(e1.getStackTrace());
			throw t;
		}
	}});

When the response is received, the TimeServiceRestResponseDeserializer.deserializeResponse method will be called, and from above this code then parses the json from the host

	return new JSONObject(new String(responseBody)).get("time");

The connect method implementation creates and registers an IRemoteCallable instance that associates the proxy's method name getCurrentTime with the URL of the time service (consisting of the hosts's container id...i.e. http://localhost:8080/ with the ITimeService servlet path /com.mycorp.examples.timeservice.ITimeService.

The disconnect method simply unregisters the IRemoteCallable.

As for the host remote service container, a container Instantiator must be created for the TimeServiceRestClientContainer and that is used by ECF RSA to create an instance of the TimeServiceRestClientContainer at the appropriate time.

Note the getImportedConfigs method in the Instantiator, which is automatically called by the ECF Remote Service implementation in order to allow the provider to convey that the TimeServiceRestClientContainer should be used for importing when the TIMESERVICE_HOST_CONFIG_NAME i.e. com.mycorp.examples.timeservice.rest.host.

This remote service container instantiator is then declared as a IRemoteServiceDistributionProvider in the activator

public class Activator implements BundleActivator {
 
	public void start(final BundleContext context) throws Exception {
		context.registerService(IRemoteServiceDistributionProvider.class,
				new RemoteServiceDistributionProvider.Builder()
						.setName(TimeServiceRestClientContainer.TIMESERVICE_CONSUMER_CONFIG_NAME)
						.setInstantiator(new TimeServiceRestClientContainer.Instantiator()).build(),
				null);
	}
 
	public void stop(BundleContext context) throws Exception {
	}
}

This completes the consumer provider. The source for the complete bundle is in the com.mycorp.examples.timeservice.provider.rest.consumer bundle project.

Using the New Provider

Now we have a completed host provider, and a completed consumer provider for the restful timeservice. These two providers are entirely represented by the two bundles

  1. com.mycorp.examples.timeservice.provider.rest.host
  2. com.mycorp.examples.timeservice.provider.rest.consumer

With these bundles and their dependencies, the following may now be used to export a remote service using the host provider

Dictionary<String, String> props = new Hashtable<String, String>();
props.put("service.exported.interfaces", "*");
// Specify the newly created com.mycorp.examples.timeservice.rest.host provider
props.put("service.exported.configs","com.mycorp.examples.timeservice.rest.host");
// Specify the 'id' parameter for the ID creation of the host (see 
// the TimeServiceServerContainerInstantiator.createInstance method 
props.put("com.mycorp.examples.timeservice.rest.host.id","http://localhost:8181");
// Register a new TimeServiceImpl with the above props
bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);

During registerService, the ECF RSA implementation does the following:

  1. Detects that the "service.exported.interfaces" property is set and so the service is to be exported
  2. Detects that the "service.exported.configs" property is set, and selects the container Instantiator that returns a matching value from a call to getSupportedConfigs
  3. Creates a new instance of TimeServiceServerContainer by calling the approprate container instantiator's createInstance method, and passes in a Map of appropriate service properties (i.e. com.mycorp.examples.timeservice.rest.host.id).
  4. Uses the created remote service container to export the remote service
  5. Publishes the EndpointDescription resulting from the export for consumer discovery

Note that after host registration as above that this restful provider can be tested simply by using a browser and going to

http://localhost:8181/com.mycorp.examples.timeservice.ITimeService

In the browser this will return the following json

{"time":1386738084894}

On the OSGi Remote Service consumer, upon discovery of the EndpointDescription (through network discovery protocol, or EDEF) the ECF RSA implementation does the following

  1. Select a remote service consumer container by calling all container instantiator's getImportedConfigs method...with the value of exporterSupportedConfigs from the discovered EndpointDescription
  2. Create a new container via the selected container instantiator's createInstance method
  3. Call IContainer.connect(ID,IConnectContext) on the newly created container
  4. Create an ITimeService proxy
  5. Registers this ITimeService proxy in the consumer's local OSGi service registry...along with the standardized service property values

If using DS, the last step above will result in the ITimeService proxy being injected into client code and the client code may then call the ITimeService.getCurrentTime() method. Calling this method will result in a http GET to the URL:

http://localhost:8181/com.mycorp.examples.timeservice.ITimeService

The TimeRemoteServiceHttpServlet.doGet method will be called by the HttpService and then the resulting json will be deserialized via the TimeServiceRestResponseDeserializer in the consumer...resulting in a Long value returned from by the proxy.

As with the Tutorial:_Building_your_first_OSGi_Remote_Service the consumer code is simply

package com.mycorp.examples.timeservice.consumer.ds;
 
import com.mycorp.examples.timeservice.ITimeService;
 
public class TimeServiceComponent {
 
	void bindTimeService(ITimeService timeService) {
		System.out.println("Discovered ITimeService via DS");
		// Call the service and print out result!
		System.out.println("Current time is: " + timeService.getCurrentTime());  // Call the ITimeService remote service
	}
}

Note that both the consumer code and the host code (except for the service property values on the host) are exactly the same. This makes it possible to develop, test, and deploy remote services independent of the underlying distribution providers being used.

Background and Related Articles

Getting Started with ECF's OSGi Remote Services Implementation

OSGi Remote Services and ECF

Asynchronous Proxies for Remote Services

Static File-based Discovery of Remote Service Endpoints

Download ECF Remote Services/RSA Implementation

How to Add Remote Services/RSA to Your Target Platform

Customization of ECF Remote Services

Back to the top