Home |  White Papers |  Message Board |  Search |  Products |  Purchase |  News | 

  Building and using a SOAP Web Service
  with Visual FoxPro and Web Connection

 

By Rick Strahl

http://www.west-wind.com/

 

Last Update: 10/24/2000

 

See Also:

XML Messaging in Distributed Applications (previous parts of this series)

Samples for this article

wwXML Conversion classes (required for samples)

wwIPStuff classes (contains wwHTTPData class)

           

  

Imagine that you need some specific information in your application such as a shipping rate calculator. You now go to a service search engine and look up availability for the type of service you need in a standard manner over the Web. Now imagine that you can get at this information easily and simply plug the info directly into your application. Sound too good to be true? Believe it or not the technological pieces to make this type of functionality possible are available today. Web Services promise to make this type of functionality a reality by bringing the same interlinked mechanisms that have made the Web so popular for Web browsing to application development by sharing data over the Web using standard formats. Web Services have become the new industry buzzword. Microsoft is talking about Web Services as the second life of the Internet that will tie together applications the same way that the Web Browser and URL based links have tied together HTML based Web pages. The Web at your Service is a new mantra rising. In this article Rick discusses what SOAP and Web Services are and then delves into creating a sample Web Service and calling and integrating it into an application.

Web Services promise to bring the same kind of interlinked functionality that hyperlinks brought to the browser experience to application development. 

The move to distributed application development is a natural evolution for the Web. It gets back to the roots of how data is used in applications in general. With HTML the focus has always been on presentation with data bound directly into the presentation. On the other hand the focus in distributed applications is on totally separating the display and the data delivery. XML has become the preferred way to provide the data to client applications. To date XML has rapidly gained ground as a messaging format that serves as an intermediary between the data and the consuming client application – XML is typically converted from some native data format like a database table or an object and then used as the transfer mechanism. The client then has the choice of consuming that data directly through the XMLDOM or by converting it back into a native format such as a table or object that maps the XML. In the latter scenario XML is primarily used as a persistence format to transfer a state from the server to client or vice versa. In order to do this, all you need is an XML parser and a mechanism for pushing the XML over the wire via HTTP.

 

SOAP and Web Services don't change this model in any way. Instead SOAP standardizes it for the purpose of making remote calls on object methods or functions (such as script page calls) more natural. In custom XML applications the application – both client and server – have to know what the message format is beforehand which results in some amount of coupling between the client and the server. By providing a standard mechanism for representing the procedure call interface and a mechanism for querying what functionality is available and what the signature of each call is, SOAP can abstract away the explicit XML conversions that occur in custom XML implementations. To make this process truly seamless some services or tools must be in place that can provide the SOAP XML packaging and unpackaging and perform the wire transfer operations. The current flock of tools is not there yet, although as we'll see in a minute it only takes a few lines of code to make a remote procedure call in this fashion.

 

SOAP is a contender in the field for remoting technologies like DCOM and CORBA. Unlike those technologies SOAP has the advantage of easily running over HTTP and avoiding the complex configuration and security administration issues that surround DCOM for example. Because SOAP uses HTTP it can take advantage of the HTTP features like encryption and authentication as well as having the benefit of going through the Enterprise firewall.

 

It's important to understand though that SOAP is not meant to replace DCOM or CORBA in high performance environments. SOAP has a lot of overhead associated with it  compared to these lower level binary formats. HTTP is slower than native TCP/IP for example and the XML encoding required by the SOAP messages cause SOAP to be up to a 1000 times slower than a similar implemtation using DCOM (it depends on what type of SOAP server you use of course – if you build an ISAPI all C++ listener you probably get better numbers.

 

It's important to remember that you should not think of SOAP as a Remote COM implementation. Although the MS SOAP implementation focuses on exposing COM objects as Web Services, that is just one way you can create a Web Service. SOAP is open and does not specify how a Web Service must be implemented, so you can implement a SOAP Web Service with a function in script code in ASP, a visual FoxPro class, JSP class or a COM object run through the SDL Wizard.

 

What makes SOAP so nice is that it's relatively simple protocol that's easy to implement and work with and building a custom SOAP server that can handle requests is trivial.

What's a Web Service?

Over the last few issues I've discussed using XML in a distributed messaging architecture. The basic concept in these scenarios is using the HTTP protocol to communicate between client and server and passing data in XML format over the wire. This mechanism works really well for custom applications that follow a standard format and where both sides know and agree on the message format (the XML structure, the URL to call etc.)

 

