MIDAS - The Multiple Instance Data Acquisition System

Implementation of XML/SOAP client and server facilities

(1) Introduction

This document describes how Web services based on the use of XML/SOAP are available within the MIDAS environment. It uses the implementation of the MIDAS MessageLogger server and client as an example application and shows how a useful application can be implemented with a very small amount of code.

(2) Client (Tcl)

During the initialisation of each task (interpreter) within a MIDAS session the contents of the file /Midas/tcl/package-load.tcl are executed which source (if the file exists) /MIDAS/TclTkpackages/package-load.tcl; /MIDAS/SOAP/tcl/package-load and /MIDAS/packages/package-load.tcl. These all use the Tcl package ifneeded command to define the location and method of loading of a number of MIDAS packages and third party Tcl packages.
The default MIDAS core session now makes available if required in this way the libraries TclX; TclLib and libraries needed for client SOAP/XML access.

The following command (which is taken from /MIDAS/SOAP/tcl/package-load.tcl)


package ifneeded MessageLoggerClient 1.0 \
    [list source [file join $env(MIDASBASE) SOAP tcl MessageLoggerClient.tcl]]

defines how version 1.0 of the MessageLoggerClient package can be loaded.
It says that if the MessageLoggerClient package version 1.0 is needed then the contents of the file /MIDAS/SOAP/tcl/MessageLoggerClient.tcl should be loaded.

The file /MIDAS/SOAP/tcl/MessageLoggerClient.tcl



#    MIDAS Access Client  for MessageLogger service

global env
source [file join $env(MIDASBASE) SOAP tcl MIDASAccessClient.tcl]    ;#   load default support procedures

#    obtain Message Logger Server host and port using environment variables (if they exist) or 
#    defaults  -  egmsg:8015

global MessageLoggerHost MessageLoggerPort

if {[info exists env(MessageLoggerHost)]} then {set MessageLoggerHost $env(MessageLoggerHost)} \
    else {set MessageLoggerHost egmsg}
if {[info exists env(MessageLoggerPort)]} then {set MessageLoggerPort $env(MessageLoggerPort)} \
    else {set MessageLoggerPort 8015}

#    The service URL is  egmsg:8015/MessageLogger

variable uri "urn:MessageLogger"
variable proxy "http://$MessageLoggerHost:$MessageLoggerPort/MessageLogger"

#    create and configure the SOAP method

SOAP::create MessageLogger__DefineMessage -proxy $proxy -uri $uri -timeout 5000 -version 1.2 \
     -name DefineMessage \
     -params {class int level int source string message string} \
     -wrapProc  ::SOAP::MIDAS_SOAPAccessClient_Request \
     -parseProc ::SOAP::MIDAS_SOAPAccessClient_Reply

#    define a Tcl procedure which has some (optional) arguments
#    format of Tcl procedure call to define a message is:-        DefineMessage [-c class] [-l level] [-s source] message

proc DefineMessage  {args} {
    global errorCode errorInfo
    
    set Class 1       ;# define some default values
    set Level 3

    set Source [exec uname -n]
    set Message ""

    set l [llength $args]
    if {$l < 1}  {return 0}      ;# no message
    if {$l == 1} {
        set Message "$args"
    } else {
        for {set i 0} {$i < $l} {incr i} {
            switch -- [lindex $args $i] {
                -c   {incr i; set Class [lindex $args $i]}
                -l   {incr i; set Level [lindex $args $i]}
                -s   {incr i; set Source [lindex $args $i]}
                default {set Message [lreplace $args 0 [expr $i-1]]}
            }
            if {$Message != ""} {break}
        }
    }

   set z [catch {MessageLogger__DefineMessage $Class $Level "$Source" "$Message"} m]}  use SOAP method to define the message

   if {$z != 0} {puts stderr "Error in DefineMessage: $m\n$errorCode\n$errorInfo"}
}

package provide MessageLoggerClient 1.0

