Skip to main content

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

Jump to: navigation, search

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

Line 151: Line 151:
 
</source>
 
</source>
  
==Provider Step 2: Creating the Host Implementation==
+
==Provider Step 2: Creating the Provider Host Implementation==
  
 
Since we would like to interact with this via http+rest, we will create a Servlet to actually handle the request...and register it dynamically to the OSGi standard HttpService.  That way we can implement the ITimeService.getCurrentTime() method by implementing the HttpServlet.doGet method.  Here is the complete implementation of the TimeRemoteServiceHttpServlet as well as the necessary ECF container code
 
Since we would like to interact with this via http+rest, we will create a Servlet to actually handle the request...and register it dynamically to the OSGi standard HttpService.  That way we can implement the ITimeService.getCurrentTime() method by implementing the HttpServlet.doGet method.  Here is the complete implementation of the TimeRemoteServiceHttpServlet as well as the necessary ECF container code
Line 265: Line 265:
 
</source>
 
</source>
  
which reference the TimeServiceRestNamespace, as defined above, and
+
which references the TimeServiceRestNamespace to associate this remote service container with the TimeServiceRestNamespace we created above.
  
 
<source lang="java">
 
<source lang="java">
Line 300: Line 300:
 
</source>
 
</source>
  
As can be seen by the comments, all that's happening here is that
+
All that's happening here is that
  
 
#1 The local ITimeService implementation is retrieved
 
#1 The local ITimeService implementation is retrieved
 
#2 The ITimeService.getCurrentTime() method is called to get the local time (Long)
 
#2 The ITimeService.getCurrentTime() method is called to get the local time (Long)
#3 The result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream.
+
#3 The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream.
 +
 
 +
The next thing to do is to implement a ECF container instantiator, which is responsible for creating and using instances of TimeServiceServerContainer under the appropriate OSGi Remote Service conditions.  Here is the container instantiator class:
 +
 
 +
<source lang="java">
 +
public class TimeServiceServerContainerInstantiator extends
 +
ServletServerContainerInstantiator {
 +
 
 +
public IContainer createInstance(ContainerTypeDescription description,
 +
Object[] parameters) throws ContainerCreateException {
 +
try {
 +
// Get HttpServices
 +
Collection<HttpService> httpServices = TimeServiceHttpServiceComponent.getDefault().getHttpServices();
 +
if (httpServices == null || httpServices.size() == 0) throw new NullPointerException("Cannot get HttpService for TimeServiceServerContainer creation");
 +
// If we've got more than one, then we'll just use the first one
 +
HttpService httpService = httpServices.iterator().next();
 +
@SuppressWarnings("unchecked")
 +
Map<String, Object> map = (Map<String, Object>) parameters[0];
 +
String id = (String) map.get("id");
 +
return new TimeServiceServerContainer(IDFactory.getDefault()
 +
.createID(TimeServiceRestNamespace.NAME, id), httpService);
 +
} catch (Exception e) {
 +
throw new ContainerCreateException(
 +
"Could not create TimeServiceServerContainer", e);
 +
}
 +
}
 +
 
 +
public String[] getSupportedAdapterTypes(
 +
ContainerTypeDescription description) {
 +
return getInterfacesAndAdaptersForClass(TimeServiceServerContainer.class);
 +
}
 +
 
 +
public String[] getSupportedConfigs(ContainerTypeDescription description) {
 +
return new String[] { TimeServiceServerContainer.NAME };
 +
}
 +
}
 +
</source>
 +
 
 +
The createInstance method selects an HttpService to use in the TimeServiceServerContainer constructor, and the "id" parameter is used to define the TimeServiceServerContainer ID.  The host container ID corresponds to the '''http://localhost:8080''' in the remote service URL given above.
 +
 
 +
Note also the '''getSupportedConfigs''' method.  This method allows providers to define a String[] of config types that they support.  This is used by the ECF RSA implementation to determine at remote service export time exactly which remote service containers to create and use to export the remote service.
 +
 
 +
This container instantiator class can then be declared as an extension via plugin.xml markup:
 +
 
 +