Web Services are based on the same concepts and technologies, but extend this mechanism by providing a standard interface as to how the server side Web Service is called. This interface comes in the form of the Simple Object Access Protocol or SOAP. SOAP's mission in life is to provide a standard, XML based interface to making remote procedure calls. The purpose of SOAP is to standardize the way that data is requested and remote code is executed by providing standard parameter information (input) and return value (output) that returned in an XML document that follow the SOAP protocol specification. A server SOAP implementation is required to handle incoming SOAP requests. This implementation can be implemented with any Web Backend. MS SOAP uses ASP, Web Connection uses a special process class to handle SOAP requests to a specific Web scriptmap. Keep in mind that the server implementation is independent of the SOAP spec. As long as it can read the incoming SOAP Request packet and return a valid SOAP Response packet it's fulfilling the SOAP contract.

 

In simplistic terms the idea behind a Web Service and SOAP is nothing more than making a remote function call over the Internet. The client passes a parameter, the server returns a result value. It's not quite that easy yet, but we can look forward to full development tool integration of SOAP and Web Services that literally will blur the lines between where code is executed, whether it's in you own compiled application or from a service sitting on the other side of the globe with a 12 hour time difference.

Why bother since XML is already here?

If you're already building XML based applications you may be asking yourself just about now what is so different about using SOAP compared to using standard XML messages? There are many reasons why SOAP has advantages over raw XML as a protocol, but here are the two most important ones:

 

 

 

 

SOAP is still evolving as we speak, but tools that let you take advantage of it today are available. The most visible tool is Microsoft's SOAP Toolkit. For more info on the MS implemententation see my article Using SOAP for remote object access with Microsoft's SOAP Toolkit. In this article I'll discuss building a Web Service with West Wind Web Connection's Web Service implementation, which is much easier and more flexible in a number of ways especially when demonstrating functionality.

 

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.

Getting stock quotes over the Web

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'll  get the wizard shown in Figure 1.

 

Figure 1 – 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.

 

In the dialog you need to specify a file location for the Web Service. This file should be placed into a Web virtual directory, because the Web Server will actually access this file and route it to the Web Connection Web Service handler via the .wwSOAP script map extension. The actual template generated looks like this:

 

*** DO NOT REMOVE - CALL WRAPPER

PARAMETERS lcMethod, lcParmString, lvResult

PRIVATE _loServer

_loServer = CREATEOBJECT("StockService")

lvResult =  Eval("_loServer."+ lcMethod + "(" + lcParmString+ ")" )

RETURN lvResult

ENDFUNC

*** DO NOT REMOVE - CALL WRAPPER

 

*************************************************************

DEFINE CLASS StockService AS Session OLEPUBLIC

*************************************************************

 

*** Remove after testing

FUNCTION Test(lcEcho)

IF EMPTY(lcEcho)

   RETURN "Test Result"

ENDIF  

RETURN lcEcho

 

ENDDEFINE

*EOC test

 

The Web Service consists of a small loader that's called by the Web Connection Web Service engine, which in turn loads the class and calls the method in question. Note that the class is created with the OLEPUBLIC keyword. The Web Connection Web Service classes don't load this class as a COM object however – the OLEPUBLIC is only used to create an SDL file for consumption by MS SOAP as well as providing the ability to compile your object into COM object that can be called from an MS SOAP hosted Web Service. More on that later.

 

The Wizard also generates a test method into the class as well so you can check out the Web Service easily. Let's remove that test class and instead add our GetStockQuoteSimple() function into the class as a method like this:

 

 

*************************************************************

DEFINE CLASS StockService AS Session OLEPUBLIC

*************************************************************

 

************************************************************************

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

 

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

*  SOAPService :: GetStockQuoteSimple

 

ENDDEFINE

*EOC test

 

That's all there's to it! Voila, you've created your first Web Service!

 

Before you go on make sure the Web Connection Server is running. Note that Web Services in Web Connection are dynamically compiled both under VFP 6 and 7. This means that you can make changes to the Web Service while the Web Connection server is running and without stopping the Web service.

Calling the Web Service

Let's make sure that the service actually works. Since this service exists on the Internet on the West Wind Web Site I'll use that as an example. wwSOAP includes a SOAP Method Tester form that you can use to quickly check out a Web Service. Figure 2 shows how to set up the form to call our newly created Web Service:

 

