Constructing MOBY-S Compliant Services

First you need to check the following:
  • Do the object type(s) that your service will consume already exist in the Object Ontology? If not, learn how to design your own MOBY objects
  • Do the object type(s) that your service will produce already exist in the Object Ontology? If not, then you need to register your objects in the MOBY-S object/class ontology
  • Does an appropriate service type term already exist in the Service Ontology? You need to register your new service type in the MOBY-S service ontology.
  • Do the namespaces that you are going to use in your objects already exist in the Namespace Ontology? Register your namespace, if it doesn't already exist.

If you want a graphical way for registering your terms into the ontologies, you can try:

If all object types, service types, and namespaces already exist in the ontology then you are ready to set up your service. Once you have finished setting it up following the guidelines below, you will then need to Register your service for it to become visible on the MOBY Central registry. In Perl, MOBY-S services are currently limited to SOAP over HTTP; the name of the method call (i.e. the SOAP Action header) is the same as the service name registered in the MOBY Central registry.

Alternatively, a new, more simple way to implement your service exists in Perl MOSES!

The dispatcher

The most straightforward paradigm is to have a single SOAP server running as a CGI script, and this listener hands-off requests to the appropriate code module as requests arrive. The SOAP::Lite module makes this ridiculously easy! Here is the code for a typical SOAP server CGI, that is able to handle requests for two method calls (defined below): Echo, and getGOTerm .
 1.  #!/usr/bin/perl -w
 2.  use SOAP::Transport::HTTP;
 3.  use lib '/usr/local/apache2/cgi-bin/MOBY/'; # the location of your Service modules
 4.  use Services::LocalServices;  # the name of your Service modules

 5.  my $x = new SOAP::Transport::HTTP::CGI;
 
 6.  $x->dispatch_with({
 7.      'http://biomoby.org/#Echo' => 'Services::LocalServices',  
 8.      'http://biomoby.org/#getGoTerm' => 'Services::LocalServices',
 9.  });
10. $x->handle;
So, line by line:
  1. header to make the script executable
  2. use the SOAP::Transport::HTTP module - this is a CGI SOAP server
  3. add the path of your Services subroutines into your perl lib path
  4. use the code module that contains your service subroutines
  5. create an instance of a SOAP CGI server
  6. make a mapping between SOAP Action headers and code modules (in this case, we are using only one module, but you may want to use more than one)
  7. calls to the method "Echo" will be passed to the Services::LocalServices module (there should be a subroutine called "Echo" in that module)
  8. calls to the method "getGoTerm" will be passed to the same module
  9. The "handle" method starts the script listening.
  10. To deploy the dispatcher, simply put it in your cgi-bin folder of your webserver.

The service module

The dispatcher merely listens for incoming MOBY service requests, and then passes them off to the appropriate Perl module (in this case Services::LocalServices) where the real work gets done. Now let's look at the structure of the Services::LocalServices module. Generally speaking, it is just a regular Perl module, so it has a familiar structure:
package Services::LocalServices;
use strict;
use SOAP::Lite;
use MOBY::CommonSubs qw(:all); 

1;
We are using MOBY::CommonSubs because it exports many useful subroutines that we will require to handle incoming MOBY objects and output valid MOBY responses. Services must take incoming data, parse it into individual (enumerated) requests, process each request, generate a MOBY-compliant response message for each request, and then return the (identically enumerated) response messages, along with optional server-side provision information. Thus, a generic service will look something like this:
package Services::LocalServices;
use strict;
use SOAP::Lite;
use MOBY::CommonSubs qw(:all); 
# MOBY::CommonSubs exports 
# genericServiceInputParser(), getSimpleArticleIDs(), validateNamespaces()
# getSimpleArticleNamespaceURI(), responseHeader(), responseFooter()


