Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Tutorial: Creating a RESTful Remote Service Provider
Contents
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
- The local ITimeService host implementation is retrieved via HttpServiceComponent.getDefault().getService(ITimeService.class);
- The ITimeService.getCurrentTime() method is called to get the local time, which returns and instance of type Long
- 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
- com.mycorp.examples.timeservice.provider.rest.host
- 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:
- 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
- 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
- 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
- Select a remote service consumer container by calling all container instantiator's getImportedConfigs method...with the value of exporterSupportedConfigs from the discovered EndpointDescription
- Create a new container via the selected container instantiator's createInstance method
- Call IContainer.connect(ID,IConnectContext) on the newly created container
- Create an ITimeService proxy
- 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
Asynchronous Proxies for Remote Services
Static File-based Discovery of Remote Service Endpoints
Download ECF Remote Services/RSA Implementation