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

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'/>
                  &nb5ZĀÃ8Á5ZĀÃ8Á;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.


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?)… 

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