provides the MessageLoggerClient package version 1.0 and creates a Tcl command MessageLogger__DefineMessage with arguments named class and level of type integer and source and message of type string. These are the normal arguments for the Message Logger Service.
Additional a Tcl command DefineMessage is created in which the arguments (except message) are optional. This is not an essential part of the MessageLogger client interface.
The Tcl MessageLogger command will cause access via XML/SOAP to a method DefineMessage which is to be found at the Host; Port and Domain given by the URL defined by the proxy variable.
A 5000 millisecond timeout is applied on the wait for the reply.

The Server host name and port are taken from the environment variables MessageLoggerHost and MessageLoggerPort if these exist otherwise defaults are used.

The file /MIDAS/SOAP/tcl/MIDASAccessClient.tcl defines the procedures ::SOAP::MIDAS_SOAPAccessClient_Request and ::SOAP::MIDAS_SOAPAccessClient_Reply which are used to create the XML message and to parse the response. This is done purely for reasons of performance since we generate very simple XML which does not require use of the full XML/DOM implementation. However use of this file is optional and could be removed in which case the -wrapProc and -parseProc options in the SOAP::create command should also be removed and the default procedures provided by the SOAP package used.

Any client task can then use the MessageLogger command as follows


package require MessageLoggerClient

DefineMessage  Hello World

DefineMessage -c 10 -l 8  A serious error has occurrred!!!

This generates the XML message


<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
xmlns:ns="urn:MessageLogger">
<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<ns:DefineMessage>
<class xsi:type="xsd:int">1</class>
<level xsi:type="xsd:int">3</level>
<source xsi:type="xsd:string">nnsv</source>
<message xsi:type="xsd:string">Hello World</message>
</ns:DefineMessage>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

and receives the response


<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
xmlns:ns="urn:MessageLogger">
<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<ns:DefineMessageResponse>
<result xsi:type="xsd:int">0</result>
</ns:DefineMessageResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

This completes the Client implementation notes. It really is as simple as that!
For further details see the SOAP Client package documentation

(3) Client (gSOAP)

In the previous section it is shown how the DefineMessage client can be implemented within a MIDAS Tcl session. It is however also required that SOAP/XML messages can be generated from within programs (e.g. a data acquisition program). This can be done using a package such as gSOAP.

The gSOAP package contains a compiler which will take a simple definition of the client interface and generate the client side C code stubs. It can also generate these client C code stubs from the Web Service WSDL description (see later).

The gSOAP definition file (MessageLogger.def) for the MessageLogger client is



//gsoap ns service name:         MessageLogger
//gsoap ns service style:        rpc
//gsoap ns service encoding:     encoded
//gsoap ns service namespace:    http://nnsb.dl.ac.uk:8015/MessageLogger/service.wsdl
//gsoap ns service location:     http://nnsb.dl.ac.uk:8015/MessageLogger
//gsoap ns schema namespace:     urn:MessageLogger

int ns__DefineMessage(int class_, int level, char *source, char *message, int *result);

This definition file can be converted into the Web Service Description file (WSDL) which is the preferred way for a Web service to publish its documentation.
This can be done using the gSOAP program soapcpp2.


soapcpp2 -C -c -x MessageLogger.def

The WSDL description of the MessageLogger service (MessageLogger.wsdl) is



<?xml version="1.0" encoding="UTF-8"?>
<definitions name="MessageLogger"
 targetNamespace="http://nnsb.dl.ac.uk:8015/MessageLogger/service.wsdl"
 xmlns:tns="http://nnsb.dl.ac.uk:8015/MessageLogger/service.wsdl"
 xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:ns="urn:MessageLogger"
 xmlns:SOAP="http://schemas.xmlsoap.org/wsdl/soap/"
 xmlns:MIME="http://schemas.xmlsoap.org/wsdl/mime/"
 xmlns:DIME="http://schemas.xmlsoap.org/ws/2002/04/dime/wsdl/"
 xmlns:WSDL="http://schemas.xmlsoap.org/wsdl/"
 xmlns="http://schemas.xmlsoap.org/wsdl/">

<types>

 <schema targetNamespace="urn:MessageLogger"
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:ns="urn:MessageLogger"
  xmlns="http://www.w3.org/2001/XMLSchema"
  elementFormDefault="unqualified"
  attributeFormDefault="unqualified">
  <import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>
 </schema>

