Skip to main content
Jump to: navigation, search

Difference between revisions of "Tutorial: ECF Remote Services for Accessing Existing REST Services"

(Declaration of the Timezone Service Interface)
Line 152: Line 152:
 
These three classes:  '''ITimezoneService''', '''Timezone''', and '''ITimezoneServiceAsync''' are all that are needed in this service API bundle.  The manifest.mf and other meta-data to complete this bundle is available in our Github repository [https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone  here].  Note that there are no references in this API to ECF classes, or any other dependencies for that matter.  As it should be, this API is completely independent of the distribution system, and only depends upon the '''semantics''' of the [https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone  Geonames Timezone service].
 
These three classes:  '''ITimezoneService''', '''Timezone''', and '''ITimezoneServiceAsync''' are all that are needed in this service API bundle.  The manifest.mf and other meta-data to complete this bundle is available in our Github repository [https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone  here].  Note that there are no references in this API to ECF classes, or any other dependencies for that matter.  As it should be, this API is completely independent of the distribution system, and only depends upon the '''semantics''' of the [https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone  Geonames Timezone service].
  
==Provider Step 1: Implement New Provider Namespace==
+
==Implementing a Client Provider==
  
ECF allows providers to create their own Namespace types, so that the endpoint id of the remote service can be properly interpreted. Here is a '''TimeServiceRestNamespace''' class
+
ECF's implementation of Remote Services exposes open, community-created APIs to make it easy to create and use a custom Client Provider.  What is a Client Provider?  It's a custom implementation of the consumer/client-side of the remote services distribution system.  An ECF client provider is able to substitute custom protocols/transports (e.g. http/https, MQTT, JMS, private protocols) and custom serialization formats (e.g. xml, JSON, object serialization, others), without having to implement the entire OSGi Remote Services and Remote Service Admin specification.  In an open, modular fashion, an ECF Client Provider can reuse any/all existing ECF or third party remote service provider functionality.  A Client Provider automatically is fully compliant with the OSGi Remote Services/RSA standards, while providing the flexibility to use new, custom, or even private transports.   [[OSGi Remote Services and ECF | Here is an architecture diagram]] showing the relationship between distribution providers, the Remote Services standards and ECF's extensible and open implementation.
  
