Skip to main content
Jump to: navigation, search

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

(Asynchronous Access to the Service)
 
(42 intermediate revisions by the same user not shown)
Line 74: Line 74:
 
==Introduction==
 
==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 [[ Tutorial: Creating a RESTful Remote Service Provider  | 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.
+
Remote Services are usually defined with both a 'host' component (aka 'server') and a 'consumer' component (aka 'client').  The host typically implements and exports a service for remote access, and the consumer discovers, imports, and then uses the remote service by calling methods on the service interface.  ECF has a previous tutorial showing how to [[ Tutorial: Creating a RESTful Remote Service Provider  | 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]] showing 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
+
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.  These services are frequently not running within an OSGi framework, nor are implemented in Java, and therefore not exposed as a Remote Service host.    For many such existing services, it would be valuable 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, nor even necessarily be implemented in Java.   
  
#Existing server/services don't change, and may be implemented in any language, using any framework
+
With any Remote Service, what the consumer '''sees''' at the time of use is a dynamically-constructed proxy implementing one or more service interfaces.  ECF's RSA implementation provides open, community-developed APIs allowing full customization of the both the creation and runtime behavior of the proxy, further enabling the creation of a 'consumer/client-only' remote service provider capable of interacting with any Internet-based service.
#Existing clients don't change, and may be implemented in any language, using any framework
+
 
#This new Remote Service consumer can benefit from advantages coming from Remote Services
+
The use of a 'consumer/client-only' provider has several important advantages for consumer application development
 +
 
 +
#Existing server/services don't change, and may be implemented in any language, using any framework or service infrastructure
 +
#Existing clients don't change, and may continue to be implemented in any language, using any framework
 +
#This Remote Service consumer can automatically benefit from advantages of using Remote Services
 
##Clear separation between the '''remote service interface/API''' and the '''remote service implementation'''
 
##Clear separation between the '''remote service interface/API''' and the '''remote service implementation'''
 
##Support for rich versioning (e.g. of the service interface)
 
##Support for rich versioning (e.g. of the service interface)
 
##Full dynamics of the remote service
 
##Full dynamics of the remote service
 
##Remote service discovery
 
##Remote service discovery
##Fine-grained security and a standardized management agent provided by [[Remote_Services_Admin | Remote Service Admin (RSA)]]
+
##Fine-grained service-level type-safe security
 +
##Standardized management agent via [[Remote_Services_Admin | Remote Service Admin (RSA)]]
 
##Synchronous and/or asynchronous/non-blocking access to the remote service without any implementation changes
 
##Synchronous and/or asynchronous/non-blocking access to the remote service without any implementation changes
##Integration with/use of a variety of injection frameworks (e.g. Spring/Blueprint, Declarative Services, others)
+
##Automatic integration with/use of a variety of injection frameworks (e.g. Spring/Blueprint, Declarative Services, others)
##Use of open standards (OSGi Remote Services/RSA) and open, community-developed implementations (ECF Remote Services) to avoid vendor lock-in
+
##Automatic integration with any framework that uses OSGi Services
 +
##Use of open standards (OSGi Remote Services/RSA) and open, community-developed implementations (ECF Remote Services) to avoid vendor lock-in and provide easy customization
  
 
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 [http://www.geonames.org/ Geonames] [http://www.geonames.org/export/web-services.html#timezone 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 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 [http://www.geonames.org/ Geonames] [http://www.geonames.org/export/web-services.html#timezone 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.
Line 94: Line 100:
 
The complete code for this tutorial is available at the [https://github.com/ECF/Geonames ECF GitHub GeoNames repository].
 
The complete code for this tutorial is available at the [https://github.com/ECF/Geonames ECF GitHub GeoNames repository].
  
==Declaration of the Service Interface==
+
==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 [http://www.geonames.org/export/web-services.html#timezone Geonames Timezone service].  Here is a declaration that captures the documented semantics of the [http://www.geonames.org/export/web-services.html#timezone Geonames Timezone service]
+
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 [http://www.geonames.org/export/web-services.html#timezone Geonames Timezone service].   
  
 
<source lang="java">
 
<source lang="java">
Line 121: Line 127:
 
The '''getTimezone''' method returns an instance of the Timezone class.  The Timezone class exposes the data returned by the [http://www.geonames.org/export/web-services.html#timezone Geonames Timezone service] after converting the JSON to a Java Timezone instance.  [https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone/src/org/geonames/timezone Here is the full source to both the ITimezoneService and the Timezone classes].
 
The '''getTimezone''' method returns an instance of the Timezone class.  The Timezone class exposes the data returned by the [http://www.geonames.org/export/web-services.html#timezone Geonames Timezone service] after converting the JSON to a Java Timezone instance.  [https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone/src/org/geonames/timezone Here is the full source to both the ITimezoneService and the Timezone classes].
  
===Asynchronous Access to the Service===
+
===Asynchronous Remote Service===
  
[[ECF#OSGi Remote Services  | ECF Remote Services]] can automatically and dynamically create an [[ECF/Asynchronous Remote Services | 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 [[ECF/Asynchronous Remote Services 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
+
[[ECF#OSGi Remote Services  | ECF Remote Services]] can automatically and dynamically create an [[ECF/Asynchronous Remote Services | 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 [[ECF/Asynchronous Remote Services | 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
  
 
<source lang="java">
 
<source lang="java">
Line 147: Line 153:
 
</source>
 
</source>
  
Note the 'Async' suffix in both the interface class name ('''ITimezoneServiceAsync''') and the method name ('''getTimezoneAsync''').  This is as described in [[ECF/Asynchronous Remote Services Asynchronous Remote Services]] and is all that is necessary to get ECF's Remote Services implementation to dynamically
+
Note the 'Async' suffix in both the interface class name ('''ITimezoneServiceAsync''') and the method name ('''getTimezoneAsync''').  This is as described in [[ECF/Asynchronous Remote Services | 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.
 
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 [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==
+
==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">
+
===Remote Service Admin Integration===
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>
+
/**
 
+
* 1. This method is called by the ECF RSA implementation when a remote
==Provider Step 2: Creating the Provider Host Implementation==
+
* service is to be imported. The exporterSupportedConfigs parameter
 
+
* contains the exported config types associated with the remote
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
+
* service. The implementation of this method decides whether we are
 
+
* interested in this remote service config type. If we are
<source lang="java">
+
* (exporterSupportedConfigs contains CONTAINER_TYPE_NAME, then we
public class TimeServiceServerContainer extends ServletServerContainer {
+
* return an array of strings containing our CONTAINER_TYPE_NAME
 
+
*/
public static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
+
public String[] getImportedConfigs(
public static final String TIMESERVICE_SERVLET_NAME = "/" + ITimeService.class.getName();
+
ContainerTypeDescription description,
 
+
String[] exporterSupportedConfigs) {
private final HttpService httpService;
+
/**
+
* If the exporterSupportedConfigs contains CONTAINER_TYPE_NAME,
TimeServiceServerContainer(String id, HttpService httpService) throws ContainerCreateException {
+
* then return that CONTAINER_TYPE_NAME to trigger RSA usage of this
super(IDFactory.getDefault()
+
* container instantiator
.createID(TimeServiceRestNamespace.NAME, id));
+
*/
this.httpService = httpService;
+
if (Arrays.asList(exporterSupportedConfigs).contains(
// Register our servlet with the given httpService with the TIMESERVICE_SERVLET_NAME
+
CONTAINER_TYPE_NAME))
// which is "/com.mycorp.examples.timeservice.ITimeService"
+
return new String[] { CONTAINER_TYPE_NAME };
try {
+
return null;
this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
+
new TimeRemoteServiceHttpServlet(), null, null);
+
} catch (Exception e) {
+
throw new ContainerCreateException("Could not create Time Service Server Container",e);
+
 
}
 
}
}
+
/**
 
+
* 2. This method is called by the ECF RSA to create a new instance of
public void dispose() {
+
* the appropriate
httpService.unregister(TIMESERVICE_SERVLET_NAME);
+
* container type (aka OSGi config type)
super.dispose();
+
*/
}
+
public IContainer createInstance(ContainerTypeDescription description,
 
+
Object[] parameters) throws ContainerCreateException {
public Namespace getConnectNamespace() {
+
return new TimezoneClientContainer();
return IDFactory.getDefault().getNamespaceByName(
+
TimeServiceRestNamespace.NAME);
+
}
+
 
+
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 summarized as:  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:
+
This line passes the RSA-created '''targetID''' to the RestClientContainer super class, which simply 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 automatically construct the 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 request consists of
 +
 
 +
#A complete URL for the request (protocol/hostname/port + path)
 +
#The required parameters.  In the case of the Geonames Timezone service the parameters are
 +
##latitude (aka 'lat')
 +
##longitude ('lng')
 +
##username (for authentication)
 +
 
 +
First we construct the necessary remote call parameters using a '''RemoteCallParameter.Builder'''
  
 
<source lang="java">
 
<source lang="java">
public Namespace getConnectNamespace() {
+
RemoteCallParameter.Builder parameterBuilder = new RemoteCallParameter.Builder()
return IDFactory.getDefault().getNamespaceByName(
+
.addParameter("lat").addParameter("lng")
TimeServiceRestNamespace.NAME);
+
.addParameter("username", USERNAME);
}
+
 
</source>
 
</source>
  
which references the '''TimeServiceRestNamespace.NAME''' to associate this remote service container with the Namespace we created above.
+
Then we construct an IRemoteCallable, defining the association between the '''getTimezone''' method and the '''/timezoneJSON''' remote service path, including the above-created parameters, and setting the request to use HTTP GET.
  
 
<source lang="java">
 
<source lang="java">
public void dispose() {
+
RemoteCallable.Builder callableBuilder = new RemoteCallable.Builder(
httpService.unregister(TIMESERVICE_SERVLET_NAME);
+
"getTimezone", "/timezoneJSON").setDefaultParameters(
super.dispose();
+
parameterBuilder.build()).setRequestType(
}
+
new HttpGetRequestType());
 
</source>
 
</source>
  
which dynamically unregisters the servlet when the host container instance is disposed.
+
Finally, we register this IRemoteCallable and associate it with the '''ITimezoneService.class''' service interface
 
+
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)
+
tzServiceRegistration = registerCallables(ITimezoneService.class,
throws ServletException, IOException {
+
new IRemoteCallable[] { callableBuilder.build() }, null);
+
// 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);
+
}
+
}
+
 
</source>
 
</source>
  
What's happening here
+
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
  
# The local ITimeService implementation is retrieved
+
#Construct the appropriate URL for calling the Geonames Timezone service:  <pre>http://api.geonames.org/timezoneJSON</pre>
# The ITimeService.getCurrentTime() method is called to get the local time (Long)
+
#Serialize the parameters passed to the '''getTimezone''' method (i.e. 'lat' and 'lng') and add them as URL request parameters, resulting in the URL:  <pre>http://api.geonames.org/timezoneJSON?lat=<lat param value>&lng=<lng param value>&username=<username default value></pre>
# The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream.
+
#Use the resulting URL to issue an HTTP GET request to the Geonames service
  
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:
+
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 TimeServiceServerContainerInstantiator extends
+
setResponseDeserializer(new IRemoteResponseDeserializer() {
ServletServerContainerInstantiator {
+
@Override
 
+
public Object deserializeResponse(String endpoint,
public IContainer createInstance(ContainerTypeDescription description,
+
IRemoteCall call, IRemoteCallable callable,
Object[] parameters) throws ContainerCreateException {
+
@SuppressWarnings("rawtypes") Map responseHeaders,
try {
+
byte[] responseBody) throws NotSerializableException {
// Get HttpServices
+
try {
Collection<HttpService> httpServices = TimeServiceHttpServiceComponent.getDefault().getHttpServices();
+
// Convert responseBody to String and parse using org.json
HttpService httpService = httpServices.iterator().next();
+
// lib
Map<String, Object> map = (Map<String, Object>) parameters[0];
+
JSONObject jo = new JSONObject(new String(responseBody));
String id = (String) map.get("id");
+
// Check status for failure. Throws exception if
return new TimeServiceServerContainer(IDFactory.getDefault()
+
// error status
.createID(TimeServiceRestNamespace.NAME, id), httpService);
+
if (jo.has("status")) {
} catch (Exception e) {
+
JSONObject status = jo.getJSONObject("status");
throw new ContainerCreateException(
+
throw new JSONException(status.getString("message")
"Could not create TimeServiceServerContainer", e);
+
+ ";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;
 +
}
 +
}
 +
});
 
}
 
}
 
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.
+
Note that deserialization code may do whatever is appropriate (e.g. parse xml, parse JSON, deserialize java Objects, use some other serialization technique like protocol buffers, etc) to convert from appropriate remote response (JSON) to an instance of the service interface ('''Timezone''').
  
Note also the container instantiator's '''getSupportedConfigs''' method.   This method allows providers to define a String[] of config types that they supportThis 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.
+
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 parametersNote 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.
  
This container instantiator class can then be declared as an extension for the ECF '''org.eclipse.ecf.containerFactory''' extension point:
+
===Registering the Container and Namespace===
  
<source lang="xml">
+
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
  <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==
+
 
+
Here is the consumer remote service container provider implementation
+
  
 
<source lang="java">
 
<source lang="java">
public class TimeServiceRestClientContainer extends RestClientContainer {
+
public void start(BundleContext bundleContext) throws Exception {
 
+
Activator.context = bundleContext;
public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
+
// Register an instance of TimezoneNamespace
 
+
bundleContext.registerService(Namespace.class, new TimezoneNamespace(),
private IRemoteServiceRegistration reg;
+
null);
 
+
// Register an instance of TimezoneContainerTypeDescription (see class
TimeServiceRestClientContainer() {
+
// below)
// Create a random ID for the client container
+
bundleContext.registerService(ContainerTypeDescription.class,
super((RestID) IDFactory.getDefault().createID(
+
new TimezoneContainerTypeDescription(), null);
TimeServiceRestNamespace.NAME, "uuid:"
+
+ java.util.UUID.randomUUID().toString()));
+
// This sets up the JSON deserialization of the server's response.
+
// See below for implementation of TimeServiceRestResponseDeserializer
+
setResponseDeserializer(new TimeServiceRestResponseDeserializer());
+
 
}
 
}
  
public void connect(ID targetID, IConnectContext connectContext1)
+
class TimezoneContainerTypeDescription extends ContainerTypeDescription {
throws ContainerConnectException {
+
public TimezoneContainerTypeDescription() {
super.connect(targetID, connectContext1);
+
super(TimezoneClientContainer.CONTAINER_TYPE_NAME,
// Create the IRemoteCallable to represent
+
new TimezoneClientContainer.Instantiator(),
// access to the ITimeService method.
+
"Geonames Timezone Remote Service Client Container");
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);
+
}
+
 
+
public void disconnect() {
+
super.disconnect();
+
if (reg != null) {
+
reg.unregister();
+
reg = null;
+
 
}
 
}
 
}
 
}
 
class TimeServiceRestResponseDeserializer implements
 
IRemoteResponseDeserializer {
 
public Object deserializeResponse(String endpoint, IRemoteCall call,
 
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() {
 
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.
+
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.
  
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 entire/complete provider, along with all meta-data are available in the ECF Github repository [https://github.com/ECF/Geonames/tree/master/bundles/org.eclipse.ecf.provider.geonames.timezone.client here].
  
<source lang="java">
+
==Testing the Remote Service==
// 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
+
===Example Consumer/Application Code===
  
<source lang="java">
+
To test the above defined Timezone Client Provider, we can use an OSGi ServiceTracker to create some code that uses the '''ITimezoneService'''.  The Remote Service consumer (application) code is appropriately in a separate bundle from both the Timezone service bundle (which contains the '''ITimezoneService''' and '''ITimezoneServiceAsync''' interface classes), and the Client Provider bundle defined above.   Here is a ServiceTracker.addingService method implementation, where the proxy will be made available when the proxy is created
return new JSONObject(new String(responseBody)).get("time");
+
</source>
+
 
+
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
+
  
 
<source lang="java">
 
<source lang="java">
public class TimeServiceRestClientContainerInstantiator extends
+
public ITimezoneServiceAsync addingService(
RestClientContainerInstantiator {
+
ServiceReference<ITimezoneServiceAsync> reference) {
 
+
ITimezoneServiceAsync service = getContext().getService(reference);
private static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
+
System.out.println("Got ITimezoneServiceAsync");
 
+
// Get completable future and when complete
@Override
+
service.getTimezoneAsync(47.01, 10.2).whenComplete(
public IContainer createInstance(ContainerTypeDescription description,
+
(result, exception) -> {
Object[] parameters) throws ContainerCreateException {
+
// Check for exception and print out
// Create new container instance
+
if (exception != null) {
return new TimeServiceRestClientContainer();
+
System.out.println(exception.getMessage());
}
+
exception.printStackTrace();
 
+
} else
public String[] getImportedConfigs(ContainerTypeDescription description,
+
// Success!
String[] exporterSupportedConfigs) {
+
System.out.println("Received response:  timezone="+ result);
@SuppressWarnings("rawtypes")
+
});
List supportedConfigs = Arrays.asList(exporterSupportedConfigs);
+
// Report
// If the supportedConfigs contains the timeservice host config,
+
System.out.println("Returning ITimezoneServiceAsync");
// then we are the client to handle it!
+
return service;
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'''.
+
Once the '''ITimezoneServiceAsync''' proxy has been retrieved, the '''getTimezoneAsync''' method is called with parameters latitude=47.01 and longitude=10.2With the whenComplete method and Java8 lambda, we provide the block of code that is executed asynchronously when the remote call completes
 
+
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 timeserviceThese 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">
 
<source lang="java">
Dictionary<String, String> props = new Hashtable<String, String>();
+
(result, exception) -> {
props.put("service.exported.interfaces", "*");
+
// Check for exception and print out
// Specify the newly created com.mycorp.examples.timeservice.rest.host provider
+
if (exception != null) {
props.put("service.exported.configs","com.mycorp.examples.timeservice.rest.host");
+
System.out.println(exception.getMessage());
// Specify the 'id' parameter for the ID creation of the host (see
+
exception.printStackTrace();
// the TimeServiceServerContainerInstantiator.createInstance method
+
} else
props.put("com.mycorp.examples.timeservice.rest.host.id","http://localhost:8181");
+
// Success!
// Register a new TimeServiceImpl with the above props
+
System.out.println("Received response: timezone="+ result);
bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);
+
}
 
</source>
 
</source>
  
During registerService, the ECF RSA implementation does the following:
+
As from the above code, if an exception occurs then the message and stack trace are printed out.  If successful, the returned Timezone instance is printed out.
  
#Detects that the "service.exported.interfaces" property is set and so the service is to be exported
+
===Triggering Discovery===
#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
+
The Remote Service Admin specification standardizes the meta-data to dynamically discover remote services.  One way to trigger discovery at consumer runtime is to provide a file in the standardized format called '''EDEF''' for [[File-based Discovery with the Endpoint Description Extender Format | Endpoint Description Extender Format]].  For reference, here is the EDEF for the Timezone remote service
  
<source lang="java">
+
<source lang="xml">
http://localhost:8181/com.mycorp.examples.timeservice.ITimeService
+
<?xml version="1.0" encoding="UTF-8"?>
 +
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
 +
  <endpoint-description>
 +
    <property name="ecf.endpoint.id" value-type="String" value="http://api.geonames.org/"/>
 +
    <property name="ecf.endpoint.id.ns" value-type="String" value="ecf.geonames.timezone.namespace" />
 +
    <property name="ecf.endpoint.ts" value-type="Long" value="1387233380373"/>
 +
    <property name="endpoint.framework.uuid" value-type="String" value="20cc5d57-e8f0-0012-192b-c570b422d1f9"/>
 +
    <property name="endpoint.id" value-type="String" value="87d3ef4f-8e8f-4187-873e-166dcc58c9ea"/>
 +
    <property name="endpoint.package.version.org.geonames.timezone" value-type="String" value="1.0.0"/>
 +
    <property name="endpoint.service.id" value-type="Long" value="0"/>
 +
    <property name="ecf.exported.async.interfaces" value-type="String" value="*"/>
 +
    <property name="objectClass" value-type="String">
 +
      <array>
 +
        <value>org.geonames.timezone.ITimezoneService</value>
 +
      </array>
 +
    </property>
 +
    <property name="remote.configs.supported" value-type="String">
 +
      <array>
 +
        <value>ecf.container.client.geonames.timezone</value>
 +
      </array>
 +
    </property>
 +
    <property name="remote.intents.supported" value-type="String">
 +
      <array>
 +
        <value>passByValue</value>
 +
        <value>exactlyOnce</value>
 +
        <value>ordered</value>
 +
      </array>
 +
    </property>
 +
    <property name="service.id" value-type="Long" value="66"/>
 +
    <property name="service.imported" value-type="String" value="true"/>
 +
    <property name="service.imported.configs" value-type="String">
 +
      <array>
 +
        <value>ecf.container.client.geonames.timezone</value>
 +
      </array>
 +
    </property>
 +
  </endpoint-description>
 +
</endpoint-descriptions>
 
</source>
 
</source>
  
In the browser this will return the following json
+
and [https://github.com/ECF/Geonames/tree/master/bundles/org.eclipse.ecf.geonames.timezone.consumer.edef here] is the complete bundle containing this EDEF file.
  
<source lang="javascript">
+
===Running the Consumer/Application===
{"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
+
Now we may run this example by adding these four bundles to an OSGi Framework with ECF Remote Services installed
  
#Select a remote service consumer container by calling all container instantiator's '''getImportedConfigs''' method...with the value of '''exporterSupportedConfigs''' from the discovered EndpointDescription
+
#[https://github.com/ECF/Geonames/tree/master/bundles/org.geonames.timezone  org.geonames.timezone] - The Geonames Timezone API
#Create a new container via the selected container instantiator's '''createInstance''' method
+
#[https://github.com/ECF/Geonames/tree/master/bundles/org.eclipse.ecf.provider.geonames.timezone.client  org.eclipse.ecf.provider.geonames.timezone.client] - The Timezone Client Provider
#Call '''IContainer.connect(ID,IConnectContext)''' on the newly created container
+
#[https://github.com/ECF/Geonames/tree/master/bundles/org.eclipse.ecf.geonames.timezone.consumer org.eclipse.ecf.geonames.timezone.consumer] - The consumer/application example code (i.e. ServiceTracker.addingService method implementation)
#Create an ITimeService proxy
+
#[https://github.com/ECF/Geonames/tree/master/bundles/org.eclipse.ecf.geonames.timezone.consumer.edef org.eclipse.ecf.geonames.timezone.consumer.edef] - The bundle containing the EDEF file used to trigger discovery
#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:
+
Once started, we can trigger the discovery of the Timezone remote service by simply starting the '''org.eclipse.ecf.geonames.timezone.consumer.edef''' bundle (20)
  
<source lang="java">
+
<pre>
http://localhost:8181/com.mycorp.examples.timeservice.ITimeService
+
osgi> start 20
</source>
+
Got ITimezoneServiceAsync
 +
Returning ITimezoneServiceAsync
 +
Received response: timezone=Timezone [countryCode=SO, countryName=Somalia, latitude=10.2, longitude=47.01, timezoneId=Africa/Mogadishu, dstOffset=3.0, gmtOffset=3.0, rawOffset=3.0, time=Tue Dec 09 22:23:00 PST 2014, sunriseTime=Tue Dec 09 05:57:00 PST 2014, sunsetTime=Tue Dec 09 17:30:00 PST 2014]
 +
osgi>
 +
</pre>
  
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 can be seen from the order of the above lines of output, the 'Returning ITimezoneServiceAsync' is printed prior to the 'Received response: ...' line, indicating that the whenComplete block was executed after executing the remote http get call to the api.geonames.org timezone service.
  
As with the [[Tutorial:_Building_your_first_OSGi_Remote_Service]] the consumer code is simply
+
The complete source and meta-data for all of above are located in the [https://github.com/ECF/Geonames ECF Geonames Github Repository].
  
<source lang="java">
+
==Background and Related Articles==
package com.mycorp.examples.timeservice.consumer.ds;
+
  
import com.mycorp.examples.timeservice.ITimeService;
+
[[Tutorial: Creating a RESTful Remote Service Provider | Creating a RESTful Remote Service Provider ]]
 
+
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>
+
 
+
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.
+
 
+
==Background and Related Articles==
+
  
 
[[Getting Started with ECF's OSGi Remote Services Implementation]]
 
[[Getting Started with ECF's OSGi Remote Services Implementation]]
Line 595: Line 477:
  
 
[[EIG:Add to Target Platform|How to Add Remote Services/RSA to Your Target Platform]]
 
[[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 15:57, 9 December 2014


Introduction

Remote Services are usually defined with both a 'host' component (aka 'server') and a 'consumer' component (aka 'client'). The host typically implements and exports a service for remote access, and the consumer discovers, imports, and then uses the remote service by calling methods on the service interface. 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 a number of other tutorials and examples showing 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. These services are frequently not running within an OSGi framework, nor are implemented in Java, and therefore not exposed as a Remote Service host. For many such existing services, it would be valuable 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, nor even necessarily be implemented in Java.

With any Remote Service, what the consumer sees at the time of use is a dynamically-constructed proxy implementing one or more service interfaces. ECF's RSA implementation provides open, community-developed APIs allowing full customization of the both the creation and runtime behavior of the proxy, further enabling the creation of a 'consumer/client-only' remote service provider capable of interacting with any Internet-based service.

The use of a 'consumer/client-only' provider has several important advantages for consumer application development

  1. Existing server/services don't change, and may be implemented in any language, using any framework or service infrastructure
  2. Existing clients don't change, and may continue to be implemented in any language, using any framework
  3. This Remote Service consumer can automatically benefit from advantages of using 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 service-level type-safe security
    6. Standardized management agent via Remote Service Admin (RSA)
    7. Synchronous and/or asynchronous/non-blocking access to the remote service without any implementation changes
    8. Automatic integration with/use of a variety of injection frameworks (e.g. Spring/Blueprint, Declarative Services, others)
    9. Automatic integration with any framework that uses OSGi Services
    10. Use of open standards (OSGi Remote Services/RSA) and open, community-developed implementations (ECF Remote Services) to avoid vendor lock-in and provide easy customization

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.

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.

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

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.

Remote Service Admin Integration

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
		 */
		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;
		}
		/**
		 * 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 summarized as: 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);

This line passes the RSA-created targetID to the RestClientContainer super class, which simply 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 automatically construct the 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 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 setting the request to use HTTP GET.

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 deserialization code may do whatever is appropriate (e.g. parse xml, parse JSON, deserialize java Objects, use some other serialization technique like protocol buffers, etc) to convert from appropriate remote response (JSON) to an instance of 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.

The entire/complete provider, along with all meta-data are available in the ECF Github repository here.

Testing the Remote Service

Example Consumer/Application Code

To test the above defined Timezone Client Provider, we can use an OSGi ServiceTracker to create some code that uses the ITimezoneService. The Remote Service consumer (application) code is appropriately in a separate bundle from both the Timezone service bundle (which contains the ITimezoneService and ITimezoneServiceAsync interface classes), and the Client Provider bundle defined above. Here is a ServiceTracker.addingService method implementation, where the proxy will be made available when the proxy is created

public ITimezoneServiceAsync addingService(
		ServiceReference<ITimezoneServiceAsync> reference) {
	ITimezoneServiceAsync service = getContext().getService(reference);
	System.out.println("Got ITimezoneServiceAsync");
	// Get completable future and when complete
	service.getTimezoneAsync(47.01, 10.2).whenComplete(
			(result, exception) -> {
				// Check for exception and print out
				if (exception != null) {
					System.out.println(exception.getMessage());
					exception.printStackTrace();
				} else
					// Success!
					System.out.println("Received response:  timezone="+ result);
			});
	// Report
	System.out.println("Returning ITimezoneServiceAsync");
	return service;
}

Once the ITimezoneServiceAsync proxy has been retrieved, the getTimezoneAsync method is called with parameters latitude=47.01 and longitude=10.2. With the whenComplete method and Java8 lambda, we provide the block of code that is executed asynchronously when the remote call completes

	(result, exception) -> {
		// Check for exception and print out
		if (exception != null) {
			System.out.println(exception.getMessage());
			exception.printStackTrace();
		} else
			// Success!
			System.out.println("Received response:  timezone="+ result);
	}

As from the above code, if an exception occurs then the message and stack trace are printed out. If successful, the returned Timezone instance is printed out.

Triggering Discovery

The Remote Service Admin specification standardizes the meta-data to dynamically discover remote services. One way to trigger discovery at consumer runtime is to provide a file in the standardized format called EDEF for Endpoint Description Extender Format. For reference, here is the EDEF for the Timezone remote service

<?xml version="1.0" encoding="UTF-8"?>
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
    <property name="ecf.endpoint.id" value-type="String" value="http://api.geonames.org/"/>
    <property name="ecf.endpoint.id.ns" value-type="String" value="ecf.geonames.timezone.namespace" />
    <property name="ecf.endpoint.ts" value-type="Long" value="1387233380373"/>
    <property name="endpoint.framework.uuid" value-type="String" value="20cc5d57-e8f0-0012-192b-c570b422d1f9"/>
    <property name="endpoint.id" value-type="String" value="87d3ef4f-8e8f-4187-873e-166dcc58c9ea"/>
    <property name="endpoint.package.version.org.geonames.timezone" value-type="String" value="1.0.0"/>
    <property name="endpoint.service.id" value-type="Long" value="0"/>
    <property name="ecf.exported.async.interfaces" value-type="String" value="*"/>
    <property name="objectClass" value-type="String">
      <array>
        <value>org.geonames.timezone.ITimezoneService</value>
      </array>
    </property>
    <property name="remote.configs.supported" value-type="String">
      <array>
        <value>ecf.container.client.geonames.timezone</value>
      </array>
    </property>
    <property name="remote.intents.supported" value-type="String">
      <array>
        <value>passByValue</value>
        <value>exactlyOnce</value>
        <value>ordered</value>
      </array>
    </property>
    <property name="service.id" value-type="Long" value="66"/>
    <property name="service.imported" value-type="String" value="true"/>
    <property name="service.imported.configs" value-type="String">
      <array>
        <value>ecf.container.client.geonames.timezone</value>
      </array>
    </property>
  </endpoint-description>
</endpoint-descriptions>

and here is the complete bundle containing this EDEF file.

Running the Consumer/Application

Now we may run this example by adding these four bundles to an OSGi Framework with ECF Remote Services installed

  1. org.geonames.timezone - The Geonames Timezone API
  2. org.eclipse.ecf.provider.geonames.timezone.client - The Timezone Client Provider
  3. org.eclipse.ecf.geonames.timezone.consumer - The consumer/application example code (i.e. ServiceTracker.addingService method implementation)
  4. org.eclipse.ecf.geonames.timezone.consumer.edef - The bundle containing the EDEF file used to trigger discovery

Once started, we can trigger the discovery of the Timezone remote service by simply starting the org.eclipse.ecf.geonames.timezone.consumer.edef bundle (20)

osgi> start 20
Got ITimezoneServiceAsync
Returning ITimezoneServiceAsync
Received response:  timezone=Timezone [countryCode=SO, countryName=Somalia, latitude=10.2, longitude=47.01, timezoneId=Africa/Mogadishu, dstOffset=3.0, gmtOffset=3.0, rawOffset=3.0, time=Tue Dec 09 22:23:00 PST 2014, sunriseTime=Tue Dec 09 05:57:00 PST 2014, sunsetTime=Tue Dec 09 17:30:00 PST 2014]
osgi>

As can be seen from the order of the above lines of output, the 'Returning ITimezoneServiceAsync' is printed prior to the 'Received response: ...' line, indicating that the whenComplete block was executed after executing the remote http get call to the api.geonames.org timezone service.

The complete source and meta-data for all of above are located in the ECF Geonames Github Repository.

Background and Related Articles

Creating a RESTful Remote Service Provider

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

Copyright © Eclipse Foundation, Inc. All Rights Reserved.