</types>

<message name="DefineMessageRequest">
 <part name="class" type="xsd:int"/>
 <part name="level" type="xsd:int"/>
 <part name="source" type="xsd:string"/>
 <part name="message" type="xsd:string"/>
</message>

<message name="DefineMessageResponse">
 <part name="result" type="xsd:int"/>
</message>

<portType name="MessageLoggerPortType">
 <operation name="DefineMessage">
  <documentation>Service definition of function ns__DefineMessage</documentation>
  <input message="tns:DefineMessageRequest"/>
  <output message="tns:DefineMessageResponse"/>
 </operation>
</portType>

<binding name="MessageLogger" type="tns:MessageLoggerPortType">
 <SOAP:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
 <operation name="DefineMessage">
  <SOAP:operation style="rpc" soapAction=""/>
  <input>
     <SOAP:body use="encoded" namespace="urn:MessageLogger" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
  </input>
  <output>
     <SOAP:body use="encoded" namespace="urn:MessageLogger" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
  </output>
 </operation>
</binding>

<service name="MessageLogger">
 <documentation>gSOAP 2.7.6c generated service definition</documentation>
 <port name="MessageLogger" binding="tns:MessageLogger">
  <SOAP:address location="http://nnsb.dl.ac.uk:8015/MessageLogger"/>
 </port>
</service>

</definitions>

If you are starting from the WSDL file which you can download from the MessageLogger Web Service then gSOAP can convert this into a C code header file by


wsdl2h -c MessageLogger.wsdl

which creates the file MessageLogger.h
Next use soapcpp2 to create the C code client stubs and header files


soapcpp2  -C -c -x -w -L -t -I $(SOAPBASE)\import MessageLogger.h

which creates the files soapStub.h, soapH.h, soapC.c, soapClient.c and MessageLogger.nsmap
If you are starting from the def file then you can use this directly


soapcpp2  -C -c -x -w -L -t -I $(SOAPBASE)\import MessageLogger.def
A simple code segment (DefineMessage.c) which uses the ns__DefineMessage method created and has very much the same effect as the DefineMessage Tcl procedure described above is


#include "soapH.h"
#include "MessageLogger.nsmap"

#include <unistd.h>

const char server[] = "http://egmsg:8015/MessageLogger";
char myname[32];

int main(int argc, char **argv)
{ struct soap soap;
  int class_ = 1;
  int level = 3;
  char *source = &myname[0];;
  char *message = NULL;
  int result;
  int i;

        (void) gethostname(myname, 32);

	if (argc <= 1) {return 0;}

		for (i=1; i< argc; i++) {
			if (argv[i][0] == '-') {
				switch(tolower(argv[i][1])) {
					case 'c':
						class_ = atoi(argv[++i]);
						break;
					case 'l':
						level = atoi(argv[++i]);
						break;
					case 's':
						source = argv[++i];
						break;
					default:
						message = argv[i];
				}
			} else {message = argv[i];}
			if (message != NULL) {break;}
		 }

  soap_init(&soap);
  
  soap_call_ns__DefineMessage(&soap, (char *)server, NULL, class_, level, (char*)source, (char *)message, &result);
  
  if (soap.error)
  { soap_print_fault(&soap, stderr);
	exit(1);
  }

  soap_destroy(&soap);
  soap_end(&soap);
  soap_done(&soap);
  return 0;
}

Finally to create an executable

gcc -Wall -O2 -I $(SOAPBASE) -I . -o DefineMessage DefineMessage.c soapC.c soapClient.c $(SOAPBASE)\stdsoap2.c
For full details of the installation of gSOAP see the provided README.

(4) Server

The TclHttpd server is started by running the Tcl script /M90AS/TclHttpd/bin/httpd_Server.tcl.
This will also load the file /Midas/tcl/package-load.tcl as for the client case to make available useful packages. Then ,in order, the files /MIDAS/config/TclHttpd/****/startup.tcl [where **** is the result of running the uname -n]; /MIDAS/config/TclHttpd/localhost/startup.tcl and /MIDAS/config/TclHttpd/startup.tcl are checked. The first to exist is taken to supply commands in order to start the services to be provided.

