Home |  White Papers |  Message Board |  Search |  Products |  Purchase |  News | 

 Using Microsoft's SOAP Toolkit
 for remote object access

For a more up to date tool that uses more the reliable .NET Web Service Client to call Web Services from Visual FoxPro check out:

West Wind Web Service Proxy Generator for Visual FoxPro

 

by Rick Strahl

West Wind Technologies

www.west-wind.com

 

Microsoft's SoapToolKit:
http://www.msdn.microsoft.com/xml/general/toolkit_intro.asp

 

Examples for this paper:

http://www.west-wind.com/presentations/soap/soapsamples.zip

 

Last update: 06/27/2009

Donate


Web Services and XML Messaging are gaining ever more attention and at the core of this movement is the Simple Object Access Protocol (SOAP), which promises to bring a standard interface to calling server side code. In this article Rick discusses what SOAP is how it works and how to use Microsoft's SOAP Development Kit to call COM components and script code over the Web.

 

SOAP's mission in life is to provide a standard, XML based interface to making remote procedure calls. Distributed application development has really taken off over the last year or so, and XML has been at the center of making it possible to efficiently share data and content between client and server applications. I've written a number of papers (see resources at the end of the article) on the basic concepts of XML messaging in custom application environments. In these scenarios the XML is based on custom XML structures that map application specific functionality. Typically the data maps to table based data or to business objects with the XML providing the message format to share the data over the wire. While this works really well for custom applications, it doesn't use a standard message format and it leaves the XML management up to the application. SOAP promises to help in both respects by using a standard XML wrapper (called an envelope) that transports the content of the message (typically parameters to a function call and result values). Special system tools (such as Microsoft's ROPE.DLL client) can then be used abstract the process of making the remote call. When it's all said and done SOAP's goal is to make it as easy to call a remote function as it is making a function call in your local application. As we'll see shortly it's not quite this easy, but this is definitely a step in the right direction.

 What is SOAP?

The move to distributed application development is a natural evolution for the Web. It gets back to the roots of how data is used in applications in general. With HTML the focus has always been on presentation with data bound directly into the presentation. On the other hand the focus in distributed applications is on totally separating the display and the data delivery. XML has become the preferred way to provide the data to client applications. To date XML has rapidly gained ground as a messaging format that serves as an intermediary between the data and the consuming client application – XML is typically converted from some native data format like a database table or an object and then used as the transfer mechanism. The client then has the choice of consuming that data directly through the XMLDOM or by converting it back into a native format such as a table or object that maps the XML. In the latter scenario XML is primarily used as a persistence format to transfer a state from the server to client or vice versa. In order to do this, all you need is an XML parser and a mechanism for pushing the XML over the wire via HTTP.

 

SOAP doesn't change this model in any way. Instead it standardizes it for the purpose of making remote calls on object methods or functions (such as script page calls) more natural. In custom XML applications the application – both client and server – have to know what the message format is beforehand which results in some amount of coupling between the client and the server. By providing a standard mechanism for representing the procedure call interface and a mechanism for querying what functionality is available and what the signature of each call is, SOAP can abstract away the explicit XML conversions that occur in custom XML implementations. To make this process truly seamless some services or tools must be in place that can provide the SOAP XML packaging and unpackaging and perform the wire transfer operations. The current flock of tools is not there yet, although as we'll see in a minute it only takes a few lines of code to make a remote procedure call in this fashion.

 

The implementation of this 'middle-ware' tool is up to the vendor and in fact major implementations from Microsoft, IBM and DevelopMentor (one of the original partners in the SOAP spec group) differ significantly in their implementations. However, the actual content that goes over the wire – an XML document – is interchangeable between these implementations. The idea is that the customized aspects of each implementation facilitates the actual calling process and results in fully spec compliant SOAP packages being sent over the wire. The rest is simply services that are used internally by the implementation.

SOAP is not a replacement for DCOM or CORBA

It's important to understand that SOAP is not to be considered as COM over the Web. For one, the server handling a SOAP request does not need to use COM to return a result. In fact results can be generated through simple ASP script code that never calls a COM object providing a sort of remote scripting. You can also use VFP code which may or may not sit in a COM object. It can be Java code or an ASP+ Web service in Visual Studio 7. The implementation and the tool of implementation is up to the developer and independent of the SOAP spec.

 

There is a significant difference between what COM provides and what SOAP does. SOAP is only a protocol that mandates how a method call transfers over the wire. More importantly, SOAP caters only to single function or method invocations, which means you can't maintain state to an object between calls. With SOAP, the client makes single method or function calls, one per HTTP request. The SOAP model follows standard Web and distributed application fare: Stateless transactions with disconnected data used to provide the client with the data needed to do its work. Get the data from the Web server in a method call, use the data offline locally, the send the updated data back to the server in another stateless remote call.

 

This is very different from DCOM or CORBA where a client creates a persistent connection to the object to perform multiple property accesses and method calls. DCOM and CORBA both support stateful remote connections – SOAP typically does not (although it's conceivable this could be implemented with server side state management of an application, something that's usually to be avoided in distributed applications).

 

