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:
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).
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.
[ 1 ]
[ 2 ]
[ 3 ]
[ 4 ]
[ 5 ]
[ 6 ]
[ 7 ]
[ 8 ]
[ 9 ]