<source lang="java">
+
===Integration with ECF Remote Service Admin===
public class TimeServiceRestNamespace extends RestNamespace {
+
  
public static final String NAME = "com.mycorp.examples.timeservice.provider.rest.namespace";
+
The Remote Service Admin specification standardizes an API and mechanism for dynamic discovery of remote services.   ECF's implementation of this standard provides a way for new Client Providers to dynamically insert themselves into this discovery process. The way this is done is to implement a new '''container''' class along with a '''container instantiator''', and to register a '''container type description''' instance as a local OSGi service using the [http://wiki.osgi.org/wiki/Whiteboard_Pattern whiteboard pattern].
+
public ID createInstance(Object[] parameters) throws IDCreateException {
+
return new TimeServiceRestID(this, URI.create((String) parameters[0]));
+
}
+
  
public static class TimeServiceRestID extends RestID {
+
For the example Timezone remote service the container class is called '''TimezoneClientContainer'''.  The full source for this class is available [https://github.com/ECF/Geonames/blob/master/bundles/org.eclipse.ecf.provider.geonames.timezone.client/src/org/eclipse/ecf/internal/provider/geonames/timezone/client/TimezoneClientContainer.java  here].
  
public TimeServiceRestID(Namespace namespace, URI uri) {
+
The role of the container instantiator is to create an appropriately typed instance of the container when needed by RSA.   When a remote service is discovered and then imported, the ECF RSA implementation takes the OSGi-define remote service properties and passes them to every available container instantiator, essentially asking:  'a container of given type has been discovered, and a new instance is needed, can you provide such an instance?'.   The implementation of this query is via the '''TimezoneClientContainer.Instantiator.getImportedConfigs''' method  (labelled in comment with '1').
super(namespace, uri);
+
}
+
}
+
}
+
</source>
+
 
+
When the '''createInstance''' call is made a new instance of '''TimeServiceRestID''' is created from the single String parameter[0] provided by the '''createInstance''' caller. In addition, we may define an extension for the '''org.eclipse.ecf.identity.namespace''' ECF extension point to allow this '''TimeServiceRestNamespace''' to be used when needed by the ECF Remote Service implementation.
+
 
+
<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 'common' bundle that can be depended upon by both the host and consumer provider bundles (below). The complete common bundle is available via the ECF git repo via the [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 bundle project].
+
 
+
Note that the package with this new Namespace/ID class must be exported by having this in the necessary Export-Package in the common bundle's manifest:
+
  
 
<source lang="java">
 
<source lang="java">
Export-Package: com.mycorp.examples.timeservice.provider.rest.common
+
public static class Instantiator extends RestClientContainerInstantiator {
</source>
+
 
+
==Provider Step 2: Creating the Provider Host Implementation==
+
 
+
Since we would like to access this service via http+rest+json, we will create a Servlet to actually handle the request...and register it dynamically via the OSGi standard HttpService.  That way we may implement the remote '''ITimeService.getCurrentTime()'' method by doing the appropriate http GET request which will be handled by our '''HttpServlet.doGet''' implementation.  Here is the complete implementation of the '''TimeRemoteServiceHttpServlet''' as well as the required ECF remote service container code
+
 
+
<source lang="java">
+
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();
+
  
private final HttpService httpService;
+
/**
+
* 1. This method is called by the ECF RSA implementation when a remote
TimeServiceServerContainer(String id, HttpService httpService) throws ContainerCreateException {
+
* service is to be imported. The exporterSupportedConfigs parameter
super(IDFactory.getDefault()
+
* contains the exported config types associated with the remote
.createID(TimeServiceRestNamespace.NAME, id));
+
* service. The implementation of this method decides whether we are
this.httpService = httpService;
+
* interested in this remote service config type. If we are
// Register our servlet with the given httpService with the TIMESERVICE_SERVLET_NAME
+
* (exporterSupportedConfigs contains CONTAINER_TYPE_NAME, then we
// which is "/com.mycorp.examples.timeservice.ITimeService"
+
* return an array of strings containing our CONTAINER_TYPE_NAME
try {
+
*/
this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
+
@Override
new TimeRemoteServiceHttpServlet(), null, null);
+
public String[] getImportedConfigs(
} catch (Exception e) {
+
ContainerTypeDescription description,
throw new ContainerCreateException("Could not create Time Service Server Container",e);
+
String[] exporterSupportedConfigs) {
 +
/**
 +
* If the exporterSupportedConfigs contains CONTAINER_TYPE_NAME,
 +
* then return that CONTAINER_TYPE_NAME to trigger RSA usage of this
 +
* container instantiator
 +
*/
 +
if (Arrays.asList(exporterSupportedConfigs).contains(
 +
CONTAINER_TYPE_NAME))
 +
return new String[] { CONTAINER_TYPE_NAME };
 +
return null;
 
}
 
}
}
 
  
public void dispose() {
+
@Override
httpService.unregister(TIMESERVICE_SERVLET_NAME);
+
/**
super.dispose();
+
* 2. This method is called by the ECF RSA to create a new instance of
}
+
* the appropriate
 
+
* container type (aka OSGi config type)
public Namespace getConnectNamespace() {
+
*/
return IDFactory.getDefault().getNamespaceByName(
+
public IContainer createInstance(ContainerTypeDescription description,
TimeServiceRestNamespace.NAME);
+
Object[] parameters) throws ContainerCreateException {
}
+
return new TimezoneClientContainer();
 
+
class TimeRemoteServiceHttpServlet extends RemoteServiceHttpServlet {
+
 
+
// Handle get call right here.
+
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 time
+
    Long currentTime = timeService.getCurrentTime();
+
+
    // 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);
+
}
+
 
}
 
}
 
}
 
}
 
}
 
 
</source>
 
</source>
 +
.
 +
The '''getImportedConfigs''' method logic is:  'if my CONTAINER_TYPE_NAME matches one of the '''exporterSupportedConfigs''' then return a String[] including my CONTAINER_TYPE_NAME.  This approach provides the flexibility to support multiple '''exporterSupportedConfigs''' with a single container type. The ECF RSA implementation then turns around and calls the ''TimezoneClientContainer.'Instantiator.createContainer''' method, which in the implementation above (2) returns a new instance of the '''TimezoneClientContainer'''.
  