<source lang="xml">
 +
  <extension
 +
        point="org.eclipse.ecf.containerFactory">
 +
      <containerFactory
 +
            class="com.mycorp.examples.timeservice.internal.provider.rest.host.TimeServiceServerContainerInstantiator"
 +
            hidden="false"
 +
            name="com.mycorp.examples.timeservice.rest.host"
 +
            server="true">
 +
      </containerFactory>
 +
  </extension>
 +
</source>
 +
 
 +
Note that the 'name' attribute ('''com.mycorp.examples.timeservice.rest.host''') must match the value defined in the code for '''TimeServiceServerContainer.NAME'''.
 +
 
 +
Finally, the HttpService is injected into the '''TimeServiceHttpServiceComponent''' via OSGi Declarative Services.  See the [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.host/OSGI-INF/httpservicecomponent.xml standard DS xml for that component here].  Here is the entire host provider implementation in [http://git.eclipse.org/c/ecf/org.eclipse.ecf.git/tree/examples/bundles/com.mycorp.examples.timeservice.provider.rest.host com.mycorp.examples.timeservice.rest.host bundle project].
 +
 
 +
==Provider Step 3: Creating the Consumer Provider Implementation==
 +
 
 +
Here is the consumer remote service container provider implementation
 +
 
 +
<source lang="java">
 +
public class TimeServiceRestClientContainer extends RestClientContainer {
 +
 
 +
public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
 +
 
 +
private IRemoteServiceRegistration reg;
 +
 
 +
TimeServiceRestClientContainer() {
 +
// Create a random ID for the client container
 +
super((RestID) IDFactory.getDefault().createID(
 +
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)
 +
throws ContainerConnectException {
 +
super.connect(targetID, connectContext1);
 +
// Create the IRemoteCallable to represent
 +
// access to the ITimeService method. 
 +
IRemoteCallable callable = RestCallableFactory.createCallable(
 +
"getCurrentTime", ITimeService.class.getName(), null,
 +
new HttpGetRequestType(), 30000);
 +
// Register the callable and associate it with the ITimeService class
 +
// name
 +
reg = registerCallables(new String[] { ITimeService.class.getName() },
 +
new IRemoteCallable[][] { { callable } }, null);
 +
}
 +
 
 +
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>
 +
 
 +
First, as with the host, the getConnectNamespace override uses/refers to the '''TimeServiceRestNamespace.NAME''' definition.  This associates this consumer provider with the host provider.
 +
 
 +
The '''TimeServiceRestClientContainer''' constructor first creates a unique id for itself and then it sets up a json response deserializer (for handling the json result of getCurrentTime()) with this code:
 +
 
 +
<source lang="java">
 +
// 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
 +
 
 +
<source lang="java">
 +
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'''.

Revision as of 22:08, 10 December 2013


Introduction

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

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

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

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

Exporting Using a Config Type

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

Dictionary<String, String> props = new Hashtable<String, String>();
// OSGi Standard Property - indicates which of the interfaces of the service will be exported.  '*' means 'all'.
props.put("service.exported.interfaces", "*");
// OSGi Standard Property (optional) - indicates which provider config(s) will be used to export the service
// (If not explicitly given here, the provider is free to choose a default configuration for the service)
props.put("service.exported.configs","ecf.generic.server");
// Register a new TimeServiceImpl with the above props
bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);

Note the line:

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

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

It's quite possible, however, to create a replacement provider, based upon our own http+rest+json transport protocol. The remainder of this tutorial will step through how to create and run your own provider.

Provider Step 1: Implement New Provider Namespace

ECF allows providers to create their own namespaces, so that the endpoint id of the remote service can be properly interpreted. Here is a TimeServiceRestNamespace class

public class TimeServiceRestNamespace extends RestNamespace {
 
	public static final String NAME = "com.mycorp.examples.timeservice.provider.rest.namespace";
 
	@Override
	public ID createInstance(Object[] parameters) throws IDCreateException {
		return new TimeServiceRestID(this, URI.create((String) parameters[0]));
	}
 
	public static class TimeServiceRestID extends RestID {
 
		public TimeServiceRestID(Namespace namespace, URI uri) {
			super(namespace, uri);
		}
	}
}

As can be seen above, when the createInstance call is made a new instance of TimeServiceRestID is created from the single String parameter[0] passed into the createInstance call. Along with this class, we can define an extension in the plugin.xml of this bundle to allow this namespace to be used at the appropriate point.

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

Since this Namespace/ID is needed for both the host and the consumer providers, it should be placed in a separate bundle. The complete bundle is available via the ECF git repo with project (and symbolic) name com.mycorp.examples.timeservice.provider.rest.common.

Note that the package with this new Namespace/ID class must be exported by having this in the manifest.mf:

Export-Package: com.mycorp.examples.timeservice.provider.rest.common

Provider Step 2: Creating the Provider Host Implementation

Since we would like to interact with this via http+rest, we will create a Servlet to actually handle the request...and register it dynamically to the OSGi standard HttpService. That way we can implement the ITimeService.getCurrentTime() method by implementing the HttpServlet.doGet method. Here is the complete implementation of the TimeRemoteServiceHttpServlet as well as the necessary ECF container code

package com.mycorp.examples.timeservice.internal.provider.rest.host;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.eclipse.ecf.core.ContainerCreateException;
import org.eclipse.ecf.core.identity.IDFactory;
import org.eclipse.ecf.core.identity.Namespace;
import org.eclipse.ecf.remoteservice.servlet.HttpServiceComponent;
import org.eclipse.ecf.remoteservice.servlet.RemoteServiceHttpServlet;
import org.eclipse.ecf.remoteservice.servlet.ServletServerContainer;
import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.service.http.HttpService;
 
import com.mycorp.examples.timeservice.ITimeService;
import com.mycorp.examples.timeservice.provider.rest.common.TimeServiceRestNamespace;
 
public class TimeServiceServerContainer extends ServletServerContainer {
 
	public static final String TIMESERVICE_HOST_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.host";
	public static final String TIMESERVICE_SERVLET_NAME = "/" + ITimeService.class.getName();
 
	private final HttpService httpService;
 
	TimeServiceServerContainer(String id, HttpService httpService) throws ContainerCreateException {
		super(IDFactory.getDefault()
				.createID(TimeServiceRestNamespace.NAME, id));
		this.httpService = httpService;
		// Register our servlet with the given httpService with the TIMESERVICE_SERVLET_NAME
		// which is "/com.mycorp.examples.timeservice.ITimeService"
		try {
			this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
					new TimeRemoteServiceHttpServlet(), null, null);
		} catch (Exception e) {
			throw new ContainerCreateException("Could not create Time Service Server Container",e);
		}
	}
 
	@Override
	public void dispose() {
		httpService.unregister(TIMESERVICE_SERVLET_NAME);
		super.dispose();
	}
 
	@Override
	public Namespace getConnectNamespace() {
		return IDFactory.getDefault().getNamespaceByName(
				TimeServiceRestNamespace.NAME);
	}
 
	class TimeRemoteServiceHttpServlet extends RemoteServiceHttpServlet {
 
		private static final long serialVersionUID = 3906126401901826462L;
 
		// Handle get call right here.
		@Override
		protected void doGet(HttpServletRequest req, HttpServletResponse resp)
				throws ServletException, IOException {
 
			// No arguments to getCurrentTime() method, so
			// nothing to deserialize from request
 
			// 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);
			}
		}
	}
 
}

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

this.httpService.registerServlet(TIMESERVICE_SERVLET_NAME,
				new TimeRemoteServiceHttpServlet(), null, null);

Note that the TIMESERVICE_SERVLET_NAME is defined to be "/com.mycorp.examples.timeservice.ITimeService". Since this is used to register the servlet this means that this service will have an URL that is (for example):

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

Other paths can obviously be specified as desired.

Also present are overrides of two ECF container lifecycle methods

public Namespace getConnectNamespace() {
	return IDFactory.getDefault().getNamespaceByName(
			TimeServiceRestNamespace.NAME);
}

which references the TimeServiceRestNamespace to associate this remote service container with the TimeServiceRestNamespace we created above.

public void dispose() {
	httpService.unregister(TIMESERVICE_SERVLET_NAME);
	super.dispose();
}

which dynamically unregisters the servlet when the container instance is disposed.

All of the actual behavior is given by the doGet method implementation in the TimeRemoteServiceHttpServlet

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

All that's happening here is that

  1. 1 The local ITimeService implementation is retrieved
  2. 2 The ITimeService.getCurrentTime() method is called to get the local time (Long)
  3. 3 The getCurrentTime() result is serialized to json (in this case using the json.org json implementation) and printed to the HttpServletResponse output stream.

The next thing to do is to implement a ECF container instantiator, which is responsible for creating and using instances of TimeServiceServerContainer under the appropriate OSGi Remote Service conditions. Here is the container instantiator class:

public class TimeServiceServerContainerInstantiator extends
		ServletServerContainerInstantiator {
 
	public IContainer createInstance(ContainerTypeDescription description,
			Object[] parameters) throws ContainerCreateException {
		try {
			// Get HttpServices
			Collection<HttpService> httpServices = TimeServiceHttpServiceComponent.getDefault().getHttpServices();
			if (httpServices == null || httpServices.size() == 0) throw new NullPointerException("Cannot get HttpService for TimeServiceServerContainer creation");
			// If we've got more than one, then we'll just use the first one
			HttpService httpService = httpServices.iterator().next();
			@SuppressWarnings("unchecked")
			Map<String, Object> map = (Map<String, Object>) parameters[0];
			String id = (String) map.get("id");
			return new TimeServiceServerContainer(IDFactory.getDefault()
					.createID(TimeServiceRestNamespace.NAME, id), httpService);
		} catch (Exception e) {
			throw new ContainerCreateException(
					"Could not create TimeServiceServerContainer", e);
		}
	}
 
	public String[] getSupportedAdapterTypes(
			ContainerTypeDescription description) {
		return getInterfacesAndAdaptersForClass(TimeServiceServerContainer.class);
	}
 
	public String[] getSupportedConfigs(ContainerTypeDescription description) {
		return new String[] { TimeServiceServerContainer.NAME };
	}
}

The createInstance method selects an HttpService to use in the TimeServiceServerContainer constructor, and the "id" parameter is used to define the TimeServiceServerContainer ID. The host container ID corresponds to the http://localhost:8080 in the remote service URL given above.

Note also the getSupportedConfigs method. This method allows providers to define a String[] of config types that they support. This is used by the ECF RSA implementation to determine at remote service export time exactly which remote service containers to create and use to export the remote service.

This container instantiator class can then be declared as an extension via plugin.xml markup:

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

Note that the 'name' attribute (com.mycorp.examples.timeservice.rest.host) must match the value defined in the code for TimeServiceServerContainer.NAME.

Finally, the HttpService is injected into the TimeServiceHttpServiceComponent via OSGi Declarative Services. See the standard DS xml for that component here. Here is the entire host provider implementation in 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

public class TimeServiceRestClientContainer extends RestClientContainer {
 
	public static final String TIMESERVICE_CONSUMER_CONFIG_NAME = "com.mycorp.examples.timeservice.rest.consumer";
 
	private IRemoteServiceRegistration reg;
 
	TimeServiceRestClientContainer() {
		// Create a random ID for the client container
		super((RestID) IDFactory.getDefault().createID(
				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)
			throws ContainerConnectException {
		super.connect(targetID, connectContext1);
		// Create the IRemoteCallable to represent
		// access to the ITimeService method.  
		IRemoteCallable callable = RestCallableFactory.createCallable(
				"getCurrentTime", ITimeService.class.getName(), null,
				new HttpGetRequestType(), 30000);
		// Register the callable and associate it with the ITimeService class
		// name
		reg = registerCallables(new String[] { ITimeService.class.getName() },
				new IRemoteCallable[][] { { callable } }, null);
	}
 
	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);
	}
}

First, as with the host, the getConnectNamespace override uses/refers to the TimeServiceRestNamespace.NAME definition. This associates this consumer provider with the host provider.

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

	// This sets up the JSON deserialization of the server's response.
	// See below for implementation of TimeServiceRestResponseDeserializer
	setResponseDeserializer(new TimeServiceRestResponseDeserializer());

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

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

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

The disconnect method simply unregisters the IRemoteCallable.

Back to the top