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:
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.).
In the next section I'll describe using a free VFP based SOAP
implementation called wwSOAP that can work around some of the problems
described with the MS SOAP Toolkit.
West Wind Web Connection and Web Services
West Wind Web Connection includes direct support for SOAP based Web
Services with both a wwSOAP client implementation and a wwWebService
process class that can handle incoming SOAP requests mapped to a specific
.wwSOAP scriptmap extension in IIS. To demonstrate how all of this works
I'll implement a stock lookup service as a SOAP application. We then also
look at another example in a VFP fat client application that is a bit more
sophisticated in a Time and Billing sample application. All of the samples
are available as part of the free wwSOAP classes which you can download
from
http://www.west-wind.com/wwsoap.asp.
Retrieving Stock Quotes from the Web
Let's talk a little about the application I'll build as an example.
Now I want you to understand up front that there are other ways to do this
example application, especially because some of this data that I'll be
using for quotes is directly available over the Web. However, this is
meant as an example to show how to present data and as such shows a
variety of ways that you can consume data from Web Services.
This application is an HTML based Web Server application that allows
you to add stocks to a personal portfolio. The user enters a symbol name
and a qty and the app then recalculates the portfolio based on the current
stock prices. The portfolio form also contains a simple stock quote
retriever that lets you pull a single quote and display the stock price
and other info. The stock data is retrieved from a SOAP Web Service that
I'll describe in detail. The Web Service is responsible for retrieving the
stock quotes in a variety of ways. The Web Service retrieves the actual
stock information from the NASDAQ and MSNBC Web sites (I used both for a
little variety <s>).So, we're dealing with three Web sites here: The Web
site that runs the portfolio application, the Web site that hosts the SOAP
Web Service and the stock server at NASDAQ or MSNBC. The portfolio
application can be considered an aggregation engine that consolidates data
from the local data store (the portfolio) and the Web Service.
Getting a Stock Quote from MSNBC
Let's start with retrieving only a single stock price based on a symbol
to demonstrate the basics of how Web Services work. Here's the code that
retrieves a stock quote from the MSNBC Web site using the wwHTTP class (included
as part of wwSOAP):
************************************************************************
* SOAPService :: GetStockQuoteSimple
****************************************
*** Function: Returns a stock quote by symbol
*** Assume: Must be connected to Web and
msn.moneycentral.com
*** Pass: lcSymbol -
*** Return: Last stock price in string
format
************************************************************************
FUNCTION GetStockQuoteSimple(lcSymbol as String) as String
lcSymbol = UPPER(lcSymbol)
oHTTP=CREATEOBJECT("wwHTTP")
lcHTML=oHTTP.HTTPGet("http://www.msnbc.com/tools/newsalert/nagetstk.asp?s="
+ lcSymbol)
RETURN EXTRACT(lcHTML,"N=",CHR(13),CHR(10))
ENDFUNC
To get the latest stock price for Microsoft for example you'd
simply do:
lcQuote = GetStockQuoteSimple("MSFT")
What you'll see is a string result that returns something like:
65.888. A pretty depressing number when considered that it's off from
Microsoft's 120 high earlier this year, huh?
Easy enough. So, now lets set this up as a Web Service that can be
generically called from other applications. To do this with Web Connection
you can use the Create Web Service option of the Web Connection Management
Console. To start the console type: DO CONSOLE and you'llget the wizard
shown in Figure 1.
Figure 5 Creating a new Web Service involves specifying of a new
file to create the Web Service class into. The template contains the class
plus a small loader function.
[ 1 ]
[ 2 ]
[ 3 ]
[ 4 ]
[ 5 ]
[ 6 ]
[ 7 ]
[ 8 ]
[ 9 ]