In the '''TimeServiceServerContainer''' constructor a new instance of the '''TimeRemoteServiceHttpServlet''' is created and registered as a servlet with the given HttpService
+
Once the TimezoneClientContainer instance is created, ECF RSA calls it's '''connect''' method with the '''targetID''' defined in the standardized meta-data for the remote service.  Here are the lines of the TimezoneClientContainer connect method explained
  
 
<source lang="java">
 
<source lang="java">
this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
+
super.connect(targetID, connectContext1);
new TimeRemoteServiceHttpServlet(), null, null);
+
 
</source>
 
</source>
  
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:
+
Passes the '''targetID''' to the RestClientContainer super class, which sets a member variable to the value of '''targetID'''.
  
 
<source lang="java">
 
<source lang="java">
http://localhost:8080/com.mycorp.examples.timeservice.ITimeService
+
setAlwaysSendDefaultParameters(true);
 
</source>
 
</source>
  
Alternative paths may be specified as desired by changing the value of TIMESERVICE_SERVLET_NAME.
+
This line configures the container instance to always send the default parameters on the remote call.  This is necessary for the Geonames Timezone service because one of the required http request parameters is the '''username''' parameter.  Setting this to 'true' means that the default parameter values (username) are always sent (as required by the Timezone service).
  
Also present are overrides of two ECF remote service container lifecycle methods
+
To create the Remote Service proxy (the ECF-constructed proxy instance of the '''ITimezoneService''' and '''ITimezoneServiceAsync''') it's necessary to define the '''association''' between the '''getTimezone''' method and the Geonames Timezone request.  As documented [http://www.geonames.org/export/web-services.html#timezone  here], the Timzeone service request consists of
  
<source lang="java">
+
#A complete URL for the request (protocol/hostname/port + path)
public Namespace getConnectNamespace() {
+
#The required parameters. In the case of the Geonames Timezone service the parameters are
return IDFactory.getDefault().getNamespaceByName(
+
##latitude (aka 'lat')
TimeServiceRestNamespace.NAME);
+
##longitude ('lng')
}
+
##username (for authentication)
</source>
+
  
which references the '''TimeServiceRestNamespace.NAME''' to associate this remote service container with the Namespace we created above.
+
First we construct the necessary remote call parameters using a '''RemoteCallParameter.Builder'''
  
 
<source lang="java">
 
<source lang="java">
public void dispose() {
+
RemoteCallParameter.Builder parameterBuilder = new RemoteCallParameter.Builder()
httpService.unregister(TIMESERVICE_SERVLET_NAME);
+
.addParameter("lat").addParameter("lng")
super.dispose();
+
.addParameter("username", USERNAME);
}
+
 
</source>
 
</source>
  
which dynamically unregisters the servlet when the host container instance is disposed.
+
Then we construct an IRemoteCallable, defining the association between the '''getTimezone''' method and the '''/timezoneJSON''' remote service path, including the above-created parameters, and defining that this service is based upon an http GET request.
 
+
All of the actual behavior is given by the doGet method implementation in the '''TimeRemoteServiceHttpServlet'''
+
  
 
<source lang="java">
 
<source lang="java">
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+
RemoteCallable.Builder callableBuilder = new RemoteCallable.Builder(
throws ServletException, IOException {
+
"getTimezone", "/timezoneJSON").setDefaultParameters(
+
parameterBuilder.build()).setRequestType(
// 1. Get local OSGi ITimeService
+
new HttpGetRequestType());
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);
+
}
+
}
+
 
</source>
 
</source>
  
What's happening here
+
Finally, we register this IRemoteCallable and associate it with the '''ITimezoneService.class''' service interface
 
+
# The local ITimeService implementation is retrieved
+
# The ITimeService.getCurrentTime() method is called to get the local time (Long)
+
# The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream.
+
 
+
The next thing to do is to implement a ECF remote service container instantiator, which is responsible for creating and using instances of '''TimeServiceServerContainer''' under the appropriate conditions.  Here is the container instantiator class:
+
  
 
<source lang="java">
 
<source lang="java">
public class TimeServiceServerContainerInstantiator extends
+
tzServiceRegistration = registerCallables(ITimezoneService.class,
ServletServerContainerInstantiator {
+
new IRemoteCallable[] { callableBuilder.build() }, null);
 
+
public IContainer createInstance(ContainerTypeDescription description,
+
Object[] parameters) throws ContainerCreateException {
+
try {
+
// Get HttpServices
+
Collection<HttpService> httpServices = TimeServiceHttpServiceComponent.getDefault().getHttpServices();
+
HttpService httpService = httpServices.iterator().next();
+
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[] 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 container's ID.  The host container ID therefore corresponds to the '''http://localhost:8080''' in the remote service URL given above.
+
The above registration is all that's needed for the ECF remote service code to construct a proxy implementing '''ITimezoneService''' and '''ITimezoneServiceAsync'''. When a consumer application actually calls either the '''getTimezone''' or '''getTimezoneAsync''' methods on the proxy, the following will occur automatically
 
+
Note also the container instantiator's '''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 export time exactly which remote service containers to create and use to export the remote service.
+
 
+
This container instantiator class can then be declared as an extension for the ECF '''org.eclipse.ecf.containerFactory''' extension point:
+
 
+
<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...with value '''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==
+
#Construct the appropriate URL for calling the Geonames Timezone service: '''http://api.geonames.org/timezoneJSON'''
 +
#Serialize the parameters passed to the '''getTimezone''' method (i.e. 'lat' and 'lng') and add them as URL request parameters, resulting in the URL:  '''http://api.geonames.org/timezoneJSON?lat=<lat param value>&lng=<lng param value>&username=<username default value>'''
 +
#Use the resulting URL to issue an HTTP GET request to the Geonames service
  
Here is the consumer remote service container provider implementation
+
When the Geonames service responds to the GET request, it returns a single instance of a JSON Object container several fields with various information associated with the timezone...e.g. countryCode, countryName, lat, lng, timezoneId, dstOffset, etc.  The following code sets up a response deserializer, that parses the returned String using a common JSON parser.  Then given the data, it creates an instance of the '''Timezone''' class and returns it.  At remote method call time, when the Geonames service responds this method will be called to deserialize the HTTP response and return a proper Timezone instance to the caller of '''getTimezone'''.
  
 
<source lang="java">
 