The SOAP concept is simple: If a single wire format is used to call a function or object method on the server, the server doesn't care what type of client the call is being made from. So you could be calling with the Microsoft SOAP Toolkit from a Windows 2000 machine, or you could be calling from an IBM Java SOAP implementation running on a Linux box. The server can run ASP script code, a COM object, or run a Java Servlet on Linux – as long as the data traveling over the wire matches the SOAP spec they all can talk to each other. The server doesn't care and services the request as needed and simply returns a SOAP compliant result package.

 

Implementations vary however, with Microsoft's implementation using an intermediary SDL file that

acts as a sort of remote type library for the Web service. This concept is not supported by other implementations such as the one from IBM. But because the Microsoft packager still outputs a plain SOAP package in the end it can call an IBM implementation on the server or talk to an IBM client.

 

What all this means is that SOAP is vendor independent and the spec has been submitted for standard status. Several large players besides Microsoft have added support including IBM, but some big players like Sun, Oracle and Netscape (surprise, surprise, eh?) are not on board as of yet. Third party and free tools are available for those platforms however.

 Microsoft's SOAP Toolkit

In May Microosoft introduced the SOAP Toolkit, which has now been updated with a July release that fixes a few bugs and adds slightly improved HTTP support. It should be noted that the Microsoft SOAP toolkit is distributed as a demo application, rather than a fully supported product at this time. The toolkit is meant as a pre-runner to the Web Services that will be provided by the Visual Studio .Net platform and employ a similar although more seamless SOAP implementation for calling remote Web Services. Support is available only through a public newsgroup, which is fairly active and has had some good discussions on the implications on using the toolkit.

 

When Microsoft released the SOAP toolkit I was pleasantly surprised at what I found: The toolkit comes with heaps of documentation, full source code (yes, full source for the ROPE client DLL, the ISAPI listener and the ASP listener – each of these has a ton of useful pieces of code for developers), a Wizard that generates Service Descriptions (SDL) from COM objects and several useful and easy to follow examples. Getting started involves downloading and installing the SOAP toolkit. I highly suggest you read through the first section of the help file that explains how the toolkit is structured – I'll review the main points here, but the documentation is very useful and easy to follow and has a number of useful tips and insights. The trouble shooting section is also good – it helped me figure out a couple of assumptions I made when I started.

 

The Microsoft toolkit is a custom SOAP implementation. What this means is that the SOAP messages passing over the wire are SOAP protocol compliant, but that there are additional tools and operations that occur to verify method call signatures. In particular the toolkit uses a Service Description file in XML Schema format that describes what methods the Web Services exposes and how those methods should be called. You can think of the SDL (Service Description Language) file as a server side type library that both the client and the server use to validate the method call parameters and result values.

 

Figure 1 – The Microsoft Toolkit relies on a Service Description to provide type information both to the client and the server applications accessing the 'Web Service'. Based on the type information SOAP messages are created and passed over the wire. The SDL is implementation specific, but is not required as long as the server receives a valid SOAP request from the client and the client receives a valid SOAP response from the server.

 

Here's how it works: The process is started by creating a Web Service on the server. To help with this process an SDL Wizard is provided which can create a Web Service template from an existing COM object. Let's say we have a COM DLL SoapDemo with a SoapServer class and a HelloWorld method to match Figure 1. The Wizard generates the SDL file as well as a wrapper ASP page that calls the COM component. The ASP page references a library ASP page that knows how to process the SOAP request and pass the call on to the method in the generated ASP file. The actual method does nothing more than a CreateObject followed by a call to the method with the parameters and returning that value back to the listener.

 

The client application initiates a Web Service request by downloading the SDL file. The SDL file is then passed to the Proxy object, which uses it to create the SOAP package that is put onto the wire and transferred to the server. The ROPE.DLL provides both the SOAP encoding/decoding functionality (SOAPPackager methods) as well as the ability to make HTTP calls to the server (WireTransfer component). The SOAP request is then sent to the server for processing. Note that the actual SOAP request is the key piece here – the server won't care how the SOAP package was created as long as it is in the right format. So, you can manually create the appropriate XML via code, or you could have a non-Windows client create that message. The server doesn't care and happily processes the message.

 

The code on the server fires the ASP file the Wizard generated which has the name of the class – SoapServer.asp. The ASP page includes Listener.asp which is a library file that contains the code that uses ROPE.DLL to process the incoming SOAP message, route the request to the function in SoapServer.asp, and then package up the result value into a response SOAP package. The server also uses the SDL file to validate the parameters the client passed as well as the result value that the listener function returns. The call to the function is made, The actual function merely instantiates the COM object, calls the method and returns the value to the listener code. The listener, checks the return value and if it doesn't match the type in the SDL file, creates an error response package. If it does match a Result SOAP package is sent back to the client.

 

The client now picks up the SOAP message as part of the Proxy's operation to make the SOAP call. Behind the scenes the Proxy object is making an HTTP POST request against the server using the WireTransfer component posting the SOAP request and retrieving the SOAP response in a single HTTP operation. Once the result comes back the result is unbundled in a value that's properly typed and returned to the client application.

 