N.B. The file /MIDAS/TclHttpd/****/TclHttpd-server where **** is the OS in use is a shell script which calls the program tclsh to execute the script
/MIDAS/TclHttpd/bin/httpd_Server.tcl. If your path is set up to use a previously installed copy of Tcl you may require to use the following to ensure that the correct version of TclHttpd/bin/httpd_Server is used and the required environment variables have been setup. This (the file /MIDAS/TclHttpd/Linux/TclHttpd-server) is an example (for Linux).


#!/bin/csh -f

# ensure MIDASBASE points to root of Midas software installation
if (! $?MIDASBASE) setenv MIDASBASE /MIDAS

if (! $?TCLVERSION) setenv TCLVERSION 8.4

if (! $?MIDAS_LIBRARY)   setenv MIDAS_LIBRARY ${MIDASBASE}/TclHttpd/Linux
if (! $?LD_LIBRARY_PATH) setenv LD_LIBRARY_PATH ${MIDAS_LIBRARY}

setenv LD_LIBRARY_PATH ${MIDAS_LIBRARY}:${LD_LIBRARY_PATH}
setenv PATH ${MIDASBASE}/TclHttpd/Linux:${PATH}

tclsh$TCLVERSION $MIDASBASE/TclHttpd/bin/httpd_Server.tcl

The following command which will be found in /MIDAS/config/TclHttpd/startup.tcl


source [file join $env(MIDASBASE) TclHttpd tcl MessageLoggerServer.tcl]

causes the MessageLogger service to be started.

The following output typically will be seen on the stdout stream from TclHttpd


can't find package limit
Running with default file descriptor limit
/debug user "debug" password "2qxi7vho72t2"
httpd started on port 8015

/MessageLogger

Additionally any other services included by the TclHttpd startup.tcl script may generate output.

The file /MIDAS/TclHttpd/tcl/MessageLoggerServer.tcl



#    MIDAS Access Server  for MessageLogger service

global env
source [file join $env(MIDASBASE) TclHttpd tcl MIDASDomainHandler.tcl]       ;#  load default support procedures

global MessageLoggerDirectory

if {[info exists env(MessageLoggerBase)]} then {
    set MessageLoggerDirectory $env(MessageLoggerBase)
} else {
    switch $tcl_platform(platform) {
        windows  {set MessageLoggerDirectory "c:\\midas\\log"}
        unix     {set MessageLoggerDirectory "/MIDAS/log"}
    }
}

global MIDAS_MessageLogger_Verbose
set MIDAS_MessageLogger_Verbose 0

# Use namespaces to isolate your methods
namespace eval urn:MessageLogger {

    proc DefineMessage {class level source message} {
        global MIDAS_MessageLogger_Verbose
        global MessageLoggerDirectory
        if {$MIDAS_MessageLogger_Verbose} {puts "called method MessageLogger with parameters $class $level $source $message"}

        set fileName [file join $MessageLoggerDirectory [clock format [clock seconds] -format "%d%b%y"]]
        set TimeStamp [clock format [clock seconds] -format "%H:%M:%S"]

        set message [format "%8s %2d %1d %-16s %s" $TimeStamp $class $level $source $message]
        
        set z [catch {open $fileName a} file]
        if {$z != 0} then {return -code error -errorcode "$file" -errorinfo "Error detected in open"}
        puts $file "$message"
        close $file

        return [rpcvar::rpcvar int 0]
    }

    # We have to publish the public methods...

     SOAP::export DefineMessage
}

# register this service with tclhttpd
puts [SOAP::Domain::register  -prefix  /MessageLogger -namespace urn:MessageLogger -uri urn:MessageLogger]


proc ::SOAP::Domain::MIDAS_SOAPDomainHandler_/MessageLogger {optsname sock args} {   \
    ::SOAP::Domain::MIDAS_SOAPDomainHandler_/DefaultAccessServer $optsname $sock $args}

