Skip to main content

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

Jump to: navigation, search

Difference between revisions of "Tutorial: Creating Custom Distribution Providers"

(Created page with "<!-- This CSS wraps code blocks in pretty boxes --> <css> .mw-code { background-color: #fafafa; padding: 20px; border-color: #ddd; border-width: 3px; border-sty...")
 
(Step 2: Server)
 
(22 intermediate revisions by the same user not shown)
Line 74: Line 74:
 
==Introduction==
 
==Introduction==
  
ECF provides a modular implementation of the OSGi Remote Services and Remote Service Admin specifications.  It does this by defining defining two APIs corresponding to the two major subsystems defined by RSA:  discovery and distribution. In RSA, discovery is the function of finding remote services that may be imported and used.   Discovery is done by communicating service metadata from the service implementation to the service consumer. This communication can be static (e.g. via xml file) or dynamically via an arbitrary network protocol. 
+
The ECF project provides an implementation of the OSGi R6 [https://www.osgi.org/developer/specifications/ Remote Services and Remote Service Admin specifications]The RSA specification defines two major subsystems:  discovery and distribution.   Discovery concerns finding remote services exported by other processes on the network. The distribution subsystem is responsible for the actual communication of invoking a remote call:  serializing remote method parameters, communicating with the remote service host via some network transport protocol, unmarshalling and invoking the service method with provided parameters, and returning a result to the consumer.
  
The distribution subsystem provides the ability to actually make the remote call: marshalling of method name and parameters, communicating with the remote service host via some network transport protocol, unmarshalling and invoking the service method with provided parameters, and returning a result to the consumer.
+
ECF's implementation of RSA defines an API to create new distribution providers.  This API is declared in the [[Distribution_Providers#Remote_Services_API | ECF remote services API, provided by the org.eclipse.ecf.remoteservices bundle]].  Custom distribution providers implement a portion of this API and then will be used at runtime to supply the necessary functions of the distribution provider.
  
ECF's RSA implementation provides APIs for both discovery and distribution.  For distribution, this ECF API is called the remote services API, and is found in the bundle:  org.eclipse.ecf.remoteservices.  ECF has the concept of a distribution provider, which is a bundle or bundles that implement the ECF remote services API, and thus can be used by ECF's RSA implementation to provide compliant Remote Services and Remote Service Admin functionality.
+
This tutorial will describe the creation of a simple custom distribution provider using the relevant portions of the ECF remote services API.
  
This tutorial will walk through the creation of a simple distribution provider.  Although there are many existing [[Distribution_Providers | distribution providers]], there will continue to be new protocols (e.g. MQTT), new architectural communication styles (e.g. REST), and new serialization formats (e.g. JSON, or Protocol Buffers), as well as application or system-specific requirements for security, interoperability, and integration.  ECF's pluggable provider approach allows the easy creation of arbitrary distribution providers.
+
==Remote Service Containers, IDs, and Namespaces==
  
 +
ECF has the concept of a 'container' ([http://download.eclipse.org/rt/ecf/3.13.0/javadoc/org/eclipse/ecf/core/IContainer.html IContainer]), which is an object instance that implements the remote services API and represents a network-accessible endpoint.
  
 +
Containers have unique transport-specific [http://download.eclipse.org/rt/ecf/3.13.0/javadoc/org/eclipse/ecf/core/identity/ID.html ID].  Some examples of transport-specific container IDs:<br>
  
It's currently popular to create REST-based networked services.   This is understandable, as the ubiquity http/https, the simplicity of the REST approach, and the availability of open, cross-language object serialization formats like JSON and XML, along with the availability of quality distribution frameworks all make it easier than ever to define, implement, and deploy a service and it's associated API.
+
https://myhost.com/v1/path/to/service<br>
 +
ecftcp://localhost:3282/server<br>
 +
mqtt://mybroker.com/mytopic<br>
 +
jms:tcp://jmsbroker.com:6686/jmstopic<br>
 +
r_osgi://somehost/<br>
 +
hazelcast:///mytopicname<br>
 +
g6YuiWAjkk34je1lJlNmv==<br>
 +
uuid:de305d54-75b4-431b-adb2-eb6b9e546014<br>
 +
241<br>
  
One thing that is relatively new, however, is that there are now open standards that deal with two levels of concerns
+
Each ID instance must be unique within a [http://download.eclipse.org/rt/ecf/latest/javadoc/org/eclipse/ecf/core/identity/Namespace.html Namespace].  Distribution providers must define a new Namespace that enforces the expected syntax requirements of the endpoint identifier.
  
#Transport/Distribution-Level Concerns:   What protocol and serialization format are to be used?  How to support cross-language type systems?  How to be as bandwidth efficient and performant as possible?  How to handle network failure? etc.
+
==Step 1: Namespace==
#Service-Level Concerns:  How to discover the service?  How to version the service?  How to handle service dynamics?  How to secure the service?  How to describe and document the service so that others may easily understand and use it?  How to combine services and have service-level dependencies in a dynamic environment? etc.
+
  
==Transport and Distribution Concerns==
+
The first thing a distribution provider must do is to register a new type of Namespace.  ECF provides a number of Namespace classes that can be extended to make this easy.  For example, the URIIDNamespace can handle any ID syntax that can be represented as a URI, like all of the above.  Here is an example Namespace class that extends the URIIDNamespace:
  
From the service implementer's perspective, the selection of a framework or implementation makes de-facto design choices about transport or distribution concerns.  For example, some REST frameworks use XML, some JSON, some support both.  However, once such a framework is chosen the service implemented and deployed, it can become very difficult, time-consuming, or even technically impossible to change to use other frameworks or distribution systems. 
+
<source lang="java">
 +
public class Example1Namespace extends URIIDNamespace {
  
For REST-based services, however, standards are beginning to emerge that allow REST services to be defined and implemented without making a specific choice of distribution system or framework.  One example in Java is [https://en.wikipedia.org/wiki/Java_API_for_RESTful_Web_Services JAX Remote Services] or JAX-RS.  JAX-RS defines/standardizes Java annotations for classes and methods that are to be exposed for remote access.  JAX-RS implementations then use these annotations to 'export' the service (make it available for remote access) and handle the mapping between http/https requests, and the appropriate method invocation.  For example, here's a small 'hello-world' service that uses JAX-RS annotations:
+
private static final long serialVersionUID = 2460015768559081873L;
  
<source lang="java">
+
public static final String NAME = "ecf.example1.namespace";
import javax.ws.rs.GET;
+
private static final String SCHEME = "ecf.example1";
import javax.ws.rs.Produces;
+
private static Example1Namespace INSTANCE;
import javax.ws.rs.Path;
+
 +
/**
 +
* The singleton instance of this namespace is created (and registered
 +
* as a Namespace service) in the Activator class for this bundle.
 +
* The singleton INSTANCE may then be used by both server and client.
 +
*/
 +
public Example1Namespace() {
 +
super(NAME, "Example 1 Namespace");
 +
INSTANCE = this;
 +
}
  
// The Java class will be hosted at the URI path "/helloworld"
+
public static Example1Namespace getInstance() {
@Path("/helloworld")
+
return INSTANCE;
public class HelloWorldResource implements HelloWorldService {
+
}
   
+
    // The Java method will process HTTP GET requests
+
@Override
    @GET
+
public String getScheme() {
    // The Java method will produce content identified by the MIME Media
+
return SCHEME;
    // type "text/plain"
+
}
    @Produces("text/plain")
+
    public String getMessage() {
+
        // Return some textual content
+
        return "Hello World";
+
    }
+
 
}
 
}
 
</source>
 
</source>
  
One very useful aspect of the JAX-RS annotations is that multiple implementations may exist, and it is relatively easy to switch from one implementation to another.
+
In bundle Activator start, the singleton Example1Namespace is created and registered:
  
==Service-Level Concerns==
+
<source lang="java">
 +
// Create and register the Example1Namespace
 +
context.registerService(org.eclipse.ecf.core.identity.Namespace.class, new Example1Namespace(),  null);
 +
</source>
  
Although frequently of secondary concern to service implementers, service-level concerns are typically of primary importance to service '''consumers'''. For example, before I can use any REST-based service, I have to know about (discover) the service and needed meta-data: the service location (i.e. it's URL), what methods exist and arguments are required, and what can be expected in response. Additionally, what version of the service is being accessed, what credentials/security are required, what are the run-time qualities of the remote service?
+
The same Example1Namespace type must be used by both servers and clients, and so it typically makes sense to define the Namespace in a small common bundle, which can be deployed on both servers and clients.   For the source code for this example, see the [https://github.com/ECF/ExampleRSADistributionProviders/tree/master/bundles/org.eclipse.ecf.example1.provider.dist.common bundle here].
  
===OSGi Remote Services===
+
Note that other type of ID syntax can be easily supported by either inheriting from other Namespace classes (e.g. LongIDNamespace, StringIDNamespace, UUIDNamespace, etc.), or creating one's own Namespace subclass.
  
In recent releases the OSGi Alliance has defined a specification called [http://www.osgi.org/Specifications/HomePage Remote Services] (chapter 100 in the enterprise spec).  Along with the Remote Service Admin specification (chapter 122 in enterprise spec), Remote Services defines ways to express answers to service-level concerns in an implementation-independent way, similar to what JAX-RS does for transport-level concerns.  For example, the Remote Service spec allows meta-data for service type, service version, service endpoint/URL identification, security, and quality of service properties to be communicated between service implementer and service consumer without referring to any Remote Services implementation.  Further, Remote Services/RSA defines a way to dynamically discover and un-discover remote services, allowing for easy consumer support for networked and frequently unreliable services.
+
==Step 2: Server==
  
Several implementations of OSGi Remote Services exist, including open source [https://wiki.eclipse.org/ECF#OSGi_Remote_Services ECF], [http://cxf.apache.org/ CXF], [http://www.amdatu.org/ Amdatu] and commercial implementations.
+
In addition to registering a new Namespace, distribution providers must implement and register the [http://download.eclipse.org/rt/ecf/3.13.0/javadoc/org/eclipse/ecf/remoteservice/provider/IRemoteServiceDistributionProvider.html IRemoteServiceDistributionProvider] interface using the whiteboard pattern.
  
OSGi RSA has the concept of a distribution system responsible for exporting a local service and making it available for remote access.  According to the specification, many distributions systems may be used even for a single service.  ECF's implementation is unique because it has an open API for creating/adding new distribution providers.  Currently, ECF committers have implemented the following distribution providers
+
For example, here is the code for registering a new custom distribution provider server
  
*[https://www.eclipse.org/ecf/downloads.php ECF 'generic']
+
<source lang="java">
*[https://www.eclipse.org/ecf/downloads.php r-OSGi]
+
public static final String SERVER_ID_PARAMETER = "id";
*[https://github.com/ECF/JMS Java Messaging Service (JMS)]
+
public static final String SERVER_ID_PARAMETER_DEFAULT = "tcp://localhost:3333";
*[https://github.com/ECF/Mqtt-Provider MQTT]
+
// register this remote service distribution provider
*[https://github.com/ECF/HazelcastProvider Hazelcast]
+
context.registerService(IRemoteServiceDistributionProvider.class,
*[https://github.com/ECF/JGroups JavaGroups]
+
  new RemoteServiceDistributionProvider.Builder().setName(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE)
*[https://github.com/ECF/JaxRSProviders Jax-RS/Jersey and Jax-RS/CXF ]
+
      .setInstantiator(new RemoteServiceContainerInstantiator(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,
 +
        ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) {
 +
        @Override
 +
        public IContainer createInstance(ContainerTypeDescription description,Map<String, ?> parameters) {
 +
            // Create and configure an instance of our server
 +
            // container type
 +
            return new Example1ServerContainer(getIDParameterValue(Example1Namespace.getInstance(), parameters,
 +
SERVER_ID_PARAMETER, SERVER_ID_PARAMETER_DEFAULT));
 +
        }
 +
  }).build(),
 +
null);
 +
</source>
  
==Jax-RS with OSGi Remote Services==
+
Notes for the above code<br>
  
Since ECF's RSA implementation implements the OSGi RS and RSA specifications, and Jersey implements the Jax-RS specification, it's possible create and export a remote service that deals with both the transport and service-level concerns in a way completely standardized, and not dependent upon either the OSGi RS/RSA implementation (e.g. ECF), nor on the distribution system provider implementation (Jersey).
+
The RemoteServiceDistributionProvider.Builder class is used to create the IRemoteServiceDistributionProvider instance. <br>
 +
<br>
 +
The <b>setName(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE)</b> method on the Builder sets the distribution provider's name (ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE==ecf.example1.provider.dist.server).
  
For example, here is some example OSGi code for exporting a remote service using the [https://github.com/ECF/JaxRSProviders Jax-RS/Jersey] provider:
+
This name corresponds to the OSGi Remote Service config type, and must uniquely identify the distribution provider.  When a remote service is registered, the standard service property <b>service.exported.configs</b> may be specified so that the remote service will be exported via this distribution provider rather than some other distribution provider.  For example:
  
 
<source lang="java">
 
<source lang="java">
BundleContext bundleContext;
+
Hashtable serviceProperties = new Hashtable();
Dictionary<String, String> props = new Hashtable<String,String>();
+
serviceProperties.put("service.exported.interfaces","*");
// osgi rs property to signal to distribution system
+
serviceProperties.put("service.exported.configs","ecf.example1.provider.dist.server");
// that this is a remote service
+
...
props.put("service.exported.interfaces","*");
+
context.registerService(MyService.class,serviceInstance,serviceProperties);
// specify the distribution provider with osgi rs property
+
props.put("service.exported.configs", "ecf.jaxrs.jersey.server");
+
// as per spec, <provider>.<prop> represents a property intended for use by this provider
+
props.put("ecf.jaxrs.jersey.server.alias", "/jersey");
+
// register (and export) HelloImpl as remote service described by Hello interface
+
bundleContext.registerService(HelloWorldService.class, new HelloWorldResource(), props);
+
 
</source>
 
</source>
  
HelloWorldResource implements a HelloWorldService interface which is defined
+
The underlying ECF RSA implementation will export the serviceInstance via the ecf.example2.provider.dist.server distribution provider.
 +
 
 +
'''NOTE:''' As above, the IRemoteServiceDistributionProvider should be registered '''before''' any remote services using these distribution providers are exported.  In other words, if you create and register a IRemoteServiceDistributionProvider with name 'ecf.example1.provider.dist.server' the provider implementation bundle should be started and the IRemoteServiceDistributionProvider service should be registered <b>prior</b> to registering the service to be exported using that provider.  If the distribution provider is not registered prior to the remote service export, then the correct
 +
distribution provider will not be available to do the export.
 +
 
 +
The other required part of the distribution provider is the container instantiator:
  
 
<source lang="java">
 
<source lang="java">
import javax.ws.rs.GET;
+
      .setInstantiator(new RemoteServiceContainerInstantiator(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,
import javax.ws.rs.Produces;
+
        ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) {
import javax.ws.rs.Path;
+
        @Override
 +
        public IContainer createInstance(ContainerTypeDescription description,Map<String, ?> parameters) {
 +
            // Create and configure an instance of our server
 +
            // container type
 +
            return new Example1ServerContainer(getIDParameterValue(Example1Namespace.getInstance(), parameters,
 +
SERVER_ID_PARAMETER, SERVER_ID_PARAMETER_DEFAULT));
 +
        }
 +
  })
 +
</source>
  
// The Java class will be hosted at the URI path "/helloworld"
+
This code uses the RemoteServiceContainerInstantiator class.  The two constants in the constructor (ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) sets up the instantiator so that these two config types (server and client for distribution provider) are associated with each other so at remote service consumer import time the ECF RSA implementation can select the appropriate client config type associated with the exporting server config type.  For example, when ecf.example1.provider.dist.server is used as the exporting config type, then on the client/consumer the selected config type will be:  ecf.example1.provider.dist.client
@Path("/helloworld")
+
<br>
public interface HelloWorldService {
+
The createInstance method then creates an instance of the appropriate container type.  This method allows the container to be created and configured based upon a Map of parameters.  As per the RSA specification, if the remote service registration has service properties with keys of the form:
   
+
<br>
    // The Java method will process HTTP GET requests
+
<config type>.<param>
    @GET
+
<br>
    // The Java method will produce content identified by the MIME Media
+
then the provided parameters Map will contain entries of the form
    // type "text/plain"
+
<br>
    @Produces("text/plain")
+
key: <param>=<value>
    public String getMessage();
+
<br>
}
+
For example, if the remote service is registered:
 +
<source lang="java">
 +
Hashtable serviceProperties = new Hashtable();
 +
serviceProperties.put("service.exported.interfaces","*");
 +
serviceProperties.put("service.exported.configs","ecf.example1.provider.dist.server");
 +
serviceProperties.put("ecf.example1.provider.dist.server.id","tcp://localhost:3333");
 +
...
 +
context.registerService(MyService.class,serviceInstance,serviceProperties);
 
</source>
 
</source>
  
Note that the HelloWorldService has exactly the same Jax-RS annotations as the HelloWorldResource implementation class.
+
then the parameters Map will contain an entry with key="id" and value="tcp://localhost:3333".  Such values can be used to configure the provider.  In the case of the Example1 provider above, the id parameter is used to create an ID via the call to getIDParameterValue.  Other distribution providers can define arbitrary parameters used to configure the underlying transport such as host, port, bind address, path, etc.
 +
<br>
 +
At the appropriate time during export, the createInstance method will be called and the Example1ServerContainer instance created.  Here is the implementation of Example1Container
 +
<source lang="java">
 +
/**
 +
* Instances of this class are created via the container instantiator specified
 +
* by the RemoteServiceDistributionProvider whiteboard service in the Activator.
 +
* @see Activator#start(org.osgi.framework.BundleContext)
 +
*
 +
*/
 +
public class Example1ServerContainer extends AbstractRSAContainer {
  
With the Jax-RS/Jersey distribution provider, the above registerService call will dynamically export the remote service (via Jersey) so that remote clients (Java/OSGi-based or not) can access getMessage by sending a GET to an URL like this:
+
/**
 +
* Create an Example1ServerContainer.  The given id must not be null,
 +
* and should be created using the Example1Namespace.  The server's ID
 +
* will then be automatically used to provide the ecf.endpoint.id to
 +
* remote service clients via the RSA-created EndpointDescription.
 +
* @param id
 +
*/
 +
public Example1ServerContainer(ID id) {
 +
super(id);
 +
}
  
curl http://localhost:8080/jersey/helloworld/1
+
 +
@Override
 +
/**
 +
* When the ECF RSA impl is requested to export a remote service and
 +
* this container instance is selected, a remote service registration will be
 +
* created by RSA and this method will then be called to actually export the given
 +
* registration via some communications transport.  This should trigger the appropriate
 +
* networking initialization (e.g. opening listener on socket), given the info in the
 +
* registration.
 +
* The given registration will not be <code>null</code>.
 +
* If the transport would like to insert properties into the EndpointDescription
 +
* for this endpoint, a Map of name (String) -> Object map should be returned.  All
 +
* the values in the Map should be Serializable.  This provides a mechanism for
 +
* distribution providers to include arbitrary private properties for use by
 +
* clients.
 +
*
 +
* Note that keys for the Map should be unique to avoid conflicting with any other properties.
 +
*
 +
* If values are provided for either OSGI RemoteConstants or ECF Remote Services Constants
 +
* then these new values will <b>override</b> the default values in the EndpointDescription.
 +
*
 +
*/
 +
protected Map<String, Object> exportRemoteService(RSARemoteServiceRegistration registration) {
 +
// Here would be the code to export/make a remote service on the actual transport
 +
                // For example, starting a listener to handle connections, and responding to
 +
                // connection attempts, handling remote calls, etc.
 +
                // the registration parameter contains the services interfaces, the implementation, and the
 +
                // service properties passed in when the remote service is registered
 +
                // This method will be called by the same thread that calls BundleContext.registerService
 +
return null;
 +
}
  
Would respond with "Hello World"
+
@Override
 +
/**
 +
* When this remote service is unregistered, this method will be called by ECF RSA
 +
* to allow the underlying transport to unregister the remote service given
 +
* by the registration.  Necessary clean-up/shutdown of transport should be completed
 +
* before returning.
 +
*/
 +
protected void unexportRemoteService(RSARemoteServiceRegistration registration) {
 +
// This method will be called when the service registration unregister()
 +
                // method is called, and is responsible for cleaning up/shutting down
 +
                // and network resources associated with this remote service
 +
                // This method will be called by the same thread that is calling
 +
                // ServiceRegistration.unregister()
 +
}
 +
}
 +
</source>
 +
Once this container instance is created the ECF RSA implementation will then call <b>exportRemoteService(RSARemoteServiceRegistration registration)</b>
 +
and expect that upon successful completion that the distribution provider will have made the remote service described by the given registration will have been made available for remote access.  The exportRemoteService implementation is therefore where the distribution provider should actually make the remote service available over the network (e.g. by listening on socket, joining communication group, etc).  When the remote service is unregistered, the <b>unexportRemoteService(RSARemoteServiceRegistration registration)</b> will be called to allow the transport to close and clean up any network resources associated with the previous export. 
 +
<br>
 +
Each method will be called for each remote service export, with a unique RSARemoteServiceRegistration passed in by the ECF RSA implementation.
 +
<br>
 +
For the exportRemoteService method, the implementation can return a Map of service properties (or null).  If a Map instance is returned, the values in the given map will be merged with the other endpoint description properties defined/required by ECF's RSA implementation.  For example, one required ECF remote service property is ecf.endpoint.id.  The default value of the ecf.endpoint.id is the exporting container's ID...e.g. by default
 +
<br>
 +
ecf.endpoint.id will be set to: "tcp://localhost:3333" because the Example1Container's ID is set by construction to "tcp://localhost:3333".  However, if the returned Map is non null, and has the key ecf.endpoint.id, the value in the returned map will override the default value.  e.g.
 +
<source lang="java">
 +
protected Map<String, Object> exportRemoteService(RSARemoteServiceRegistration registration) {
 +
// TODO setup transport and export given remote service registration
 +
                Map<String,Object> result = new HashMap<String,Object>();
 +
                // This will override the ecf.endpoint.id value in the RSA endpoint description
 +
                result.put("ecf.endpoint.id","http://somehost:2222/foo/");
 +
                // This will add a brand new property to the RSA endpoint description
 +
                result.put("my.new.property","howdy");
 +
return result;
 +
}
 +
</source>
  
The use of Jax-RS annotations to implement an ECF distribution provider, and the use of OSGi Remote Services has several important advantages for the service implementer and the service consumer:
+
The complete source for the distribution provider server is available [https://github.com/ECF/ExampleRSADistributionProviders/tree/master/bundles/org.eclipse.ecf.example1.provider.dist.server here].
  
#An ability to flexibly handle both transport-level and service-level concerns in a way appropriate to the application
+
==Step 3: Client==
#A clean separation between service contract (HelloWorldService interface), and service implementation (HelloWorldResource)
+
#A clean separation between application concerns from transport '''or''' service-level concerns
+
#Alternative implementations of Jax-RS and/or OSGi RS/RSA may be substituted at any time without application changes
+
  
==Using the Remote Service==
+
Like the server, the distribution provider client is registered via the whiteboard pattern using the IRemoteServiceDistributionProvider service interface.  Here is the code for registering the Example1 client distribution provider
 +
<source lang="java">
 +
context.registerService(IRemoteServiceDistributionProvider.class,
 +
new RemoteServiceDistributionProvider.Builder().setName(ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE)
 +
.setInstantiator(
 +
new RemoteServiceContainerInstantiator(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,
 +
ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) {
 +
@Override
 +
public IContainer createInstance(ContainerTypeDescription description,
 +
Map<String, ?> parameters) {
 +
        // Create and configure an instance of our client
 +
        // container type (below)
 +
// This is called by RSA when a client container of this type
 +
// is needed (e.g. to import a discovered remote service)
 +
return new Example1ClientContainer();
 +
}
 +
}).build(),
 +
null);
 +
</source>
  
Since the HelloWorldService is exported via Jax-RS/Jersey provider, it may be accessed by any client (e.g. javascript, java, curl, etc) that can access via http calls.  If, however, it is an OSGi client (or a non-OSGi Java Client) ECF Remote Service can automatically construct a proxy for the remote service, and make the proxy available as an OSGi Service on the client.  For a description and example of doing this, see
+
As with the server, the setName and setInstantiator builder methods are required.  For this distribution provider all that's necessary is to return a new Example1ClientContainer which is declared
[[Tutorial:_Exposing_a_Jax_REST_service_as_an_OSGi_Remote_Service | Exposing a Jax REST Service as an OSGi Service]].
+
<source lang="java">
 +
public class Example1ClientContainer extends AbstractRSAClientContainer {
  
==A More Complete Example==
+
public Example1ClientContainer() {
 +
super(Example1Namespace.getInstance()
 +
.createInstance(new Object[] { "uuid:" + java.util.UUID.randomUUID().toString() }));
 +
}
  
A more complex example exists in the [https://github.com/ECF/JaxRSProviders Jax-RS/Jersey Provider] repo.  The remote service host/server is in the [https://github.com/ECF/JaxRSProviders/tree/master/examples/com.mycorp.examples.student.remoteservice.host com.mycorp.examples.student.remoteservice.host] bundle.  The remote service consumer/client is in [https://github.com/ECF/JaxRSProviders/tree/master/examples/com.mycorp.examples.student.client com.mycorp.examples.student.client] bundle.  Notice that neither of these bundles has references to ECF, Jersey, or even OSGi classes, but rather only to Jax-RS standard annotation types (javax.*) and model classes defined in the [https://github.com/ECF/JaxRSProviders/tree/master/examples/com.mycorp.examples.student com.mycorp.examples.student] bundle.
+
@Override
 +
// This is called when when a remote service instance is actually needed by consumers
 +
// The remote service getProxy method will be called the first time the proxy is  
 +
// to be created for a given consumer
 +
protected IRemoteService createRemoteService(RemoteServiceClientRegistration registration) {
 +
return new AbstractRSAClientService(this, registration) {
  
==Background and Related Articles==
+
@Override
 +
public Object getProxy(ClassLoader cl, @SuppressWarnings("rawtypes") Class[] interfaces) throws ECFException {
 +
return super.getProxy(cl, interfaces);
 +
}
 +
 +
@Override
 +
protected Object invokeAsync(RSARemoteCall remoteCall) throws ECFException {
 +
// TODO Auto-generated method stub
 +
return null;
 +
}
 +
// invokeSync will be called when a client needs to make a remote call on
 +
// one of the proxy methods.
 +
@Override
 +
protected Object invokeSync(RSARemoteCall remoteCall) throws ECFException {
 +
// TODO Auto-generated method stub
 +
return null;
 +
}
 +
};
 +
}
 +
}
 +
</source>
  
[[Tutorial: Exposing a Jax REST service as an OSGi Remote Service]]
+
When the ECF RSA discovers a remote service and attempts to import it, the <b>createRemoteService</b> method is called with the associated RemoteServiceClientRegistration provided.
 +
<br>
 +
In the above case an instance of AbstractRSAClientService is created for the import.  Sometime later when the proxy is actually required by a consumer, the <b>getProxy</b> method will be called.  The code above overrides the implementation of <b>getProxy</b> to show that this process can be easily customized for a distribution provider, but in most cases the AbstractRSAClientService proxy creation can be used.
 +
<br>
 +
The <b>invokeSync</b> and <b>invokeAsync</b> method are called when the proxy methods are actually invoked by the consumer.  The invokeSync method is called when the proxy methods are called synchronously (normal case), and the invokeAsync is called when the [[Asynchronous_Proxies_for_Remote_Services | ECF async proxy]] methods are invoked.  For the invokeSync method, the distribution provider should implement this method to make the appropriate remote call by serializing remote call arguments, sending over the distribution provider's transport, getting and deserializing the result, and returning the Object that represents the appropriate type for proxy method defined in the RSARemoteCall instance.  In invokeSync the calling thread should block until the remote call is made and the response is received, an exception occurs, or any timeout occurs. 
 +
<br>
 +
The complete source for the distribution provider client is available [https://github.com/ECF/ExampleRSADistributionProviders/tree/master/bundles/org.eclipse.ecf.example1.provider.dist.client here].
 +
 
 +
==Background and Related Articles==
  
 
[[Getting Started with ECF's OSGi Remote Services Implementation]]
 
[[Getting Started with ECF's OSGi Remote Services Implementation]]

Latest revision as of 16:01, 28 April 2016


Introduction

The ECF project provides an implementation of the OSGi R6 Remote Services and Remote Service Admin specifications. The RSA specification defines two major subsystems: discovery and distribution. Discovery concerns finding remote services exported by other processes on the network. The distribution subsystem is responsible for the actual communication of invoking a remote call: serializing remote method parameters, communicating with the remote service host via some network transport protocol, unmarshalling and invoking the service method with provided parameters, and returning a result to the consumer.

ECF's implementation of RSA defines an API to create new distribution providers. This API is declared in the ECF remote services API, provided by the org.eclipse.ecf.remoteservices bundle. Custom distribution providers implement a portion of this API and then will be used at runtime to supply the necessary functions of the distribution provider.

This tutorial will describe the creation of a simple custom distribution provider using the relevant portions of the ECF remote services API.

Remote Service Containers, IDs, and Namespaces

ECF has the concept of a 'container' (IContainer), which is an object instance that implements the remote services API and represents a network-accessible endpoint.

Containers have unique transport-specific ID. Some examples of transport-specific container IDs:

https://myhost.com/v1/path/to/service
ecftcp://localhost:3282/server
mqtt://mybroker.com/mytopic
jms:tcp://jmsbroker.com:6686/jmstopic
r_osgi://somehost/
hazelcast:///mytopicname
g6YuiWAjkk34je1lJlNmv==
uuid:de305d54-75b4-431b-adb2-eb6b9e546014
241

Each ID instance must be unique within a Namespace. Distribution providers must define a new Namespace that enforces the expected syntax requirements of the endpoint identifier.

Step 1: Namespace

The first thing a distribution provider must do is to register a new type of Namespace. ECF provides a number of Namespace classes that can be extended to make this easy. For example, the URIIDNamespace can handle any ID syntax that can be represented as a URI, like all of the above. Here is an example Namespace class that extends the URIIDNamespace:

public class Example1Namespace extends URIIDNamespace {
 
	private static final long serialVersionUID = 2460015768559081873L;
 
	public static final String NAME = "ecf.example1.namespace";
	private static final String SCHEME = "ecf.example1";
	private static Example1Namespace INSTANCE;
 
	/**
	 * The singleton instance of this namespace is created (and registered
	 * as a Namespace service) in the Activator class for this bundle.
	 * The singleton INSTANCE may then be used by both server and client.
	 */
	public Example1Namespace() {
		super(NAME, "Example 1 Namespace");
		INSTANCE = this;
	}
 
	public static Example1Namespace getInstance() {
		return INSTANCE;
	}
 
	@Override
	public String getScheme() {
		return SCHEME;
	}
}

In bundle Activator start, the singleton Example1Namespace is created and registered:

// Create and register the Example1Namespace
context.registerService(org.eclipse.ecf.core.identity.Namespace.class, new Example1Namespace(),  null);

The same Example1Namespace type must be used by both servers and clients, and so it typically makes sense to define the Namespace in a small common bundle, which can be deployed on both servers and clients. For the source code for this example, see the bundle here.

Note that other type of ID syntax can be easily supported by either inheriting from other Namespace classes (e.g. LongIDNamespace, StringIDNamespace, UUIDNamespace, etc.), or creating one's own Namespace subclass.

Step 2: Server

In addition to registering a new Namespace, distribution providers must implement and register the IRemoteServiceDistributionProvider interface using the whiteboard pattern.

For example, here is the code for registering a new custom distribution provider server

public static final String SERVER_ID_PARAMETER = "id";
public static final String SERVER_ID_PARAMETER_DEFAULT = "tcp://localhost:3333";
// register this remote service distribution provider
context.registerService(IRemoteServiceDistributionProvider.class,
   new RemoteServiceDistributionProvider.Builder().setName(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE)
      .setInstantiator(new RemoteServiceContainerInstantiator(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,
         ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) {
         @Override
         public IContainer createInstance(ContainerTypeDescription description,Map<String, ?> parameters) {
            // Create and configure an instance of our server 
            // container type
            return new Example1ServerContainer(getIDParameterValue(Example1Namespace.getInstance(), parameters,
				SERVER_ID_PARAMETER, SERVER_ID_PARAMETER_DEFAULT));
         }
   }).build(),	
null);

Notes for the above code

The RemoteServiceDistributionProvider.Builder class is used to create the IRemoteServiceDistributionProvider instance.

The setName(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE) method on the Builder sets the distribution provider's name (ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE==ecf.example1.provider.dist.server).

This name corresponds to the OSGi Remote Service config type, and must uniquely identify the distribution provider. When a remote service is registered, the standard service property service.exported.configs may be specified so that the remote service will be exported via this distribution provider rather than some other distribution provider. For example:

Hashtable serviceProperties = new Hashtable();
serviceProperties.put("service.exported.interfaces","*");
serviceProperties.put("service.exported.configs","ecf.example1.provider.dist.server");
...
context.registerService(MyService.class,serviceInstance,serviceProperties);

The underlying ECF RSA implementation will export the serviceInstance via the ecf.example2.provider.dist.server distribution provider.

NOTE: As above, the IRemoteServiceDistributionProvider should be registered before any remote services using these distribution providers are exported. In other words, if you create and register a IRemoteServiceDistributionProvider with name 'ecf.example1.provider.dist.server' the provider implementation bundle should be started and the IRemoteServiceDistributionProvider service should be registered prior to registering the service to be exported using that provider. If the distribution provider is not registered prior to the remote service export, then the correct distribution provider will not be available to do the export.

The other required part of the distribution provider is the container instantiator:

      .setInstantiator(new RemoteServiceContainerInstantiator(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,
         ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) {
         @Override
         public IContainer createInstance(ContainerTypeDescription description,Map<String, ?> parameters) {
            // Create and configure an instance of our server 
            // container type
            return new Example1ServerContainer(getIDParameterValue(Example1Namespace.getInstance(), parameters,
				SERVER_ID_PARAMETER, SERVER_ID_PARAMETER_DEFAULT));
         }
   })

This code uses the RemoteServiceContainerInstantiator class. The two constants in the constructor (ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) sets up the instantiator so that these two config types (server and client for distribution provider) are associated with each other so at remote service consumer import time the ECF RSA implementation can select the appropriate client config type associated with the exporting server config type. For example, when ecf.example1.provider.dist.server is used as the exporting config type, then on the client/consumer the selected config type will be: ecf.example1.provider.dist.client
The createInstance method then creates an instance of the appropriate container type. This method allows the container to be created and configured based upon a Map of parameters. As per the RSA specification, if the remote service registration has service properties with keys of the form:
<config type>.<param>
then the provided parameters Map will contain entries of the form
key: <param>=<value>
For example, if the remote service is registered:

Hashtable serviceProperties = new Hashtable();
serviceProperties.put("service.exported.interfaces","*");
serviceProperties.put("service.exported.configs","ecf.example1.provider.dist.server");
serviceProperties.put("ecf.example1.provider.dist.server.id","tcp://localhost:3333");
...
context.registerService(MyService.class,serviceInstance,serviceProperties);

then the parameters Map will contain an entry with key="id" and value="tcp://localhost:3333". Such values can be used to configure the provider. In the case of the Example1 provider above, the id parameter is used to create an ID via the call to getIDParameterValue. Other distribution providers can define arbitrary parameters used to configure the underlying transport such as host, port, bind address, path, etc.
At the appropriate time during export, the createInstance method will be called and the Example1ServerContainer instance created. Here is the implementation of Example1Container

/**
 * Instances of this class are created via the container instantiator specified
 * by the RemoteServiceDistributionProvider whiteboard service in the Activator.
 * @see Activator#start(org.osgi.framework.BundleContext)
 * 
 */
public class Example1ServerContainer extends AbstractRSAContainer {
 
	/**
	 * Create an Example1ServerContainer.  The given id must not be null,
	 * and should be created using the Example1Namespace.  The server's ID
	 * will then be automatically used to provide the ecf.endpoint.id to
	 * remote service clients via the RSA-created EndpointDescription.
	 * @param id
	 */
	public Example1ServerContainer(ID id) {
		super(id);
	}
 
 
	@Override
	/**
	 * When the ECF RSA impl is requested to export a remote service and 
	 * this container instance is selected, a remote service registration will be
	 * created by RSA and this method will then be called to actually export the given
	 * registration via some communications transport.  This should trigger the appropriate 
	 * networking initialization (e.g. opening listener on socket), given the info in the
	 * registration.
	 * The given registration will not be <code>null</code>.
	 * If the transport would like to insert properties into the EndpointDescription
	 * for this endpoint, a Map of name (String) -> Object map should be returned.  All
	 * the values in the Map should be Serializable.   This provides a mechanism for
	 * distribution providers to include arbitrary private properties for use by 
	 * clients.
	 * 
	 * Note that keys for the Map should be unique to avoid conflicting with any other properties.
	 * 
	 * If values are provided for either OSGI RemoteConstants or ECF Remote Services Constants
	 * then these new values will <b>override</b> the default values in the EndpointDescription.
	 *
	 */
	protected Map<String, Object> exportRemoteService(RSARemoteServiceRegistration registration) {
		// Here would be the code to export/make a remote service on the actual transport
                // For example, starting a listener to handle connections, and responding to
                // connection attempts, handling remote calls, etc.
                // the registration parameter contains the services interfaces, the implementation, and the
                // service properties passed in when the remote service is registered
                // This method will be called by the same thread that calls BundleContext.registerService
		return null;
	}
 
	@Override
	/**
	 * When this remote service is unregistered, this method will be called by ECF RSA
	 * to allow the underlying transport to unregister the remote service given
	 * by the registration.  Necessary clean-up/shutdown of transport should be completed
	 * before returning.
	 */
	protected void unexportRemoteService(RSARemoteServiceRegistration registration) {
		// This method will be called when the service registration unregister() 
                // method is called, and is responsible for cleaning up/shutting down
                // and network resources associated with this remote service
                // This method will be called by the same thread that is calling
                // ServiceRegistration.unregister()
	}
}

Once this container instance is created the ECF RSA implementation will then call exportRemoteService(RSARemoteServiceRegistration registration) and expect that upon successful completion that the distribution provider will have made the remote service described by the given registration will have been made available for remote access. The exportRemoteService implementation is therefore where the distribution provider should actually make the remote service available over the network (e.g. by listening on socket, joining communication group, etc). When the remote service is unregistered, the unexportRemoteService(RSARemoteServiceRegistration registration) will be called to allow the transport to close and clean up any network resources associated with the previous export.
Each method will be called for each remote service export, with a unique RSARemoteServiceRegistration passed in by the ECF RSA implementation.
For the exportRemoteService method, the implementation can return a Map of service properties (or null). If a Map instance is returned, the values in the given map will be merged with the other endpoint description properties defined/required by ECF's RSA implementation. For example, one required ECF remote service property is ecf.endpoint.id. The default value of the ecf.endpoint.id is the exporting container's ID...e.g. by default
ecf.endpoint.id will be set to: "tcp://localhost:3333" because the Example1Container's ID is set by construction to "tcp://localhost:3333". However, if the returned Map is non null, and has the key ecf.endpoint.id, the value in the returned map will override the default value. e.g.

	protected Map<String, Object> exportRemoteService(RSARemoteServiceRegistration registration) {
		// TODO setup transport and export given remote service registration
                Map<String,Object> result = new HashMap<String,Object>();
                // This will override the ecf.endpoint.id value in the RSA endpoint description
                result.put("ecf.endpoint.id","http://somehost:2222/foo/");
                // This will add a brand new property to the RSA endpoint description
                result.put("my.new.property","howdy");
		return result;
	}

The complete source for the distribution provider server is available here.

Step 3: Client

Like the server, the distribution provider client is registered via the whiteboard pattern using the IRemoteServiceDistributionProvider service interface. Here is the code for registering the Example1 client distribution provider

context.registerService(IRemoteServiceDistributionProvider.class,
	new RemoteServiceDistributionProvider.Builder().setName(ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE)
		.setInstantiator(
			new RemoteServiceContainerInstantiator(ProviderConstants.SERVER_PROVIDER_CONFIG_TYPE,
				ProviderConstants.CLIENT_PROVIDER_CONFIG_TYPE) {
				@Override
				public IContainer createInstance(ContainerTypeDescription description,
								Map<String, ?> parameters) {
				        // Create and configure an instance of our client
				        // container type (below)
					// This is called by RSA when a client container of this type
					// is needed (e.g. to import a discovered remote service)
					return new Example1ClientContainer();
				}
			}).build(),
null);

As with the server, the setName and setInstantiator builder methods are required. For this distribution provider all that's necessary is to return a new Example1ClientContainer which is declared

public class Example1ClientContainer extends AbstractRSAClientContainer {
 
	public Example1ClientContainer() {
		super(Example1Namespace.getInstance()
				.createInstance(new Object[] { "uuid:" + java.util.UUID.randomUUID().toString() }));
	}
 
	@Override
	// This is called when when a remote service instance is actually needed by consumers
	// The remote service getProxy method will be called the first time the proxy is 
	// to be created for a given consumer
	protected IRemoteService createRemoteService(RemoteServiceClientRegistration registration) {
		return new AbstractRSAClientService(this, registration) {
 
			@Override
			public Object getProxy(ClassLoader cl, @SuppressWarnings("rawtypes") Class[] interfaces) throws ECFException {
				return super.getProxy(cl, interfaces);
			}
 
			@Override
			protected Object invokeAsync(RSARemoteCall remoteCall) throws ECFException {
				// TODO Auto-generated method stub
				return null;
			}
			// invokeSync will be called when a client needs to make a remote call on 
			// one of the proxy methods.
			@Override
			protected Object invokeSync(RSARemoteCall remoteCall) throws ECFException {
				// TODO Auto-generated method stub
				return null;
			}
		};
	}
}

When the ECF RSA discovers a remote service and attempts to import it, the createRemoteService method is called with the associated RemoteServiceClientRegistration provided.
In the above case an instance of AbstractRSAClientService is created for the import. Sometime later when the proxy is actually required by a consumer, the getProxy method will be called. The code above overrides the implementation of getProxy to show that this process can be easily customized for a distribution provider, but in most cases the AbstractRSAClientService proxy creation can be used.
The invokeSync and invokeAsync method are called when the proxy methods are actually invoked by the consumer. The invokeSync method is called when the proxy methods are called synchronously (normal case), and the invokeAsync is called when the ECF async proxy methods are invoked. For the invokeSync method, the distribution provider should implement this method to make the appropriate remote call by serializing remote call arguments, sending over the distribution provider's transport, getting and deserializing the result, and returning the Object that represents the appropriate type for proxy method defined in the RSARemoteCall instance. In invokeSync the calling thread should block until the remote call is made and the response is received, an exception occurs, or any timeout occurs.
The complete source for the distribution provider client is available here.

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

Copyright © Eclipse Foundation, Inc. All Rights Reserved.