The SOAP toolkit supports both high level and low-level methods for calling server side code, with the high-level methods taking about 10 lines, while the low level takes about 25. It's relatively easy to get up and running as we'll see in a minute.

 

The MS SOAP toolkit doesn't support all of the supported SOAP datatypes. In fact you're limited to the following types as parameters and return values:

 

 

XML data type

Variant type

string

VT_BSTR

Boolean

VT_BOOL

byte

VT_I1

short

VT_I2

integer

VT_I4

long

VT_I8

float

VT_R4

double

VT_R8

 

Notably you can't return objects, which makes sense in this context. (Note: .Net's Web Services can return persisted objects). Note that date values are also not explicitly supported – you can pass and return dates only as strings.

 

There are few additional restrictions on the these types: They must be XML compliant to the point that they can live in a regular element tag. If strings contain other XML (especially XML containing CDATA content) or extended ANSI characters the SOAP call will fail. You'll get 'Bad SOAP return' errors. I'll talk about some workarounds for this later.

 Installation of the Toolkit

The toolkit comes with an install program, but the install program doesn't set you up for running any of the demos as it simply copies the files to the directory specified. Once installed you need to do the following to run the samples:

 

  • Register ROPE.DLL
    Use the command prompt to navigate to the directory where ROPE.DLL lives and REGSVR32 ROPE.DLL
  • Set up the Samples\Server directory as a Web Virtual
    In order to access the sample services you have to either move the provided samples into an existing Web directory that supports ASP scripts or you have to create a new virtual directory in the installed locations (which is probably easiest). You can do this with the IIS Management console. Make sure you create the virtual directory with Script and Execute rights enabled and that the directory has Read/Execute rights set for IUSR_/IWAM_
  • Make sure the ROPE.DLL has Read/Execute for IUSR_
    Since the ASP listener has to instantiate the ROPE.DLL it needs to be accessible by the IUSR_ or IWAM_ account with READ/EXECUTE rights. This bit me hard, as I have my machine locked down and I didn't get error messages that helped me track this down.

 

A few tips if you have problems: Read the help file's trouble shooting section! It's quite good. Additionally the server side code generates an error log file in c:\webservices by default, which contains the error log file. The messages there aren't always clear, but they will most likely point you in the right direction.

 

Once you've configured the server pieces you can run the sample VB program in the samples/client directory. The sample calls server side code in a variety of different ways which is useful to see the different ways you can handle the SOAP messaging. If you don't like looking at VB code – or even if you do – read on for VFP examples.

 Creating a Web Service from a COM object

Let's start by creating a simple COM component with a few methods and calling it from a client application using the ROPE proxy object.  The COM object looks like this starting with the mandatory HelloWorld method:

 

 

#DEFINE CRLF  CHR(13)+CHR(10)

 

*************************************************************

DEFINE CLASS SoapServer AS Session OLEPUBLIC

*************************************************************

 

************************************************************************

* SoapServer :: HelloWorld

****************************************

FUNCTION helloworld(lcName as String) AS String

RETURN "Hello World from " + GETENV("COMPUTERNAME") + ", " + lcName + "! Time is: " + TIME()

 

 

************************************************************************

* SoapServer :: Evaluate

****************************************

PROCEDURE evaluate( lcCommand as String)  AS Variant

      RETURN EVALUATE(lcCommand)

ENDPROC

 

************************************************************************

* SoapServer :: GetXML

****************************************

***  Function: Demonstrates returning XML results

***      Pass: lcName - Name to wrap in XML

***    Return:

************************************************************************

PROCEDURE getxml(lcName as String) as String

 

lcXML = [<?xml version="1.0"?>] + CRLF +;

         [<docroot>] + CRLF+;

         [  <name>] + lcName + [</name>] + CRLF + ;

         [</docroot>] + CRLF

 

RETURN lcXML

 

************************************************************************

* SoapServer :: GetXML

************************************************************************

PROCEDURE getxmlcdata(lcName as String) as String

 