sub _generic_service_name {
    my ($caller, $data) = @_;  # $data is where the message is

    my $MOBY_RESPONSE;  # start an empty response message
        # genericServiceInputParser is able to parse simple incoming messages
        # into individual inputs, returned to you as a list of listrefs.
        # Simple messages are those that have single inputs of Simple articles
        # no multi-input, nor Secondary Parameters
        # can be parsed by this subroutine (see below for a complex service example)

    my $inputs = serviceInputParser($data); # returns MOBY objects
                                             # 
        # if we are unable to parse any inputs, 
	# we return an empty response along with our authority URI; illuminae.com in this case.
    return SOAP::Data->type('base64' => responseHeader("illuminae.com") . responseFooter()) unless (keys %$inputs);

        # if you are only able to understand data in a particular namespace, then you 
        # are required to validate that namespace before continuing.  To validate it (or several)
        # pass them to the validateNamespaces subroutine, and it will return to you a list of 
        # the assicoated namespace LSID's
        # my @validNS = validateNamespaces("NCBI_Acc");  # ONLY do this if you are intending to be namespace aware!
# $queryID is the enumerated identification of this query.
              # This is client-side defined, so do not validate this!
    foreach my $queryID(keys %{$inputs}){
        $this_invocation = $inputs->{$queryID};  #  this is the  block with this queryID
        my $this_output = "";  # prepare an empty string to hold the output from this invocation
	
        if (my $input = $this_invocation->{incomingRequest}){  # "incomingRequest" is the articleName of the incoming data
                                                                            # this is a MOBY::Client::Simple|Collection|SecondaryArticle object 
                                                                # we now know that we have the input block we are expecting
            my ($namespace) = @{$input->namespaces}; # N.B. this is returned as a list of one element!
            my $id = $input->id;   # get the id portion of the primary object
            my $XML_LibXML = $input->XML_DOM;  # if I need to get the rest of the content as a XML::LibXML DOM object
    
	    # here is where you do whatever manipulation you need to do
	    # with namespace/id/data for your particular service.
	    # you will be building an XML document into $MOBY_RESPONSE
            $this_output = "<moby:Object... rest of the output XML .../>";
        }    
         # according to the API we must return a response for every input, even if it was invalid.
        # so pass a full or a blank to the simpleResponse routine, together with the query enumeration, 
        # and append it to our response message 

        $MOBY_RESPONSE .= simpleResponse($thisoutput   # appending individual responses for each query
                , "myArticleName"  # the article name of that output object
                , $queryID);    # the queryID of the input that we are responding to
    }
    # now just add the header and footer to your responses, and you're done. _
    # base64 encoding of the data ensures that the XML headers are hidden from client-side
    # XML parsers; remember that we are returning fully-formed XML documents - MOBY Messages - within
    # another fully-formed XML document (i.e. the SOAP message itself!), and it is illegal
    # to have XML headers inside of an XML document...

    return SOAP::Data->type('base64' => (responseHeader("illuminae.com") . $MOBY_RESPONSE . responseFooter));
 }


 sub generic_collection_service_template {

    my($caller, $message) = @_;
    my $inputs = serviceInputParser($message);
    my $MOBY_RESPONSE = "";           # set empty response

    # return empty SOAP envelope if ther is no moby input

    return SOAP::Data->type('base64' => responseHeader().responseFooter()) unless (keys %$inputs);

    foreach my $queryID(keys %$inputs){  # $inputs is a hashref of $input{queryid}->{articlename} = input object
        my $this_invocation = $inputs->{$queryID};
        my @outputs;
        if (my $input = $this_invocation->{incomingArticleName}){ # $input is a MOBY::Client::Simple|Collection|Parameter object
                my $id = $input->id;
                my @agis = &_getMyOutputList($id);  # this subroutine contains your business logic and returns a list of ids
                foreach (@agis){
                        push @outputs, "<Object namespace='MyNamespace' id='$_'/>";
                }
        }
        $MOBY_RESPONSE .= collectionResponse (\@outputs, "myOutputArticleName", $queryID);
    }
    return SOAP::Data->type('base64' => (responseHeader("my.authority.org") . $MOBY_RESPONSE . responseFooter));
 }

1;

A service that echoes

So let's build our "Echo" service. This service will take Simple inputs, and echo them back to you as output. The only tricky part of this service is that we have to extract the objects from their <Simple> or <Collection> XML wrappers. This is accomplished using the content subroutine exported from MOBY::CommonSubs.

package Services::LocalServices;
use strict;
use SOAP::Lite;
use MOBY::CommonSubs qw(:all); 