creates the MessageLoggerServer domain and defines the external method DefineMessage which is used by our MessageLogger client.
Note that this is the full implementation of the Message Logger Service. Normally services are more complex!

The file /MIDAS/TclHttpd/tcl/MIDASDomainHandler.tcl defines the procedure ::SOAP::Domain::MIDAS_SOAPDomainHandler_/DefaultAccessServer which is used by the procedure ::SOAP::Domain::MIDAS_SOAPDomainHandler_/MessageLoggerServer. This will parse the incoming XML message; envoke the required method and generate the response. This is done purely for reasons of performance since we generate very simple XML which does not require use of the full XML/DOM implementation. However use of this file is optional and could be removed in which case the use of ::SOAP::Domain::MIDAS_SOAPDomainHandler_/DefaultAccessServer should also be removed and the default domain handler provided by the SOAP package will automatically be used.

In some cases it is not possible or not desirable to implement a server in pure Tcl. As an example it is assumed that a server needs an arbitrary package XXX Version 1.0 which is implemented as a C code plugin.

The XXX package is defined by /MIDAS/MIDASpackages/XXX1.0/pkgIndex.tcl


#   The XXX package

global env

switch $tcl_platform(os) {
   "Windows NT" {
       package ifneeded XXX 1.0 [load $env(MIDASBASE)/MIDASpackages/XXX1.0/Win/XXX.dll]
   }
   SunOS {
       package ifneeded XXX 1.0 [load $env(MIDASBASE)/MIDASpackages/XXX1.0/SunOS/libXXX.so]
   }
   Linux {
       switch -glob $tcl_platform(machine) {
           *86 -
           intel {
               package ifneeded XXX 1.0 [load $env(MIDASBASE)/MIDASpackages/XXX1.0/Linux/libXXX.so]
           }
           ppc {
               package ifneeded XXX 1.0 [load $env(MIDASBASE)/MIDASpackages/XXX1.0/linux-mvme5500/libXXX.so]
           }
       }
   }
}

which defines how the XXX package can be loaded for the various supported operating systems.

The XXX package is implemented using a C code plugin as an example of just how easy it is.


/*
 * The XXX Tcl command.
 */


#ifdef WIN32
#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#define __WIN32__
#endif

#include <tcl.h>


static int  XXXCmd _ANSI_ARGS_((ClientData clientData, Tcl_Interp *interp, int argc, char *argv[]));

#ifdef WIN32

DLLIMPORT Tcl_Command Tcl_CreateCommand (Tcl_Interp*, CONST char *, Tcl_CmdProc*, ClientData , Tcl_CmdDeleteProc*);
DLLIMPORT int         Tcl_PkgProvide    (Tcl_Interp*, CONST char *, CONST char *);

DLLEXPORT XXX_init;
    
BOOL WINAPI DllMain(HANDLE hModule, DWORD dwFunction, LPVOID lpNot) {return TRUE;}
#endif


/*
 * XXX Cmd --
 *
 * Results:
 *  none
 *
 */
 
static int XXX_Cmd(ClientData data, Tcl_Interp *interp, int argc, char *argv[])
{
    int rc;

    printf ("XXX speaking\n");

    printf("You have %d args\n", argc);

    if (argc == 2) {
           printf ("XXX received message: %s\n", argv[1]);
           rc = TCL_OK;
    } else {
        rc = TCL_ERROR;
    }
    
    return rc;
}

/*
 * XXX_Init --
 *  Initialize the Tcl XXX facility.
 *
 * Results:
 *  TCL_OK.
 *
 * Side Effects:
 *  None.
 */
 
int XXX_Init(Tcl_Interp *interp)
{

    Tcl_CreateCommand(interp, "XXX", (Tcl_CmdProc*) XXX_Cmd,  NULL, NULL);
    Tcl_PkgProvide(interp, "XXX", "1.0");
    return TCL_OK;
}

This C code is compiled to make a dynamic load library (either a .so or .dll file).
It defines a Tcl command XXX which when envoked outputs some diagnostic information and the received text string on its stdout stream.

For further details see the Tcl Web Server documentation

 

Return to documentation index documentation index


© 2011 NPG - STFC