lcXML = [<?xml version="1.0"?>] + CRLF +;

         [<docroot>] + CRLF+;

         [  <name><![CDATA[] + lcName + CHR(13) + CHR(10) + lcName + "]]</name>" + CRLF + ;

         [</docroot>] + CRLF

RETURN lcXML

 

PROCEDURE getobject(lnValue as Long, lcString as String, ldDate as Datetime) ;

         as soapdemo.retObject

 

loCustom = CREATEOBJECT("soapdemo.retObject")

loCustom.nValue = lnValue

loCustom.cString = lcString

loCustom.dDate = ldDate

RETURN loCustom

 

ENDDEFINE

 

DEFINE CLASS retObject as Relation OLEPUBLIC

nValue = 0

cString = ""

dDate = { : }    

 

ENDDEFINE

 

Figure 2 – The SDL Wizard asks for a COM component to import

 

The first step is to pick a COM componet that the Wizard will work with. The Wizard will generate one SDL file for each class you select from the next dialog:

 

Figure 2 – The SDL Wizard lets you pick which methods to expose via the Web Service. Each COM class/interface you select is generated into a separate SDL and ASP file.

 

Notice the red methods in Figure 2. These methods mean that the parameters and return types were not set up with specific types but use variants instead. SOAP will allow you to use variant parameters but they will always be returned as Text rather than by their variant types. For example, the above evaluate method takes a string input parameter but a variant output parameter so we can return any result that our COM object can EVAL to (a string, a date or numeric value etc.). If a numeric value is returned SOAP will return the number but it will be returned as a string. The same goes for dates and logical values (1 or 0 for True and False respectively).

 

In the final two steps you need to tell the Wizard where your Web service will be located:

 

Figure 3 – You have to specify the HTTP location where the Web service will be accessed from. This Web virtual directory must already exist – the Wizard will not create it.

 

Figure 4 – This dialog requests the physical location for the file that the Wizard will generate. Again this directory must exist and should be the physical directory of the Web location chosen in the previous step.

 

It's very important that the directory that you're copying the generated files to exists already. I would suggest you create the directory and make sure it's also set up as a virtual Web directory before you actually run the Wizard. The Web name is not checked by the Wizard and is used only as a template value in the SDL file's Address element that points at the location for the processing ASP page.

 

Note that you can create both an ASP and ISAPI listener. The ISAPI listener is actually less flexible than the ASP listener, although the ISAPI version can be more efficient. The ASP version has the advantage that it can be edited and add to the functionality of calling the COM object. In fact, the ASP methods don't even need to call the COM object at all, but could perform the processing in script code.

 

Once the Wizard completes you end up with two files in the specified directory:

 

SoapServer.asp       Your custom SOAP Listener

SoapServer.xml       SDL file

 

You also need to copy:

 

listener.asp                 Library file for SOAP message cracking

 

from the samples/server directory of your toolkit installation. Alternately you can put this file into a central location and change the reference to it in the SoapServer.asp file, which uses server side includes to include it.

 

And this takes care of the server side.

 

The Client Side

The SOAP toolkit ships with VB samples, so we'll have to translate those samples into VFP. I'll show two different ways of accessing content here – the easy way using the Proxy object's dynamic method naming functionality and using complete message parsing syntax, which allows you more control and ultimately is the only way to debug the process if something goes wrong.

 

To review we want to access our SoapServer Web service and call the HelloWorld method. The method signature looks like this:

 

Helloworld(lcName as String) as String

 

Note I'm using VFP 7 syntax which allows you to cast parameters and return values into specific types. VFP 7 doesn't really do anything different here than VPF 6 did, except that these names get written into the type library. When the SDL Wizard picked up the COM object it was then able to add the type information into the SDL file it created. Let's take a look at the SDL file which contains the Web Service type information for all of the methods included in the SoapServer class:

 

<?xml version='1.0' ?>

<!-- Generated 8/9/2000 2:55:09 AM by Microsoft SOAP Toolkit Wizard, Version 205.0.3 -->

<serviceDescription name='soapdemo'

    xmlns='urn:schemas-xmlsoap-org:sdl.2000-01-25'

    xmlns:dt='http://www.w3.org/1999/XMLSchema'

    xmlns:SoapServer='SoapServer'

>

<import namespace='SoapServer' location='#SoapServer'/>

 

    <soap xmlns='urn:schemas-xmlsoap-org:soap-sdl-2000-01-25'>

        <interface name='SoapServer'>

            <requestResponse name='helloworld'>

                <request ref='SoapServer:helloworld'/>

                <response ref='SoapServer:helloworldResponse'/>

                <parameterorder>lcName</parameterorder>

            </requestResponse>

            <requestResponse name='evaluate'>

                <request ref='SoapServer:evaluate'/>

                <response ref='SoapServer:evaluateResponse'/>

                <parameterorder>lcCommand</parameterorder>

            </requestResponse>

            <requestResponse name='getxml'>

                <request ref='SoapServer:getxml'/>

                <response ref='SoapServer:getxmlResponse'/>

                <parameterorder>lcName</parameterorder>

            </requestResponse>

            <requestResponse name='getxmlcdata'>

                <request ref='SoapServer:getxmlcdata'/>

                <response ref='SoapServer:getxmlcdataResponse'/>

                <parameterorder>lcName</parameterorder>

            </requestResponse>

            <requestResponse name='getobject'>

                <request ref='SoapServer:getobject'/>

                <response ref='SoapServer:getobjectResponse'/>

                <parameterorder>lnValue lcString ldDate</parameterorder>

            </requestResponse>

        </interface>

        <service>

            <addresses>

                <address uri='http://localhost/soap/SoapServer.asp'/>

            </addresses>

            <implements name='SoapServer'/>

        </service>

    </soap>

 

 

    <SoapServer:schema id='SoapServer' targetNamespace='SoapServer' xmlns='http://www.w3.org/1999/XMLSchema'>

        <element name='helloworld'>

            <type>

                <element name='lcName' type='dt:string'/>

            </type>

        </element>

        <element name='helloworldResponse'>

            <type>

                <element name='return' type='dt:string'/>

            </type>

        </element>

        <element name='evaluate'>

            <type>

                <element name='lcCommand' type='dt:string'/>

            </type>

        </element>

        <element name='evaluateResponse'>

            <type>

                <element name='return' type='dt:string'/>

                <element name='lcCommand' type='dt:string'/>

            </type>

        </element>

        <element name='getxml'>

            <type>

                <element name='lcName' type='dt:string'/>

            </type>

        </element>

        <element name='getxmlResponse'>

            <type>

                <element name='return' type='dt:string'/>

            </type>

        </element>

        <element name='getxmlcdata'>

            <type>

                <element name='lcName' type='dt:string'/>

            </type>

        </element>

        <element name='getxmlcdataResponse'>

            <type>

                <element name='return' type='dt:string'/>

            </type>

        </element>

        <element name='getobject'>

            <type>

                <element name='lnValue' type='dt:integer'/>

                <element name='lcString' type='dt:string'/>

                <element name='ldDate' type='dt:string'/>

            </type>

        </element>

        <element name='getobjectResponse'>

            <type>

                <element name='return' type='dt:string'/>

            </type>

        </element>

    </SoapServer:schema>

 

</serviceDescription>

 

This file is an XML schema that defines the class, its member methods and each of the parameters and return values as well as the Uri link that services this Web Service (the <address> tag in the soap:service fragment). In typical Schema fashion, you have a declaration section (<interface>) which points to the <soapserver> section for the implementation details such as parameters and return values. The schema is a bit verbose, but quite readable if you look at it closely.

 

The client needs to get this SDL file downloaded first and then can assign it to the proxy object to make the SOAP call over the wire. So the simple syntax looks as follows:

 

LOCAL oProxy as Rope.Proxy, oWire as Rope.WireTransfer

 

*** Download the SDL file

oWire=CREATEOBJECT("Rope.WireTransfer")

lcXML = oWire.GetPageByURI("http://localhost/soap/soapserver.xml")

 

*** Assign it to the Proxy so it can get type info

oProxy = CREATEOBJECT("Rope.Proxy")

? oProxy.LoadServicesDescription(2, lcXML) && .T./1

 

*** Call the 'dynamic' method

lvResult = oProxy.helloworld("Rick")

 

? VARTYPE(lvResult)

? lvResult

 

If all goes well with this code you'll get the result back as:

 

Hello World from WESTWINDSERVER, Rick! Time is: 11:30:12

 

So, how does this work? In the code above LoadServicesDescription is used to assign the SDL file string downloaded using the WireTransfer object is loaded into the Proxy object. Remember the SDL file contains the method signatures as well as the URL to the actual Service Listener (the ASP file) so it's fairly self contained. Based on that SDL definition the Proxy object dynamically 'adds' method signatures to its interface so you can seemingly just call the method directly retrieving a result value. Pretty simple right?

 

The call to helloworld actually performs the entire SOAP transfer of creating the SOAP message that goes on the wire, and then unpackaging the returned SOAP package into the return value. If something goes wrong (couldn't connect, or the method call failed) the call raises a COM error (typically 'Bad Soap Return') which you can trap in your code with a typical COM error handler (or you can use an Eval() and capture the result type).

 

This scheme of accessing server side methods is nice and easy but it has several problems. First you're stuck with the case sensitivity issues mentioned earlier – there's no control over how the methods are accessed. Additionally, if there's a problem in the call you can't get any information of what goes wrong. Although the SOAP packages contain error information, the above scheme doesn't return it to you, so you're left in the dark about any failures.

 

To work around this you can take a lower level approach which requires a bit more code to deal with effectively. With this approach you can see what's happening behind the scenes as the messages are created and what sent onto the wire:

 

#INCLUDE Rope.h  && provided with these samples

 

* create objects

LOCAL oSoap AS Rope.SoapPackager

LOCAL oWire AS Rope.WireTransfer

 

*** Must set URL for listener and SDL

lcListener = "http://localhost/soap/soapserver.asp"

lcSDLUri = "http://localhost/soap/soapserver.xml"

lcMethod = "helloworld"

 

oSoap = CREATEOBJECT("ROPE.SOAPPackager")

oWire = CREATEOBJECT("ROPE.WireTransfer")

 

lcResponse = oWire.GetPageByURI(lcSDLUri)

 

*** load ServicesDescription file

IF oSoap.LoadServicesDescription(icString, lcResponse) # 1

   MESSAGEBOX("Error loading SDL file")

ENDIF  

 

*** retreive the SOAP request structure for method (XML fragment)

sRequestStruct = oSoap.GetMethodStruct(lcMethod, icINPUT)

 

*** Set up the method to call

oSoap.SetPayloadData(icREQUEST,"", lcMethod, sRequestStruct)

 

*** Add parameters

oSoap.SetParameter(icRequest,"lcName","Rick")

 

*** Return the SOAP XML packet that goes on the wire

sRequestPayload = oSoap.GetPayload(icREQUEST)

 

MESSAGEBOX( sRequestPayload, 64, "SOAP XML Payload - Client to Server")

 

*** Now put the the packet on the wire and POST to Server

oWire.AddStdSOAPHeaders(lcListener, lcMethod, LEN(sRequestPayload))

 

*** Get the SOAP response (same wire call)

sResponsePayload = oWire.PostDataToURI(lcListener, sRequestPayload)

 

MESSAGEBOX( sResponsePayload,64, "SOAP XML Payload -  Server To Client" )

 

*** Assign the result XML

oSoap.SetPayload(icRESPONSE, sResponsePayload)

 

*** Parse the SOAP response back into a result value

sTemp = oSoap.GetParameter(icRESPONSE, "return")

 

MESSAGEBOX( sTemp, "Result" )

 

In this code you can see each of the steps along the way from creating the SOAP packages and then reading the response. If we actually look at the SOAP requests captured here is what you'd see:

 

SOAP Request

<?xml version="1.0"?>

<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP:Body>

<helloworld>

<lcName>Rick</lcName>

</helloworld>

</SOAP:Body>

</SOAP:Envelope>

 

SOAP Response

<?xml version="1.0"?>

<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP:Body>

<helloworldResponse>

<return>Hello World from RASNOTE, Rick! Time is: 11:49:21</return>

</helloworldResponse>

</SOAP:Body>

</SOAP:Envelope>

 

Very straightforward. Remember that as far as the server's expectations of the client are concerned it only needs to receive this SOAP request. As far as the client is concerned all it expects is this SOAP response. In fact if you wanted to be clever and create a simple demo using only a few lines of VFP you could do something like this (VFP 7 code):

 

*** Create the SOAP string

TEXT TO lcSOAPRequest NOSHOW

<?xml version="1.0"?>

<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP:Body>

<helloworld>

<lcName>Rick</lcName>

</helloworld>

</SOAP:Body>

</SOAP:Envelope>

ENDTEXT

 

*** West Wind HTTP components

loIP = CREATEOBJECT("wwIPStuff")

 

*** Use HTTP to POST the request and get SOAP Result

loIP.nHTTPPostMode = 4  && XML

loIP.AddPostKey("",SUBSTR(lcSoapRequest,3))  && Post the data

lcSOAPResponse = loIP.HTTPGet("http://localhost/soap/soapserver.asp")

 

*** Display the XML

ShowXML(lcSoapResponse)

 

*** Extract the result value (string)

lcResult = EXTRACT(lcSoapResponse,"<return>","</return>")

 

This code simulates the ROPE client code we used above using nothing but plain VFP code and it works exactly as you'd expect. Obviously you have to do a little work on your own here if you were to do this in a real application like creating the SOAP packet and properly typing the result value, but if you know what you're calling and what the signature is, you can significantly reduce overhead by doing things this way as you don't have to download the SDL file, and have the ROPE client parse the SDL file and perform the typing. 

On the Server

So far I've discussed the client side that you need to write to call the remote component running on the server. Let's take a closer look at what actually happens on the server. By default the SDL wizard creates a template ASP page for you that automatically calls your COM component. You don't really have to do anything to this ASP page – it should work as is without modifications. However, because the page is just an ASP page you can modify it and add functionality to it. For example, you could add support for security checks (logins via Basic Authentication) or validate that a request is coming in from a certain client IP address etc.

 

The SDL wizard created the following ASP file for our SoapServer COM class:

 

<%@ Language=VBScript %>

 

<% Option Explicit

 

Response.Expires = 0

 

'--------------------------------------------

' SOAP ASP Interface file SoapServer.asp

' Generated 8/9/2000 2:55:13 AM

' By Microsoft SOAP Toolkit Wizard, Version 205.0.3

'--------------------------------------------

 

Const SOAP_SDLURI = "http://localhost/soap/SoapServer.xml"  'URI of service description file %>

<!--#include file="listener.asp"-->

 

<%

'_________________________________________________________________________________

 

Public Function helloworld (ByVal lcName)

      Dim objhelloworld

      Set objhelloworld = Server.CreateObject("soapdemo.SoapServer")

 

      helloworld = objhelloworld.helloworld(lcName)

      'Insert additional code here

 

      Set objhelloworld = NOTHING

End Function

 

'_________________________________________________________________________________

 

Public Function evaluate (ByRef lcCommand)

      Dim objevaluate

      Set objevaluate = Server.CreateObject("soapdemo.SoapServer")

 

      evaluate = objevaluate.evaluate(lcCommand)

      'Insert additional code here

 

 

 

      Set objevaluate = NOTHING

End Function

 

… additional methods left out here

 

'_________________________________________________________________________________

 

%>

 

This page is straightforward: Every method in the class gets a function here with the parameters passed in and a result value returned. Notice that listener.asp is imported into this document and that the mainline code starts in listener.asp. This code basically performs task very similar to the tasks in the manual SOAP message packaging described above (code example 2), but specific to the server side.


The way it works is that it decodes the SOAP message getting the method name and parameters to pass. The Function in the above document is then called and the return value retrieved. The result is packaged up back into a SOAP package.

Adding custom functions to the SDL

If you look closely at the ASP functions that the Wizard generates you can see that you don't necessarily need to call the COM components. You could for example run just plain ASP code in these functions. In fact, this is an easy way to create 'remote scripting' code that returns results to the client.

 

You can also add custom functions to this ASP document, but if you do you need to add the functions you create to the SDL document manually. For example, to add a HelloWorldAspOnly method to the SDL file you'd add the following:

 

            <requestResponse name='helloworldasponly'>

                <request ref='SoapServer:helloworldasponly'/>

                <response ref='SoapServer:helloworldasponlyResponse'/>

                <parameterorder>lcName</parameterorder>

            </requestResponse>

 

into the Interface section and

 

        <element name='helloworldasponly'>

            <type>

                <element name='lcName' type='dt:string'/>

            </type>

        </element>

        <element name='helloworldasponlyResponse'>

            <type>

                <element name='return' type='dt:string'/>

            </type>

        </element>

 

into the SoapServer section.

 

You can now add a new function to the ASP page:

 

Public Function helloworldasponly (ByVal lcName)

 

helloworldasponly = "Hello from ASP, " & lcName & ". Time is: " & now

 

End Function

 

Voila – you've added a new method to your Web service and without even recompiling any code. Think of this as an easy way to do remote scripting.

SOAP and Variants

Speaking of scripting, notice that the server I built also included a generic method called Evaluate. This method takes a string input parameter and a variant output result. Variants are handled specially by SOAP – basically you can return variant data from your functions as any type. You can return strings, dates, Boolean and numbers. For example try this:

 

LPARAMETER lcEvalCommand

LOCAL oProxy as Rope.Proxy, oWire as Rope.WireTransfer

 

oWire=CREATEOBJECT("Rope.WireTransfer")

lcXML = oWire.GetPageByURI("http://localhost/soap/soapserver.xml")

 

 

oProxy = CREATEOBJECT("Rope.Proxy")

? oProxy.LoadServicesDescription(2, lcXML) && .T./1

 

lvResult = oProxy.evaluate (lcEvalCommand)

? VARTYPE(lvResult)

? lvResult

 

Then run the following in the command window:

 

soapproxy("sys(0)")

soapproxy("DateTime()")

soapproxy("Month(DateTime())")

 

What you'll see is that SOAP will return the correct values, but it returns them all as strings rather than their proper types. You'd have to do the type conversions yourself.

 

Note that this is a very powerful (but potentially dangerous) concept – you can create generic methods that execute code on the server. You could for example add a method called Execute to the COM server that runs a block of VFP code and returns a result by using the COMPILE command to actually execute that block of code (or you can use VFP7's new ExecScript() function for this). However, without access restrictions this becomes a very dangerous proposition – if you can run code generically you can generically delete your hard disks file too (ExecScript("erase \winnt\system32\*.*") anyone?)…

Soap and XML results

(note this may change by the time you read this XML support is promised in updates)

 

The current release of the SOAP toolkit does not directly support XML parameters and results. The sample server includes an XML result method getXML, which passes a single lcName parameter and returns that parameter wrapped into an XML block:

 

<?xml version="1.0"?>

<docroot>

      <name>Rick</name>

</docroot>

 

To call this method against the Web Serivce use:

 

lvResult = oProxy.getxml("lcName")

 

You'll get a 'Bad SOAP return' error. This 'generic' error message is about the only error you get from the dynamic method interface, and it's not terribly useful.

 

If you run this request through the SoapManual.prg (change the method name and parameter in the PRG) file you'll see that the SOAP Response actually returns the XML string to the client. However, because the XML is not delimited in a special way the returned XML is actually an invalid XML document, resulting in the SOAP call to fail:

 

<?xml version="1.0"?>

<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP:Body>

<getxmlResponse>

<return><?xml version="1.0"?>

<docroot>

      <name>Rick</name>

</docroot></return>

</getxmlResponse>

</SOAP:Body>

</SOAP:Envelope>

 

The above is invalid XML, so the parser that ROPE uses fails to do anything with the XML document. There's a crude workaround you can use for this:

 

  • Add a <![CDATA[ ]]> tag around the output generated in your code
  • Add a <![CDATA[ ]]> tag around the ASP function that returns the XML

 

I like the latter approach the least of the two evils:

 

Public Function getxml (ByVal lcName)

      Dim objgetxml

      Set objgetxml = Server.CreateObject("soapdemo.SoapServer")

 

      getxml = "<![CDATA[" + objgetxml.getxml(lcName) + "]]>"

      'Insert additional code here

     

      Set objgetxml = NOTHING

End Function

 

Now the XML response is valid:

 

<?xml version="1.0"?>

<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP:Body>

<getxmlResponse>

<return><![CDATA[<?xml version="1.0"?>

<docroot>

      <name>Rick</name>

</docroot>

]]></return>

</getxmlResponse>

</SOAP:Body>

</SOAP:Envelope>

 

Unfortunately, this only solves part of the problem. If your XML result includes a CDATA section of its own, the above won't work still resulting in an invalid XML document. Due to XML's limitations on what can be contained inside of a CDATA section you can't embed complex XML inside of the data.

 

The problem is more farreaching than this as well – if you return a result string that contains extended characters you may also run into trouble because of the way that ROPE packages the XML manually (not using the parser). For example, try this (in SoapProxy.prg):

 

lvResult = oProxy.helloworld("Rζ¦ick")

 

You'll get an error that nothing was posted.

 

If you try this with the SoapManual.prg you'll find that the data is not getting posted to the server

and the result times out after 10 seconds (ROPE's default timeout).

 

 SOAP Problems

As you can see there are few problems with the SOAP toolkit. Here are a list of things that I ran into when playing with it:

 

Parameter/Return type support limitations

  • No direct XML parameter support
    This supposedly got fixed in the July release of the toolkit, but I couldn't find any documentation on it.
  • Extended character problems
    It appears the ROPE code creates the SOAP package XML manually and doesn't properly encode the XML so extended characters will create invalid XML messages that won't properly load into the XMLDOM. This in turn results in call failures
  • No support for objects
    Objects are a tricky thing to pass around in a distributed environment, but it would be nice if the toolkit could at least persist objects in some way. In custom XML applications you would normally persist objects to XML, but with the above XML limitations this is not possible at least not reliably.

Wire Protocol Problems

  • Lack of SSL support
    You cannot access Web Services over SSL using the ROPE client. Just about any B2B application will require secure communication.
  • Lack of Authentication support
    You cannot pass authentication information to the client to allow only certain users to access your components. This means there's no application level control over who has access to your Web services. The only block possible is based on NT authentication at the ASP file level (module level) as opposed to the method level. You can't let one method be accessed by a specific user and have another method open to all for example.
  • Limited Proxy support
    Limited proxy support requires hand configuration.
  • No support for browser hosting
    ROPE.DLL is not available to browser applications as a safe and signed control so you can't take advantage of this functionality in a browser. That's too bad really, because browser based applications are among the most obvious consumers of Web Services.

 

Whether these issues are show stoppers for you depends on your application and implementation. As showed you before, you can actually sidestep many of these issues by build a custom SOAP client and server using Wininet to address the Wire Protocol issues and using custom code that uses the XMLDOM to create messages to ensure proper message types. The only insurmountable issue then remains to be passing <![DATA[]] as part of XML messages around.

 

Note, that because all source code is provided you can even fix up the ROPE client directly instead of rebuilding everything from scratch. However, building a ROPE client in Visual FoxPro code is surprisingly easy to do and may well be worth the effort in the control it gives you. You can also build basic ROPE-like functionality using scripting and the XMLDOM in Internet Explorer.

 

Check our Web site for updates that will include VFP and Jscript SOAP clients that can interact with the Microsoft SOAP toolkit and custom VFP SOAP servers created with any VFP capable tool (ASP with COM, Web Connection, FoxISAPI etc.).

 Summary

The Microsoft SOAP toolkit has gotten a lot attention since it release. Although it has a few rough spots it's an excellent preview of what's to come in the Visual Studio .Net platform where Web Services very similar to what the toolkit provides now can be exposed directly from exposed classes. As it is now creating exposed objects is a two step process of creating the COM component and then generating the service description using the SDL Wizard.

 

The concept of Web Services is a powerful one, and if you look at the model you can see that it has lots of potential. The SDL file seems like a perfect start of a cataloging tool that allows you find out what services are available on a given Web site. Robots in the future can go out and look for service descriptions and build search engines based on exposed Web Services that are available for example. So next time you need an XML based stock quote you can look at a service directory to get find this service and plug it into your application. The possibilities are endless.

 

But we're not there yet. The tools are still immature as evidenced by the return type problems and lack of industrial strength HTTP support, which is crucial for mission critical operations. Many applications don't need these features and for those that do custom implementations allow extension to fix the problems without deviating from the SOAP standard.

 

It's a distributed world and it's getting more connected all the time – SOAP is a step in the right direction to make it easier to get at the information available. Take a shower, and rub it in…

 Resources

 

Microsoft's Soap ToolKit:
http://www.msdn.microsoft.com/xml/general/toolkit_intro.asp

 

West Wind SOAP Manager classes:
http://www.west-wind.com/wwsoap.asp

 

Soap Spec:

http://msdn.microsoft.com/xml/general/soaptemplate.asp

 

MS SOAP Newsgroup:

news:microsoft.public.msdn.soaptoolkit

 

XML Messaging Article:
Article describes custom XML messaging strategies and a pre-SOAP implementation of calling remote COM objects on the server totally generically.

http://www.west-wind.com/presentations/xmlmessaging/xmlmessaging.htm

 

West Wind Web Connection:
Complete Web development framework for Visual FoxPro.

http://www.west-wind.com/webconnection.asp

 

West Wind Internet Protocols (wwIPStuff):

HTTP services (as well as SMTP, FTP and more) for Visual FoxPro
http://www.west-wind.com/wwIPstuff.asp

 

West Wind XML Converter (convert VFP/SQL data and object to and from XML):
Tools for converting Visual FoxPro, ADO and SQL data into XML and back. Also supports converting VFP objects into hierarchical XML.

http://www.west-wind.com/wwxml.asp

 

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows 2000 and Visual Studio. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro, West Wind HTML Help Builder and co-author of Visual WebBuilder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to FoxPro magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/.

   Home |  White Papers |  Message Board |  Search |  Products |  Purchase | 

Amazon Honor System Click Here to Pay Learn More