Figure 2 – The SOAP method tester allows you to quickly test Web Services without writing any code.You can also view the SOAP Request and Response to see what the SOAP messages look like.

 

Fill in the URL to the Web Service in this case the I used the service running on the West Wind Web Site. If you're testing on your local machine use localhost and the virtual directory that you copied the Web Service to instead. Enter the method name and each of the parameters required, in this case on the symbol and set the type. Note what happens if you play with the types. Try passing an integer instead of a string. You get an error, which is the error message thrown by the FoxPro Web Service code: Variable 'LCHTML' is not found. wwSOAP embeds type information into the SOAP messages and the Web Service on the other end interprets those types. So when you passed an integer from the client it gets passed to the server application which fails in the GetStockQuoteSimple() method call because we didn't check for a non-string input parameter.

 

If you click on the SOAP Request button you can look at the request packet traveling over the wire:

 

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"

 SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"

 xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"  

 xmlns:xsd="http://www.w3.org/1999/XMLSchema">

<SOAP-ENV:Body>

<GetStockQuoteSimple>

      <lcSymbol xsi:type="xsd:string">MSFT</lcSymbol>

</GetStockQuoteSimple>

</SOAP-ENV:Body>

</SOAP-ENV:Envelope>

 

This is a basic SOAP request envelope that consists of the following sections:

 

 

The SOAP response is very similar:

 

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"

 SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"

 xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"

 xmlns:xsd="http://www.w3.org/1999/XMLSchema">

<SOAP-ENV:Body>

<GetStockQuoteSimpleResponse>

      <return xsi:type="xsd:string">65.188</return>

</GetStockQuoteSimpleResponse>

</SOAP-ENV:Body>

</SOAP-ENV:Envelope>

 

The layout of the SOAP Response is identical to the Request with the exception that here the return value is returned in the body section.

 

The SOAP Method Tester uses the wwSOAP client behind the scenes to perform the SOAP method calls. Let's see what we have to do to call the service with code:

 

* Function GetStockQuoteSimpleSOAP

LPARAMETERS lcSymbol

 

IF EMPTY(lcSYMBOL)

  lcSymbol = "MSFT"

ENDIF

 

*** Load wwSOAP dependencies

DO wwSOAP

 

oSOAP = CREATEOBJECT("wwSOAP")

oSOAP.cServerUrl = "http://www.west-wind.com/wconnect/soap/stockservice.wwsoap"

oSOAP.AddParameter("lcSymbol","MSFT")

 

lcPrice = oSOAP.CallMethod("GetStockQuoteSimple")

 

IF oSOAP.lError

  MESSAGEBOX(oSOAP.cErrorMsg)

  RETURN

ENDIF

 

? lcSymbol + ":", lcPrice

? "Result Type: " + VARTYPE(lcPrice) 

 

Pretty easy, eh? You basically specify the URL to call, add parameters then call the method. CallMethod() goes out and does all of the hard work of packaging up your parameters into the SOAP XML packet, sending the request over the wire and decoding the result into a simple return value. wwSOAP also  includes low level methods that let you do these steps independently and properties like cRequestXML and cResponseXML let you view what goes over the wire. The wwSOAP documentation help file includes examples on how to do this.

 

Notice when you run this that the Web Service returns a character value. That's not really a big problem – you can just run VAL() on the returned quote, but it would be much nicer if the server actually did this for us. Since wwSOAP and most SOAP applications understand embedded types in the SOAP packet we can make a simple change in our Web Service code to return a numeric value. Just change the last line in the GetStockQuoteSimple method of the Web Service to:

 

RETURN VAL( EXTRACT(lcHTML,"N=",CHR(13),CHR(10)) )

 

Save, then simply re-run the SOAP test code above and notice that now the value returned is a numeric value! Notice how simple this process is: You didn't have to recompile or stop the server. You simply change the code and the new value is immediately returned to you.

Expanding the Stock Web Service with Objects

Returning a single value like a stock is nice, but it's not all that useful. If you wanted to retrieve information about a stock you'd probably want to know a few things about the stock. Like the high and low and change for the day, the actual time of the quote and a few other things. You could set up multiple Web Service methods that return each of these values and then make several SOAP calls to these methods, but this would be vastly inefficient since it requires multiple round trips to the server. SOAP calls have a fair amount of overhead in the packaging and unpackaging of parameters and return values, and passing that data over the Web and through the Web Server. There is a base amount of overhead that occurs for every hit in addition to the time it takes to run the actual request, so bundling up data into a single SOAP package is a big advantage.

 

