[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ] [ 9 ]

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("Rick")

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

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'/>
                  &nb5Z85Z8;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

background:#99CCFF">

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

Using a custom HTTP Client

Why would you use a custom HTTP client especially since ROPE already provides one? The problem is that the ROPE.WireTransfer component is limited. It doesn't support secure connections (SSL) or authentication (Basic Auth) which means that it's very difficult to protect your application. Proxy support is also fairly limited which can cause problems with firewalls and inhouse proxy servers. By using a WinInet based client (such as wwIPStuff) you can implement these features easily (proxy support is mostly automatic, SSL and Authentication are supported directly) and you can actually control more tightly how data is passed between the client and server including improved connection error handling.

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.

 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ] [ 9 ]