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
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 ]