A Quick Introduction to OME-JAVA Data Services
AUDIENCE/LESSON PLAN: This document is designed to provide preliminary orientation towards developers interested in understanding and hacing the Data Services in OME-JAVA. Note that the OmeImageServer components of OME-JAVA are not discussed in this document.
OME-JAVA provides Java programs basic bare-bones access to OME objects in an OME Data server (OMEDS) via an XML-RPC interface. This document will provide a beginning roadmap to some of the finer points of OME-JAVA architecture, dataflow, and execution.
All classes discussed below are found under org.openmicroscopy.ds in the OME-JAVA distribution.
Core Concepts
There are three concepts at the core of OME-JAVA data access: RemoteCaller, DataService, and DataServices.
- RemoteCaller
A RemoteCaller is an object that handles the job of communicating via the backend. The RemoteCaller interface specifies the RPC calls that the OME backend can handle. As such, it presents one version of a definition of the API.
The RemoteCaller interface is not, however, the complete OME-JAVA API. The RemoteCaller interface provides some specific calls for login, logout, etc., but most of the power of the RemoteCaller lies in the invoke() and dispatch() calls: generic calls that take in methods names and arguments and send them along to the back-end. Thus, the actual detail of the OME-JAVA API is not found in RemoteCaller (or its implementations) but rather in the classes that call RemoteCaller's methods. More on those classes later.
Currently, XmlRpcCaller is the only implementation of RemoteCaller in OME-JAVA. XmlRpcCaller wraps Apache's XMLRPC client (org.apache.xmlrpc), handling the creation and transmission of requests and the reception and processing of results.
XmlRpcCaller is completely synchronized - only one call can be handled at any given time. This constraint is an artifact of the implementation of OME-JAVA. As the underlying Apache library includes support for multi-threading, XmlRpcCaller could conceivably be re-written to be multi-threaded.
Currently, there seems only one instance of XmlRpcCaller that is created in any given execution of OME-JAVA. However, there does not seem to be any inherent limitation to this. Thus, another way to implement multi-threading of OME-JAVA might be to create multiple XmlRpcCaller objects, each of which could (at any given time) be used by a different thread. The discussion of DataServices (below) will flesh out some of the implementation issues surrounding this.
- DataService
This very thin interface is extended or implemented by all interfaces or classes that provide some sort of data access, including RemoteCaller. Other than RemoteCaller, there are three main groups of DataService objects (the term "group" is used here because this distinction is not enforced by any language mechanism): DataFactory, the managers, and the InstantiatingCaller. DataFactory and the manager classes will be discussed below. InstantiatingCaller will be discussed in more detail in a future narrative.
DataFactory and the managers are the classes that effectively define the OME-JAVA API. These classes expose methods that can be called by client code to provide a variety of functions: retrieving objects, updating objects, counting sets of objects, etc. The implementation of these calls involves constructing calls that are eventually passed through to a RemoteCaller (specific details on this pathway below), which handle the actual mechanics of making the call and preparing the results.
The two kinds of DataService objects are distinguished by their scope: DataFactory provides generic access to a wide range of calls appropriate for almost any OME object. The managers provide calls that are specific to various purposes: adding images to datasets (DatasetManager?), retrieving information about image import (ImportManager?),etc. Most of these managers (with the exception of RemoteImportManager ) correspond directly to Facades provided in the backend implementation (which will be discussed elsewhere). Note, however, that although OME-JAVA managers are analogous to managers in the OME backend (OME::Tasks), they are not necessarily implemented in terms of calls on the OME backend managers. Instead, they are based on Facades, which may or may not make use of the backend managers.
- DataServices
DataServices provides a link between DataService objects and the RemoteCallers that handle the actual calls.
DataServices has a static map that associates remote caller objects with DataServices instances. For any given RemoteCaller, there will be at most one DataServices object - this constraint is enforced by the map and the getInstance() call.
The main entry point to DataServices is getInstance. Given a remote caller, getInstance will attempt to find an entry in DataService?'s map of instances for that caller. If there is an entry, then that entry is the DataServices instance for that caller. Otherwise, a DataServices instance is created and mapped to that caller.
Once a DataServices object is created, it can be used to instantiate instances of the various DataService classes - either DataFactory or one of the managers. This can be done by passing the appropriate DataService implementation into instantiateService. This procedure creates an object of the appropriate type, links it to the DataServices object (and therefore to the associated RemoteCaller ), and puts it into a map for the DataServices instance. This map is used on a per-instance basis to ensure that there is only one instance of any DataService class for a given DataServices object (and therefore for a given RemoteCaller ). Subsequent requests for a DataService object of a class that has already been seen will simply return the instance that was stored in this map.
This potentially confusing model is fairly important and therefore merits summarization:
- There is one DataServices instance for any given RemoteCaller instance.
- Each DataServices instance can have at most one instance of any DataService subclass associated with it.
Thus, one possible path to multi-threading might be to create multiple RemoteCallers, and then to create DataServices instances for each of them. The associated DataServer objects could then each be accessed in different threads. There does not appear to be any reason why this would not work, but careful testing will obviously be necessary.
Logging in and Establishing a Connection
The main entry point to connecting to an OMEDS is in DataServer. This class has several methods for getting remote callers and default services.
Generally, DataServer's getDefaultServices() will be called. Given a string or URL for an OMEDS, This procedure will create the RemoteCaller and create an appropriate DataServices instance.
From this DataServices instance, getRemoteCaller will be used to retrieve the remote caller. the login() method from that caller can then be used to connect to the OMEDS. The login call will either retrieve a session key from the OMEDS or throw an exception.
Instantiating Services
Once the DataServices instance is created, DataService instances can be created by passing the class of DataService to be created into the getService() call of DataServices. This will either create a new instance of the DataService class, or it will returned the previously-created instance associated with the DataServices object.
Detailed descriptions of the services provided by the various DataService classes can be found in the javadoc documentation provided in the source files.
Objects
There are two general groups of objects that OME-JAVA deals with. "Core" OME objects defined under org.openmicroscopy.ds.dto support datasets, projects, images, and other objects that are fundamental to OME's operation.
Semantic type objects (under org.oenmicroscopy.ds.st) implement semantic types used in OME. As the semantic type mechanism supports extensibility through importation of new semantic type definitions (STDs), the set of STs supported by these classes is not likely to be complete. This package are auto-generated by a script found within the main OME installation (details will be described in another narrative), based on the STs that are available at the time of execution of the script.
Whether core objects or semantic types, all OME-JAVA objects have the same general structure. Each object is defined both as an interface (Foo.java) and as an implemenation (FooDTO.java). The implementations extend the MappedDTO class, essentially putting a face of named constructors in front of a generic Map. This mechanism (which will also be described in detail elsewhere) lets OME-JAVA take generic hashes as returned from OMEDS and wrap them in interfaces that are more useful for higher-level implementations.