For this reason SOAP supports embedding of object parameters and return values. Since VFP can return objects from method calls and wwSOAP supports objects you can create a method that returns an object as a result value. There's a catch though as we'll see in a minute: The client side must provide an object instance to receive the SOAP result.

 

To demonstrate lets add a new method to our Web Service called GetStockQuote which will return an object that contains several properties retrieved from a stock quote retrieved from the NASDAQ Web site. The NASDAQ site provides quotes in XML format and this method retrieves values from this XML packet:

 

************************************************************************

* StockService :: GetStockQuote

****************************************

***  Function: Returns a Stock Quote

***    Assume: Pulls data from msn.moneycentral.com

***      Pass: lcSymbol  -  MSFT, IBM etc.

***    Return: Object

************************************************************************

FUNCTION GetStockQuote(lcSymbol as String) as Object

 

lcSymbol = UPPER(lcSymbol)

 

oHTTP = CREATEOBJECT("wwHTTP")

lcHTML=oHTTP.HTTPGet("http://quotes.nasdaq.com/quote.dll?page=xml&mode=stock&symbol=" +;

                     lcSymbol)

 

loQuote = CREATEOBJECT("Relation")  

 

loQuote.AddProperty("cSymbol",lcSymbol)

loQuote.AddProperty("cCompany",EXTRACT(lcHTML,"<issue-name>","</issue-name>"))

 

loQuote.AddProperty("nNetChange",;

                    VAL(Extract(lcHTML,"<net-change-price>","</net-change-price>")))

 

loQuote.AddProperty("nLast",;

                    VAL(EXTRACT(lcHTML,"<last-sale-price>","</last-sale-price>")))

loQuote.AddProperty("nOpen",;

                 VAL(Extract(lcHTML,"<previous-close-price>","</previous-close-price>")))

loQuote.AddProperty("nHigh",;

                    VAL(Extract(lcHTML,"<todays-high-price>","</todays-high-price>")))

loQuote.AddProperty("nLow",;

                    VAL(Extract(lcHTML,"<todays-low-price>","</todays-low-price>")))

loQuote.AddProperty("nPERatio",;

                    VAL(Extract(lcHTML,"<current-pe-ratio>","</current-pe-ratio>")))

 

lcOldDate = SET("DATE")

SET DATE TO YMD

lcDate=Extract(lcHTML,"<trade-datetime>","</trade-datetime>")

loQUote.AddProperty("tUpdated",;

                   CTOT( SUBSTR(lcDate,1,4)+"/" + SUBSTR(lcDate,5,2) + "/" +;

                   SUBSTR(lcDate,7,2)  + SUBSTR(lcDate,9) ))

SET DATE TO &lcOldDate

 

RETURN loQuote

 

This code retrieves the XML based stock quote from NASDAQ and then parses several of the XML properties into object properties for easier access and returns the newly created object over SOAP.

 

If you now call this method with the SOAP Method Tester you can use:

 

Url: http://www.west-wind.com/wconnect/soap/stockservice.wwsoap

Method: GetStockQuote

Parameter: lcSymbol  - MSFT - string

 

The result that is returned is an XML fragment that looks like this:

 

<return xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:type="record">

      <ccompany>Microsoft Corporation</ccompany>

      <csymbol>MSFT</csymbol>

      <nhigh>66.13</nhigh>

      <nlast>65.19</nlast>

      <nlow>61.13</nlow>

      <nnetchange>3.31</nnetchange>

      <nopen>61.88</nopen>

      <nperatio>38.12</nperatio>

      <tupdated>2000-10-20T00:00:00</tupdated>

</return>

 

The SOAP Method tester simply displays this XML fragment, but when you use code to call this method you can actually retrieve the object directly.

 

Here's the client code to do this:

 

* Function GetStockQuoteSOAP

LPARAMETERS lcSymbol

 

IF EMPTY(lcSYMBOL)

  lcSymbol = "MSFT"

ENDIF

 

*** Load wwSOAP dependencies

DO wwSOAP

 

oSOAP = CREATEOBJECT("wwSOAP")

oSOAP.cServerUrl = "http://www.west-wind.com/wconnect/soap/stockservice.wwsoap"

oSOAP.AddParameter("lcSymbol","MSFT")

 

*** Create object to be filled with result

loQuote = CREATEOBJECT("cStockQuote")

 

lcPrice = oSOAP.CallMethod("GetStockQuote",,loQuote)