<source lang="java">
public class TimeServiceRestClientContainer extends RestClientContainer {
+
setResponseDeserializer(new IRemoteResponseDeserializer() {
 
+
@Override
public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
+
public Object deserializeResponse(String endpoint,
 
+
IRemoteCall call, IRemoteCallable callable,
private IRemoteServiceRegistration reg;
+
@SuppressWarnings("rawtypes") Map responseHeaders,
 
+
byte[] responseBody) throws NotSerializableException {
TimeServiceRestClientContainer() {
+
try {
// Create a random ID for the client container
+
// Convert responseBody to String and parse using org.json
super((RestID) IDFactory.getDefault().createID(
+
// lib
TimeServiceRestNamespace.NAME, "uuid:"
+
JSONObject jo = new JSONObject(new String(responseBody));
+ java.util.UUID.randomUUID().toString()));
+
// Check status for failure. Throws exception if
// This sets up the JSON deserialization of the server's response.
+
// error status
// See below for implementation of TimeServiceRestResponseDeserializer
+
if (jo.has("status")) {
setResponseDeserializer(new TimeServiceRestResponseDeserializer());
+
JSONObject status = jo.getJSONObject("status");
}
+
throw new JSONException(status.getString("message")
 
+
+ ";code=" + status.getInt("value"));
public void connect(ID targetID, IConnectContext connectContext1)
+
}
throws ContainerConnectException {
+
// No exception, so get each of the fields from the
super.connect(targetID, connectContext1);
+
// json object
// Create the IRemoteCallable to represent
+
String countryCode = jo.getString("countryCode");
// access to the ITimeService method. 
+
String countryName = jo.getString("countryName");
IRemoteCallable callable = RestCallableFactory.createCallable(
+
double lat = jo.getDouble("lat");
"getCurrentTime", ITimeService.class.getName(), null,
+
double lng = jo.getDouble("lng");
new HttpGetRequestType(), 30000);
+
String timezoneId = jo.getString("timezoneId");
// Register the callable and associate it with the ITimeService class
+
double dstOffset = jo.getDouble("dstOffset");
// name
+
double gmtOffset = jo.getDouble("gmtOffset");
reg = registerCallables(new String[] { ITimeService.class.getName() },
+
double rawOffset = jo.getDouble("rawOffset");
new IRemoteCallable[][] { { callable } }, null);
+
String time = jo.getString("time");
}
+
String sunrise = jo.getString("sunrise");
 
+
String sunset = jo.getString("sunset");
public void disconnect() {
+
// Now create and return Timezone instance with all the
super.disconnect();
+
// appropriate
if (reg != null) {
+
// values of the fields
reg.unregister();
+
return new Timezone(countryCode, countryName, lat, lng,
reg = null;
+
timezoneId, dstOffset, gmtOffset, rawOffset,
}
+
dateFormat.parse(time), dateFormat.parse(sunrise),
}
+
dateFormat.parse(sunset));
 
+
// If some json parsing exception (badly formatted json and
class TimeServiceRestResponseDeserializer implements
+
// so on,
IRemoteResponseDeserializer {
+
// throw an appropriate exception
public Object deserializeResponse(String endpoint, IRemoteCall call,
+
} catch (Exception e) {
IRemoteCallable callable,
+
NotSerializableException ex = new NotSerializableException(
@SuppressWarnings("rawtypes") Map responseHeaders,
+
"Problem in response from timezone service endpoint="
byte[] responseBody) throws NotSerializableException {
+
+ endpoint + " status message: "
// We simply need to read the response body (json String),
+
+ e.getMessage());
// And return the value of the "time" field
+
ex.setStackTrace(e.getStackTrace());
try {
+
throw ex;
return new JSONObject(new String(responseBody)).get("time");
+
}
} catch (JSONException e1) {
+
throw new NotSerializableException(
+
TimeServiceRestResponseDeserializer.class.getName());
+
 
}
 
}
}
+
});
 
+
}
+
 
+
public Namespace getConnectNamespace() {
+
return IDFactory.getDefault().getNamespaceByName(
+
TimeServiceRestNamespace.NAME);
+
 
}
 
}
}
 
 
</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.
+
Note that this code may use any JSON parser, or in fact may do whatever is appropriate (e.g. parse xml, deserialize java Objects, use some other serialization technique like protocol buffers, etc to convert from the remote service response to the proper return type for the service interface ('''Timezone''').
  
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 '''connect''' implementation above is sufficient for the TimezoneClientContainer to create and execute the Geonames Timezone service request, and handle/deserialize the JSON response, and associate this implementation with the '''ITimezoneService.getTimezone''' method and provided parameters.  Note that it's also possible to reuse other codebases (other than the RestClientContainer superclass) if the remote service is not REST-based, or requires more control and customization of the remote invocation process.
  
<source lang="java">
+
==Registering the Container and Namespace==
// This sets up the JSON deserialization of the server's response.
+
// See below for implementation of TimeServiceRestResponseDeserializer
+
setResponseDeserializer(new TimeServiceRestResponseDeserializer());
+
</source>
+
  
When the response is received, the '''TimeServiceRestResponseDeserializer.deserializeResponse''' method will be called, and this code then parses the json from the host
+
In the Activator for this Client Provider, there are two OSGi service registrations, which are necessary to hook into the ECF RSA implementation as described above
  
 
<source lang="java">
 
<source lang="java">
return new JSONObject(new String(responseBody)).get("time");
+
public void start(BundleContext bundleContext) throws Exception {
</source>
+
Activator.context = bundleContext;
 
+
// Register an instance of TimezoneNamespace
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'''.
+
bundleContext.registerService(Namespace.class, new TimezoneNamespace(),
 
+
null);
The disconnect method simply unregisters the '''IRemoteCallable'''.
+
// Register an instance of TimezoneContainerTypeDescription (see class
 
+
// below)
As for the host remote service container, a container instantiator must be created for the TimeServiceRestClientContainer
+
bundleContext.registerService(ContainerTypeDescription.class,
 
+
new TimezoneContainerTypeDescription(), null);
<source lang="java">
+
public class TimeServiceRestClientContainerInstantiator extends
+
RestClientContainerInstantiator {
+
 
+
private static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
+
 
+
@Override
+
public IContainer createInstance(ContainerTypeDescription description,
+
Object[] parameters) throws ContainerCreateException {
+
// Create new container instance
+
return new TimeServiceRestClientContainer();
+
 
}
 
}
  
public String[] getImportedConfigs(ContainerTypeDescription description,
+
class TimezoneContainerTypeDescription extends ContainerTypeDescription {
String[] exporterSupportedConfigs) {
+
public TimezoneContainerTypeDescription() {
@SuppressWarnings("rawtypes")
+
super(TimezoneClientContainer.CONTAINER_TYPE_NAME,
List supportedConfigs = Arrays.asList(exporterSupportedConfigs);
+
new TimezoneClientContainer.Instantiator(),
// If the supportedConfigs contains the timeservice host config,
+
"Geonames Timezone Remote Service Client Container");
// 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>
+
 
+
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 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==
+
 
+
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
+
 
+
#[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.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
+
 
+
<source lang="java">
+
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);
+
</source>
+
 
+
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
+
 
+
<source lang="java">
+
http://localhost:8181/com.mycorp.examples.timeservice.ITimeService
+
</source>
+
 
+
In the browser this will return the following json
+
 
+
<source lang="javascript">
+
{"time":1386738084894}
+
</source>
+
 
+
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:
+
 
+
<source lang="java">
+
http://localhost:8181/com.mycorp.examples.timeservice.ITimeService
+
</source>
+
 
+
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
+
 
+
<source lang="java">
+
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
+
 
}
 
}
}
 
 
</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, test, and deploy remote services independent of the underlying providers being used.
+
As described in comments, these two service registrations (Namespace and ContainerTypeDescription) setup the necessary structures for RSA to create the TimezoneClientContainer, call '''connect''' and other methods on the container at the appropriate times during remote service discovery, import, proxy creation, and remote service call/invocation.
  
 
==Background and Related Articles==
 
==Background and Related Articles==

Revision as of 16:40, 8 December 2014


Introduction

Remote Services are usually defined with both a 'host' component (aka 'server') and a 'consumer' component of the service (aka 'client'). The host implements and exports a service for remote access, and the consumer discovers, imports, and then calls the remote service. ECF has a previous tutorial showing how to create a custom RESTful Remote Service Provider with both these host and consumer components, and there are also [[ ECF#Introductory Materials | a number of other tutorials and examples] that show how to use existing ECF providers to expose, discover, import, and use a remote service.

There are many situations, however, where it would be desirable to expose an existing (e.g. web-based REST) service as a Remote Service. There are of course many public and private web services already in place. Obviously, these services are frequently not running within an OSGi framework, and therefore not exposed as OSGi Remote Services. For many such existing services, it would be desirable to have only the consumer of the service be running OSGi and using Remote Services, and not require that the host be running within an OSGi framework, or be implemented in Java. Such a 'consumer/client-only' approach has several important advantages

  1. Existing server/services don't change, and may be implemented in any language, using any framework
  2. Existing clients don't change, and may be implemented in any language, using any framework
  3. This new Remote Service consumer can benefit from advantages coming from Remote Services
    1. Clear separation between the remote service interface/API and the remote service implementation
    2. Support for rich versioning (e.g. of the service interface)
    3. Full dynamics of the remote service
    4. Remote service discovery
    5. Fine-grained security and a standardized management agent provided by Remote Service Admin (RSA)
    6. Synchronous and/or asynchronous/non-blocking access to the remote service without any implementation changes
    7. Integration with/use of a variety of injection frameworks (e.g. Spring/Blueprint, Declarative Services, others)
    8. Use of open standards (OSGi Remote Services/RSA) and open, community-developed implementations (ECF Remote Services) to avoid vendor lock-in

The tutorial below will focus on the creation of a remote service client provider that exposes an existing REST service as an OSGi Remote Service. The example remote service used in this tutorial is the Geonames Timezone service, which is a publicly available service to provide timezone information for a given point on Earth. However, the steps shown below, however, may be applied to creating a consumer/client for any remote service, whether exposed via REST/xml, REST/json, via http/https, or any other protocol. This allows a very powerful approach to integration of remote services into applications: The use of all of the advantages of ECF Remote Services listed above, without any required changes existing services or existing clients of those services.

The complete code for this tutorial is available at the ECF GitHub GeoNames repository.

Declaration of the Timezone Service Interface

The OSGi Services model is based upon a clear separation between the service interface (one or more java interface classes) and the service implementation (a POJO instance that implements those interfaces. The first step in making the Geonames Timeservice available as an ECF Remote Service is therefore to declare a new service interface class that exposes the appropriate semantics of the Geonames Timezone service. Here is a declaration that captures the documented semantics of the Geonames Timezone service

package org.geonames.timezone;
/**
 * Service interface to represent consumer access to the Geonames timezone service. 
 * This web-service is documented here:
 * http://www.geonames.org/export/web-services.html#timezone
 *
 */
public interface ITimezoneService {
	/**
	 * Get a timezone instance, given latitude and longitude values.
	 * 
	 * @param latitude the latitude of the location to get the timezone for
	 * @param longitude the longitude of the location to get the timezone for
	 * @return the Timezone information for the given latitude and longitude.  Should
	 * return <code>null</code> if the latitude or longitude are nonsensical (e.g. negative values)
	 */
	Timezone getTimezone(double latitude, double longitude);
}

The getTimezone method returns an instance of the Timezone class. The Timezone class exposes the data returned by the Geonames Timezone service after converting the JSON to a Java Timezone instance. Here is the full source to both the ITimezoneService and the Timezone classes.

Asynchronous Access to the Remote Service

ECF Remote Services can automatically and dynamically create an asynchronous proxy to the remote service. This allows the consumer application (the code that calls the ITimezoneService.getTimezone method) to choose whether to make a synchronous remote call that will block the calling thread until the remote call is complete, or to make an asynchronous remote call that allows the application thread to continue while the remote call is made. As described in Asynchronous Remote Services all that's necessary to have ECF create an asynchronous proxy is to declare an asynchronous version of the ITimezoneService interface. Here is such a declaration for the ITimezoneService interface

package org.geonames.timezone;
 
import java.util.concurrent.CompletableFuture;
/**
 * Asynchronous version of ITimezoneService
 */
public interface ITimezoneServiceAsync {
	/**
	 * Immediately (without blocking) return a CompletableFuture to later retrieve a
	 * returned Timezone instance
	 * 
	 * @param latitude the latitude of the location to get the timezone for
	 * @param longitude the longitude of the location to get the timezone for
	 * @return the CompletableFuture<Timezone> to provide later access to an
	 * instance of Timezone information for the given latitude and longitude.  Should
	 * not return <code>null</code>.
	 */
	CompletableFuture<Timezone> getTimezoneAsync(double latitude, double longitude);	
}

Note the 'Async' suffix in both the interface class name (ITimezoneServiceAsync) and the method name (getTimezoneAsync). This is as described in Asynchronous Remote Services and is all that is necessary to get ECF's Remote Services implementation to dynamically construct a proxy exposing the ITimezoneServiceAsync method for use by consumer/application code.

These three classes: ITimezoneService, Timezone, and ITimezoneServiceAsync are all that are needed in this service API bundle. The manifest.mf and other meta-data to complete this bundle is available in our Github repository here. Note that there are no references in this API to ECF classes, or any other dependencies for that matter. As it should be, this API is completely independent of the distribution system, and only depends upon the semantics of the Geonames Timezone service.

Implementing a Client Provider

ECF's implementation of Remote Services exposes open, community-created APIs to make it easy to create and use a custom Client Provider. What is a Client Provider? It's a custom implementation of the consumer/client-side of the remote services distribution system. An ECF client provider is able to substitute custom protocols/transports (e.g. http/https, MQTT, JMS, private protocols) and custom serialization formats (e.g. xml, JSON, object serialization, others), without having to implement the entire OSGi Remote Services and Remote Service Admin specification. In an open, modular fashion, an ECF Client Provider can reuse any/all existing ECF or third party remote service provider functionality. A Client Provider automatically is fully compliant with the OSGi Remote Services/RSA standards, while providing the flexibility to use new, custom, or even private transports. Here is an architecture diagram showing the relationship between distribution providers, the Remote Services standards and ECF's extensible and open implementation.

Integration with ECF Remote Service Admin

The Remote Service Admin specification standardizes an API and mechanism for dynamic discovery of remote services. ECF's implementation of this standard provides a way for new Client Providers to dynamically insert themselves into this discovery process. The way this is done is to implement a new container class along with a container instantiator, and to register a container type description instance as a local OSGi service using the whiteboard pattern.

For the example Timezone remote service the container class is called TimezoneClientContainer. The full source for this class is available here.

The role of the container instantiator is to create an appropriately typed instance of the container when needed by RSA. When a remote service is discovered and then imported, the ECF RSA implementation takes the OSGi-define remote service properties and passes them to every available container instantiator, essentially asking: 'a container of given type has been discovered, and a new instance is needed, can you provide such an instance?'. The implementation of this query is via the TimezoneClientContainer.Instantiator.getImportedConfigs method (labelled in comment with '1').

	public static class Instantiator extends RestClientContainerInstantiator {
 
		/**
		 * 1. This method is called by the ECF RSA implementation when a remote
		 * service is to be imported. The exporterSupportedConfigs parameter
		 * contains the exported config types associated with the remote
		 * service. The implementation of this method decides whether we are
		 * interested in this remote service config type. If we are
		 * (exporterSupportedConfigs contains CONTAINER_TYPE_NAME, then we
		 * return an array of strings containing our CONTAINER_TYPE_NAME
		 */
		@Override
		public String[] getImportedConfigs(
				ContainerTypeDescription description,
				String[] exporterSupportedConfigs) {
			/**
			 * If the exporterSupportedConfigs contains CONTAINER_TYPE_NAME,
			 * then return that CONTAINER_TYPE_NAME to trigger RSA usage of this
			 * container instantiator
			 */
			if (Arrays.asList(exporterSupportedConfigs).contains(
					CONTAINER_TYPE_NAME))
				return new String[] { CONTAINER_TYPE_NAME };
			return null;
		}
 
		@Override
		/**
		 * 2. This method is called by the ECF RSA to create a new instance of
		 * the appropriate
		 * container type (aka OSGi config type)
		 */
		public IContainer createInstance(ContainerTypeDescription description,
				Object[] parameters) throws ContainerCreateException {
			return new TimezoneClientContainer();
		}
	}

. The getImportedConfigs' method logic is: 'if my CONTAINER_TYPE_NAME matches one of the exporterSupportedConfigs then return a String[] including my CONTAINER_TYPE_NAME. This approach provides the flexibility to support multiple exporterSupportedConfigs with a single container type. The ECF RSA implementation then turns around and calls the TimezoneClientContainer.'Instantiator.createContainer method, which in the implementation above (2) returns a new instance of the TimezoneClientContainer.

Once the TimezoneClientContainer instance is created, ECF RSA calls it's connect method with the targetID defined in the standardized meta-data for the remote service. Here are the lines of the TimezoneClientContainer connect method explained

super.connect(targetID, connectContext1);

Passes the targetID to the RestClientContainer super class, which sets a member variable to the value of targetID.

		setAlwaysSendDefaultParameters(true);

This line configures the container instance to always send the default parameters on the remote call. This is necessary for the Geonames Timezone service because one of the required http request parameters is the username parameter. Setting this to 'true' means that the default parameter values (username) are always sent (as required by the Timezone service).

To create the Remote Service proxy (the ECF-constructed proxy instance of the ITimezoneService and ITimezoneServiceAsync) it's necessary to define the association between the getTimezone method and the Geonames Timezone request. As documented here, the Timzeone service request consists of

  1. A complete URL for the request (protocol/hostname/port + path)
  2. The required parameters. In the case of the Geonames Timezone service the parameters are
    1. latitude (aka 'lat')
    2. longitude ('lng')
    3. username (for authentication)

First we construct the necessary remote call parameters using a RemoteCallParameter.Builder

RemoteCallParameter.Builder parameterBuilder = new RemoteCallParameter.Builder()
		.addParameter("lat").addParameter("lng")
		.addParameter("username", USERNAME);

Then we construct an IRemoteCallable, defining the association between the getTimezone method and the /timezoneJSON remote service path, including the above-created parameters, and defining that this service is based upon an http GET request.

RemoteCallable.Builder callableBuilder = new RemoteCallable.Builder(
		"getTimezone", "/timezoneJSON").setDefaultParameters(
		parameterBuilder.build()).setRequestType(
		new HttpGetRequestType());

Finally, we register this IRemoteCallable and associate it with the ITimezoneService.class service interface

tzServiceRegistration = registerCallables(ITimezoneService.class,
		new IRemoteCallable[] { callableBuilder.build() }, null);

The above registration is all that's needed for the ECF remote service code to construct a proxy implementing ITimezoneService and ITimezoneServiceAsync. When a consumer application actually calls either the getTimezone or getTimezoneAsync methods on the proxy, the following will occur automatically

  1. Construct the appropriate URL for calling the Geonames Timezone service: http://api.geonames.org/timezoneJSON
  2. Serialize the parameters passed to the getTimezone method (i.e. 'lat' and 'lng') and add them as URL request parameters, resulting in the URL: http://api.geonames.org/timezoneJSON?lat=<lat param value>&lng=<lng param value>&username=<username default value>
  3. Use the resulting URL to issue an HTTP GET request to the Geonames service

When the Geonames service responds to the GET request, it returns a single instance of a JSON Object container several fields with various information associated with the timezone...e.g. countryCode, countryName, lat, lng, timezoneId, dstOffset, etc. The following code sets up a response deserializer, that parses the returned String using a common JSON parser. Then given the data, it creates an instance of the Timezone class and returns it. At remote method call time, when the Geonames service responds this method will be called to deserialize the HTTP response and return a proper Timezone instance to the caller of getTimezone.

		setResponseDeserializer(new IRemoteResponseDeserializer() {
			@Override
			public Object deserializeResponse(String endpoint,
					IRemoteCall call, IRemoteCallable callable,
					@SuppressWarnings("rawtypes") Map responseHeaders,
					byte[] responseBody) throws NotSerializableException {
				try {
					// Convert responseBody to String and parse using org.json
					// lib
					JSONObject jo = new JSONObject(new String(responseBody));
					// Check status for failure. Throws exception if
					// error status
					if (jo.has("status")) {
						JSONObject status = jo.getJSONObject("status");
						throw new JSONException(status.getString("message")
								+ ";code=" + status.getInt("value"));
					}
					// No exception, so get each of the fields from the
					// json object
					String countryCode = jo.getString("countryCode");
					String countryName = jo.getString("countryName");
					double lat = jo.getDouble("lat");
					double lng = jo.getDouble("lng");
					String timezoneId = jo.getString("timezoneId");
					double dstOffset = jo.getDouble("dstOffset");
					double gmtOffset = jo.getDouble("gmtOffset");
					double rawOffset = jo.getDouble("rawOffset");
					String time = jo.getString("time");
					String sunrise = jo.getString("sunrise");
					String sunset = jo.getString("sunset");
					// Now create and return Timezone instance with all the
					// appropriate
					// values of the fields
					return new Timezone(countryCode, countryName, lat, lng,
							timezoneId, dstOffset, gmtOffset, rawOffset,
							dateFormat.parse(time), dateFormat.parse(sunrise),
							dateFormat.parse(sunset));
					// If some json parsing exception (badly formatted json and
					// so on,
					// throw an appropriate exception
				} catch (Exception e) {
					NotSerializableException ex = new NotSerializableException(
							"Problem in response from timezone service endpoint="
									+ endpoint + " status message: "
									+ e.getMessage());
					ex.setStackTrace(e.getStackTrace());
					throw ex;
				}
			}
		});
	}

Note that this code may use any JSON parser, or in fact may do whatever is appropriate (e.g. parse xml, deserialize java Objects, use some other serialization technique like protocol buffers, etc to convert from the remote service response to the proper return type for the service interface (Timezone).

The connect implementation above is sufficient for the TimezoneClientContainer to create and execute the Geonames Timezone service request, and handle/deserialize the JSON response, and associate this implementation with the ITimezoneService.getTimezone method and provided parameters. Note that it's also possible to reuse other codebases (other than the RestClientContainer superclass) if the remote service is not REST-based, or requires more control and customization of the remote invocation process.

Registering the Container and Namespace

In the Activator for this Client Provider, there are two OSGi service registrations, which are necessary to hook into the ECF RSA implementation as described above

	public void start(BundleContext bundleContext) throws Exception {
		Activator.context = bundleContext;
		// Register an instance of TimezoneNamespace
		bundleContext.registerService(Namespace.class, new TimezoneNamespace(),
				null);
		// Register an instance of TimezoneContainerTypeDescription (see class
		// below)
		bundleContext.registerService(ContainerTypeDescription.class,
				new TimezoneContainerTypeDescription(), null);
	}
 
	class TimezoneContainerTypeDescription extends ContainerTypeDescription {
		public TimezoneContainerTypeDescription() {
			super(TimezoneClientContainer.CONTAINER_TYPE_NAME,
					new TimezoneClientContainer.Instantiator(),
					"Geonames Timezone Remote Service Client Container");
		}
	}

As described in comments, these two service registrations (Namespace and ContainerTypeDescription) setup the necessary structures for RSA to create the TimezoneClientContainer, call connect and other methods on the container at the appropriate times during remote service discovery, import, proxy creation, and remote service call/invocation.

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

Copyright © Eclipse Foundation, Inc. All Rights Reserved.