sub Echo {
    my ($caller, $data) = @_;  # $data is where the message is
    my $MOBY_RESPONSE;  # start an empty response message
    my $inputs = serviceInputParser($data);
    return SOAP::Data->type('base64' => responseHeader("illuminae.com") . responseFooter()) unless (keys %$inputs);

    foreach my $queryID(keys %$inputs){
        my $this_invocation = $inputs->{$queryID};  # this is the  block with this queryID
	my $invocation_output = "";
	if (my $input = $this_invocation->{'This_articleName'}){
	    $invocation_output = $input->content;  # get the raw XML of the incoming request
	}
	
        $MOBY_RESPONSE .= simpleResponse( # create an empty response for this queryID
                $invocation_output   # response for this query
                , "myOutput"  # the article name of that output object
                , $queryID);    # the queryID of the input that we are responding to
    }
    return SOAP::Data->type('base64' => (responseHeader("illuminae.com") . $MOBY_RESPONSE . responseFooter));    
}
1;

A more challenging example: getGOTerm

A more challenging service is the getGOTerm service. This service consumes GO id's (contained in Objects with the namespace GO and the id containing the GO accession number of the desired term, e.g. <Object namespace='GO' id='0003576'/> ), and returns a Collection of GO Term objects that match the keyword search. For this service we are going to connect directly to the GO database in Berkeley (sin.lbl.gov). In addition, this service is namespace-aware, so we need to validate the namespaces we are going to use. Finally, there is one more hiccup in that GO identifiers are sometimes prefixed, and other times not. Although the MOBY API indicates that prefixes should be stripped, we have to accept that sometimes they will not be, and try to be accommodating. In this subroutine, the following methods are exported from MOBY::CommonSubs:
 genericServiceInputParser(), getSimpleArticleIDs(), validateNamespaces(),
 getSimpleArticleNamespaceURI(), validateThisNamespace(), responseHeader(), responseFooter()
Here's the code for the service:

 sub getGoTerm {
 
    use MOBY::CommonSubs qw{:all};
    my ($caller, $incoming_message) = @_;
    my $MOBY_RESPONSE; # holds the response raw XML
    my @validNS = validateNamespaces("GO");  # do this if you intend to be namespace aware!

    my $dbh = _connectToGoDatabase();  # connect to some database
    return SOAP::Data->type('base64' => responseHeader('my.authURI.com') . responseFooter()) unless $dbh;
    my $sth = $dbh->prepare(q{   # prepare your query
       select name, term_definition
       from term, term_definition
       where term.id = term_definition.term_id
       and acc=?});

    my $inputs= serviceInputParser($incoming_message); # get incoming invocations
        # or fail properly with an empty response if there is no input
    return SOAP::Data->type('base64' => responseHeader("my.authURI.com") . responseFooter()) unless (keys %$inputs);

    foreach my $queryID(keys %$inputs){
        my $this_invocation = $inputs->{$queryID};  # this is the  block with this queryID
	my $invocation_output = ""; # prepare a variable to hold the output XML from this invocation
	if (my $input = $this_invocation->{GO_id}){
	    
	    my ($namespace) = @{$input->namespaces}; # this is returned as a list!
	    my $id = $input->id;	
	    # optional - if we want to ENSURE that the incoming ID is in the GO namespace
	    # we can validate it using the validateThisNamespace routine of CommonSubs
	    # @validNS comes from validateNamespaces routine of CommonSubs (called above)
	    next unless validateThisNamespace($namespace, @validNS); 
    
	    # here's our business logic...
	    $sth->execute($id);
	    my ($term, $def) = $sth->fetchrow_array;
	    if ($term){
		 $invocation_output =
		 "<moby:GO_Term namespace='GO' id='$id'>
		  <moby:String namespace='' id='' articleName='Term'>$term</moby:String>
		  <moby:String namespace='' id='' articleName='Definition'>$def</moby:String>
		 </moby:GO_Term>";
	    }
	}

        # was our service execution successful?
        # if so, then build an output message
        # if not, build an empty output message
       $MOBY_RESPONSE .= simpleResponse( # simpleResponse is exported from CommonSubs
                $invocation_output   # response for this query
                , "A_GoTerm"  # the article name of that output object
                , $queryID);    # the queryID of the input that we are responding to
    }
    # now return the result
    return SOAP::Data->type('base64' => (responseHeader("illuminae.com") . $MOBY_RESPONSE . responseFooter));
}

sub _dbAccess {
    my ($dbname) = @_;
    return undef unless $dbname;
    my ($dsn) = "DBI:mysql:go:sin.lbl.gov:3306";
    my $dbh = DBI->connect($dsn, 'go', '', {RaiseError => 1}) or die "can't connect to database";
    return ($dbh);
}
When you have finished writing your service, test it (link coming soon!) and then Register it.