Imagine that you need some specific information in your application, such as a shipping rate. You now go to a special "service" search engine and look up the type of service you need over the Web.

Now, imagine that you can get this information easily from the service and simply plug it directly into your application. Sound too good to be true? Believe it or not, the technologies to make this possible are available today. Web Services provide this functionality by bringing to application development the same interlinked mechanisms that have made the Web so popular for Web browsing. By sharing data over the Web in standard formats, "Web Services" is becoming the new industry buzzword. Microsoft is talking about Web Services as the second life of the Internet. Web Services will tie together applications, just as the Web Browser and URL links have tied together Web pages. "The Web At Your Service" is the new mantra. In this article, Rick discusses SOAP and Web Services, then creates a sample Web Service and integrates it into an application.

The move to distributed application development is a natural evolution for the Web. It gets back to the core of how data is used in applications in general. With HTML, the focus has always been on presentation, with data bound directly into the display. On the other hand, the focus in distributed applications is on separating the display from the data delivery. XML has now become the preferred way to provide 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, then used as the transfer format. The client then has the choice of consuming the data through the XMLDOM or converting it back into a native format. In the latter scenario, XML is primarily used as a persistence format to transfer state from server to client or vice versa. To do this, you simply need 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, making remote calls on object methods or functions (such as script page calls) more natural. In custom XML applications, both the client and server sides of the application have to know the message format, resulting in coupling between the client and server. By providing a standard mechanism for representing the procedure call interface, and a mechanism for querying the available functionality and the signature of each call, SOAP can abstract away the explicit XML conversions that are necessary in custom XML implementations. To make this process seamless, services or tools are needed to provide the SOAP XML packaging and unpackaging, and to perform the wire transfer operations. The current flock of tools is not quite there yet, although it currently takes only a few lines of code to make this kind of remote procedure call.

SOAP is a contender in the field of 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. Because SOAP uses HTTP, it can take advantage of HTTP encryption and authentication, and can go through the Enterprise firewall in most cases.

It's important to understand, though, that SOAP is not intended to replace DCOM or CORBA in high-performance environments. SOAP has considerable overhead, 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 can cause SOAP to be as much as 1000 times slower than a similar DCOM implementation. (This depends on the type of SOAP server, of course. If you build an ISAPI listener in C++, you probably will get better numbers.)

Remember that you should not think of SOAP as a Remote COM implementation. Although the Microsoft SOAP Toolkit (see sidebar) focuses on exposing COM objects as Web Services, that is just one way to create a Web Service. SOAP is open and does not specify how a Web Service must be implemented, so you can provide a SOAP Web Service with a script code function in ASP, a Visual FoxPro class, a JSP class, or a COM object run through the SDL Wizard.

What makes SOAP so nice is that it's a relatively simple protocol that's easy to implement and use. Building a custom SOAP server 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, passing data in XML format over the wire. This mechanism works well for custom applications 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 them by providing a standard interface for calling the server-side Web Service. This interface is the Simple Object Access Protocol, or SOAP. SOAP's mission in life is to provide a standard, XML-based interface to remote procedure calls. This is accomplished with standard parameter information (input) and return values (output), packaged in XML documents that follow the SOAP protocol specification. A server SOAP implementation is required to handle incoming SOAP requests, and can be provided by any Web backend. MS SOAP uses ASP, while Web Connection uses a special process class to handle SOAP requests for a specific Web scriptmap. Keep in mind that the details of the server implementation are independent of the SOAP specification. As long as the server can read the incoming SOAP Request packet and return a valid SOAP Response packet, it fulfills the SOAP contract.

In simplistic terms, the client passes a parameter and 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. This will blur the lines about where code is executed, since it can just as easily be in you own compiled application or in a service sitting on the other side of the globe.

Why bother, since XML is already here?

If you're already building XML applications, you may be asking yourself what is so different about using SOAP, compared to 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:

  • Simplicity - The idea behind SOAP is that tools exist to facilitate the process of creating SOAP messages easily. I'll show examples later in this article, but the basic idea is that client code should resemble the simplicity of making a simple function call ? on another server. On the server side it means you can implement a Web Service by simply implementing a function or class method. The SOAP Web Service server manager and the SOAP client handle everything that happens in between ? packaging and unpackaging the XML and transferring it via HTTP. In many cases, this removes the need to create XML manually. On the client, it means writing only a few lines of code to access that remote Web Service, and not having to know how to package up XML and make an HTTP request.
  • Standardization - If you publish functionality via SOAP, you open up your architecture to other clients that can call your code easily, using any SOAP clients. SOAP clients exist today for just about all major operating system platforms and development languages, including Java, Perl, custom implementations in various languages, and (for Windows developers) COM objects. Furthermore, you can publish an interface definition to describe the service and its functionality, making possible the same type of IntelliSense functionality that is common for Windows development tools.

SOAP is still evolving as we speak, but tools are available that let you take advantage of it today. The most visible tool is Microsoft's SOAP Toolkit. For more info on the Microsoft implemententation see my article Using SOAP for remote object access with Microsoft's SOAP Toolkit (http://www.west-wind.com/presentations/soap/). In this article, I'll discuss building a Web Service with West Wind Web Connection's Web Service implementation, which is easier and more flexible in a number of ways.

West Wind Web Connection and Web Services

West Wind Web Connection includes direct support for SOAP Web Services, with both a wwSOAP client implementation and a wwWebService process class to handle incoming SOAP requests mapped to a ".wwSOAP" scriptmap extension in IIS. To demonstrate how all of this works I'll implement a stock lookup service as a SOAP Web Service. All of these samples are available as part of the free wwSOAP Visual FoxPro 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. I want you to understand up front that there are other ways to do this, because some of this data for quotes is available over the Web. However, this is meant as an example of a variety of ways to consume data from Web Services.

This 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 quantity, 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 information. The stock data is retrieved from a SOAP Web Service that I'll describe in detail. 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 by retrieving a single stock price to demonstrate the basics of how Web Services work. Here's the code to retrieve a stock quote from the MSNBC Web site, using the wwHTTP class (included as part of wwSOAP):

*********************************************************** GetStockQuoteSimple
****************************************
***  Function: Returns a stock quote by symbol
***      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 you consider that it's off from Microsoft's 120 high earlier this year, huh?

Easy enough. So, now let's 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 a new file to create the Web Service class. 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
*** DO NOT REMOVE - CALL WRAPPER

***********************************************
DEFINE CLASS StockService AS RELATION OLEPUBLIC
***********************************************

*** Remove after testing
FUNCTION Test(lcEcho)
IF EMPTY(lcEcho)
   RETURN "Test Result"
ENDIF   
RETURN lcEcho

ENDDEFINE

The Web Service contains a small loader that's called by the Web Connection Web Service engine and, 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 used only to create an SDL file for consumption by MS SOAP, as well as to provide the ability to compile your object into a COM object that can be called from an MS SOAP Web Service. More on that later.

The Wizard also generates a test method into the class so you can check out the Web Service easily. Now, 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 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))

ENDDEFINE

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

Before you go on, make sure the Web Connection Server is running (DO wcDemoMain). Note that Web Services in Web Connection are dynamically compiled under both 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 website, I'll use that as an example. If you run this sample locally you will have to start up another instance of Visual FoxPro and run the following program in it. 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 of the Web Service. In this case, I used the service running on the West Wind website. If you're testing on your local machine, use localhost and the virtual directory where you copied the Web Service. Enter the method name and each of the parameters required with their values and types, in this case the stock symbol and "string".

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 packet that consists of the following sections:

The SOAP EnvelopeThe envelope contains the SOAP header that defines the SOAP and related namespaces, such as xsi and xsd (used for data types). The envelope can also contain an optional <headers> section that lets you provide custom headers to a request. Custom SOAP server implementations can read info from the header and use it internally.

The SOAP BodyThis is the meat of the SOAP packet, and contains the method call interface. In a request packet, this has the name of the method to call on the server and all of the parameters passed. The parameters are each described as sub-elements of the method, and are typed with XML types in our wwSOAP implementation.

The SOAP response packet 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, except that the body section contains the return value.

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 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 to do these steps independently. Properties like cRequestXML and cResponseXML let you view what goes over the wire. The wwSOAP documentation help file includes examples of how to do this.

Notice that when you run this, the Web Service returns a character value. That's not really a big problem, since you can just run VAL() on the returned quote to make it numeric, but it would be much nicer if the server actually did this for us. Since wwSOAP and most SOAP applications can 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)) )

and also change the function header of the Web Service method to:

FUNCTION GetStockQuoteSimple(lcSymbol as String) as Float

Save, then re-run the SOAP test code above and now the return value is numeric! 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 on the next call.

Also, note what happens if you play with the parameter types in the Method Tester form. 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 was passed to the server application. That code failed in the GetStockQuoteSimple() method call because we didn't check for a non-string input parameter.

Expanding the Stock Web Service with Objects

Returning a single value like a stock price is nice, but you might want to retrieve more information, such as the high and low, the change for the day, the actual time of the quote, and a few other things. You could set up a Web Service method for each of these and make several SOAP calls, but this would be vastly inefficient, requiring multiple roundtrips to the server. SOAP calls have a fair amount of overhead in the packaging and unpackaging of parameters and return values, and in passing that data over the Web and through the Web Server. Additionally, information like stock quotes is time critical. Going back to the server several times for parts of the data would potentially produce mismatched results. Bundling up data into a single SOAP package is a big advantage.

For this reason, SOAP supports embedding 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. But, there's a catch: The client side must provide an object instance to receive the SOAP result object.

To demonstrate, lets add a new method to our Web Service called GetStockQuote, which will return an object containing several stock quote properties from the NASDAQ website. The NASDAQ site provides quotes in XML format and this method retrieves values from the 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 stock quote from NASDAQ, parses several of the XML properties into object properties for easier access, and returns the newly created object over SOAP.

If you 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 is an XML fragment 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 just displays this XML fragment, but by using code, you can actually retrieve the object.

Here's the client code to do that:

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

*** Pass this object as third parm to CallMethod
lcPrice = oSOAP.CallMethod("GetStockQuote",,loQuote)

*** Always check for errors
IF oSOAP.lError
  MESSAGEBOX(oSOAP.cErrorMsg)
  RETURN
ENDIF

*** Echo back some of the data
? "Last: ",loQuote.nLast
? loQuote.cCompany
? "Net Change: ",loQuote.nNetChange
? "Updated on: ",loQuote.tUpdated
? VARTYPE(loQuote)  

DEFINE CLASS cStockQuote as Relation
  cSymbol=""
  cCompany=""
  nLast=0.00
  nOpen=0.00
  nHigh=0.00
  nLow=""
  nNetChange=0.00
  nPERatio=0.00
  tUpdated={ : }
ENDDEFINE

That's pretty cool, isn't it? Now, you can have the server return objects that you can serialize over the wire with SOAP. Then, you can rebuild these objects on the client side by deserializing them into existing objects. The way this works is that each XML element from the server object is serialized into a client object property, if it exists. So, you can match each of the elements in the response simply by adding a property to the object above. If the service is expanded with other properties, it'll still work with or without the matching client properties in place.

Building a Web Page to use the Web Service

Ok, let's put the Web Service to use in a Web Application. What we want to do is retrieve several stock quotes and show them in the browser on a portfolio manager form (Figure 3).

Figure 3 - This SOAP Stock Portfolio example retrieves stock quotes via SOAP and demonstrates server-to-server and browser-to-server SOAP communication.

As you can see, a portfolio consists of several stocks stored in a portfolio table with a customer id (for demo purposes, a Session id attached to a cookie). The application backend needs to retrieve a live quote for each of the stocks in the portfolio. As I mentioned previously, making multiple SOAP calls to retrieve each quote individually is not a great idea from a performance point of view. So, rather than making one SOAP call per stock, I added another method to my Web Service to receive an XML input with a list of stocks. It will return a single XML document with one object for each stock. I'll use the wwXML class (included with the wwSOAP download) to package up those objects.

Add the following method to the Web Service:

************************************************************************
* StockService :: GetStockQuotes
****************************************
***  Function: Returns a set of Stock Quotes based on an XML input
***            string <quotes><symbol>IBM</symbol><symbol>MSFT</symbol></quote>
***    Assume: Pulls data from msn.moneycentral.com
***      Pass: lcSymbol  -  MSFT, IBM etc.
***    Return: Object
************************************************************************
FUNCTION GetStockQuotes(lcXMLSymbols as String) as String
LOCAL oDOM, lcXML, x, loQuotes, loQuote

IF EMPTY(lcXMLSymbols)
   RETURN ""
ENDIF

oDOM = CREATEOBJECT("Microsoft.XMLDOM")
oDOM.LoadXML(lcXMLSymbols)
IF !EMPTY(oDOM.ParseError.Reason)
   RETURN ""
ENDIF

loQuotes = oDOM.selectnodes("/quotes/symbol")
IF ISNULL(loQuotes)
  RETURN "" 
ENDIF

loXML = CREATEOBJECT("wwXML")
lcXML = ""


FOR x = 0 to loQuotes.Length -1
   lcSymbol = loQuotes.item(x).Text
   
   loQuote = THIS.GetStockQuote(lcSymbol)
   IF ISNULL(loQuote)
      LOOP
   ENDIF

   lcQuoteXML = loXML.CreateObjectXML( loQuote,"quote" )    
   lcXML = lcxML + STRTRAN(lcQuoteXML,"<quote>",[<quote symbol="]+lcSymbol+[">])
ENDFOR

lcXML = [<?xml version="1.0"?>] + CHR(13) + CHR(10) + ;
                [<stockquotes>] + ;
                lcXML + ;
                [</stockquotes>] 

RETURN lcXML

The method receives an input XML string in the following format:

<quotes>
   <symbol>MSFT</symbol>
   <symbol>IBM</symbol>
</quotes>

The symbols are retrieved using the XMLDOM parser, which runs through all the symbol nodes in the XML document, using a for loop.

For each symbol, a call to GetStockQuote() is made, which goes out to the NASDAQ site, retrieves a quote, and returns it as an object. This object is then turned into an XML fragment with wwXML::CreateObjectXML(), and is combined with the other symbol retrievals to create a larger XML document.

You can review the full source code of about 100 lines and the HTML script page used to display the page by clicking on the Show Code links on the bottom of the sample Web page. The relevant Web Service code is in the following block:

*** Now Refresh the profile
IF llRefresh
   *** Select all items for this user
   SELECT * from Portfolio ;
      where UserId = lcUserId ;
      INTO Cursor TQuery
        
   oSOAP = CREATEOBJECT("wwSOAP")

   *** Create stock quote request in XML format
   *** This will be our SOAP parameter
   lcXML = ;
   [<?xml version="1.0"?>] + CRLF +;
   [<quotes>] + CRLF 

   SCAN
      lcxml = lcxml + ;
       [<symbol>] + symbol + [</symbol>] + CRLF 
   ENDSCAN
   lcXML = lcXML + [</quotes>] +CRLF

   *** SOAP CALL RIGHT HERE ***
   *** Make the SOAP call and retrieve XML result
   oSOAP.cServerUrl = "http://www.west-wind.com/wconnect/soap/stockservice.wwsoap"
   oSOAP.AddParameter("lcXMLSymbols",lcXML)
   lvResult = oSOAP.CallMethod("GetStockQuotes")
   IF oSOAP.lError
      THIS.errormsg("SOAP Error",oSOAP.cErrorMsg)
      RETURN 
   ENDIF   

   *** Parse the XML result 
   loQuote = CREATEOBJECT("Quote")
   oDOM = CREATEOBJECT("Microsoft.XMLDOM")
   oDOM.LoadXML(lvResult)
   IF !EMPTY(oDOM.parseerror.reason)
      THIS.errormsg("XML Result Error",oDOM.parseerror.reason) 
      RETURN
   ENDIF

   *** Loop through all of the quotes and update
   *** the user's portfolio
   LOCATE 
   SCAN
      *** Load each symbol into an object using wwXML
      lcSymbol = TRIM(Symbol)
      loSymbol = oDOM.selectSingleNode("/stockquotes/"+ ;
                      "quote[@symbol='" + lcSymbol+ "']")

      IF ISNULL(loSymbol)
         LOOP
      ENDIF
      loXML = CREATEOBJECT("wwXML")
      loQuote = loXML.ParseXMLToObject(loSymbol,loQuote)
      IF !ISNULL(loQuote)
         SELECT Portfolio
         LOCATE FOR UserId = lcUserId AND SYMBOL = lcSymbol
         REPLACE Descript with loQuote.cCompany,;
                 updated with loQuote.tUpdated,;
                 price with loQuote.nLast
      ENDIF   
   ENDSCAN

   pcMessage = "Portfolio refreshed at: " + TIME() + " PST"
ENDIF
*** End of refreshing profile

The code starts by building an XML string containing all the symbols to retrieve from the portfolio. That string is then used in the SOAP call as a parameter to the GetStockQuotes() method call. Notice that the actual SOAP call in this example is just 3 lines of code, plus error checking. When the call returns, we should have an XML string representing a set of quote objects matching the symbols in the portfolio.

The result string looks something like this:

<?xml version="1.0"?>
<stockquotes>
   <quote symbol="INTC">
	<ccompany>Intel Corporation</ccompany>
	<csymbol>INTC</csymbol>
	<nhigh>44.38</nhigh>
	<nlast>44.13</nlast>
	<nlow>42.69</nlow>
	<nnetchange>1.06</nnetchange>
	<nopen>43.06</nopen>
	<nperatio>29.42</nperatio>
	<tupdated>2000-10-23T10:20:59</tupdated>
   </quote>
   <quote symbol="MSFT">
	<ccompany>Microsoft Corporation</ccompany>
	<csymbol>MSFT</csymbol>
	<nhigh>66.25</nhigh>
	<nlast>64.13</nlast>
	<nlow>64</nlow>
	<nnetchange>-1.06</nnetchange>
	<nopen>65.19</nopen>
	<nperatio>37.50</nperatio>
	<tupdated>2000-10-23T10:21:00</tupdated>
   </quote>
</stockquotes>

Once this result is retrieved, the code loops through the user's portfolio again and retrieves a node reference to each of the symbols by using an XSL pattern query:

loSymbol = oDOM.selectSingleNode(;
           "/stockquotes/quote[@symbol='" + lcSymbol+ "']")

From here, the node can be imported into an object via wwXML and its low-level ParseXMLToObject() method, which takes an XML node as input and parses the properties below it into the object provided:

loQuote = loXML.ParseXMLToObject(loSymbol,loQuote)

At this point, the object is filled with the appropriate data, and the portfolio record in the table can be updated with a standard VFP REPLACE command.

SOAP Browser Clients

So far, you may find that this SOAP example doesn't show much improvement over what you can accomplish with normal XML messaging. The main reason is that some of the tools, like wwXML's data conversion methods, can make short work of generating and importing XML. However, if you're not already using such tools, SOAP's advantages become much more obvious. To demonstrate, I'll use SOAP to retrieve a single stock quote from script in an HTML Web page.

In this scenario, only client-side Jscript code (Internet Explorer 5.0 and later) is used to make the SOAP method call and display the data in the browser without refreshing the entire page. If you look at Figure 3 again, you can see the Get Single Quote textbox and the Go button. The Go button points to the following Jscript in the Web page:

<SCRIPT SRC="wwsoap.js"></script>
<script>

SERVER_URL =  "http://www.west-wind.com/wconnect/soap/stockservice.wwsoap";
oDOM = null;
cPk = 0

function GetQuote() {
   
   gcParameters = ""
   addParameter("lcSymbol",document.forms[0].symbol.value,"string")

   /// make the call - XML string result (object)
   lvResult =  CallMethod("GetStockQuote",SERVER_URL);   

   if (lvResult.length == 0) {
      alert("Invalid SOAP Response")
      return 
   }
      
   oDOM = new ActiveXObject("Microsoft.XMLDOM");
   oDOM.loadXML(lvResult);
   
   if (oDOM.parseError.reason != "") {
      alert( oDOMparseError.reason);
      return;
   }
  
   lcHTML = "<hr><b>" + oDOM.selectSingleNode("/return/ccompany").text + 
            "</b><br><b>Last:</b> " + oDOM.selectSingleNode("/return/nlast").text + 
            "<br><b>Previous Close:</b> " + oDOM.selectSingleNode("/return/nopen").text + 
            "<br><b>Change:</b> " + oDOM.selectSingleNode("/return/nnetchange").text ;            
  quoteresult.innerHTML = lcHTML;
}
</script>

When you click on the Go button, a SOAP call is initiated from the browser. The actual library functions for this are contained in wwsoap.js. (You can look at that from the sample page as well, by using the link at the bottom.) The key features are the AddParameter and CallMethod functions, which work like their counterparts in wwSOAP. The difference is that Jscript doesn't support objects, so these functions are not set up as methods. This Jscript SOAP implementation is very basic and rather crude, but it will work well against the Web Connection Web Service and MS SOAP. wwSOAP.js uses the XMLHTTP component to communicate with the Web Server and retrieve the data.

Since Jscript doesn't support objects and GetStockQuote() returns an object, the Jscript wwsoap implementation instead returns an XML string. This string is loaded into the XMLDOM, the individual values are retrieved and built up into an HTML string, then the HTML string is replaced into the HTML document. The entire section below the Quote text box is dynamically generated using a <span> tag whose .innerHTML property receives the HTML contents of the string.

Creating SDL files and MS SOAP compatibility

The examples I've shown were all built for West Wind Web Connection, but they can easily be applied to other development tools. You can use the StockService Web Service with MS SOAP by simply compiling the Web Service into a COM object, then running the MS SOAP SDL Wizard to generate the SDL and ASP files. (Make sure you add all dependent files to the project, because they won't automatically get pulled in.) You can look at the MS SOAP article mentioned earlier for details.

Web Connection, however, can also create an SDL file from your Web Service. This is a requirement for a Web Service to work with the MS SOAP client. Web Connection contains a Wizard to generate the SDL file from an existing Web Service class (Figure 4).

Figure 4 - The SDL Creation Wizard takes a Web Service class and generates a Service Description XML file from it.

An SDL file describes the functionality of a Web Service and acts as a sort of type library. The MS SOAP Toolkit requires use of an SDL file, both on the client and server sides of a Web Service implementation, so a Web Service that is to be accessed through MS SOAP must expose an SDL file.

The Web Connection SDL Wizard generates an SDL file from any VFP program file containing an OLEPUBLIC class with the same name as the file. So, if I have a StockService.wwSOAP program file with a StockService class in it, an SDL file called StockService.xml is generated in the same directory as the Web Service. Note that .wwSOAP files are really just PRG files with a different extension, so this Wizard will work on any PRG-based class.

This will generate an SDL description that's compatible with the MS SOAP toolkit. Unfortunately, SDL specifications are not really specifications yet, and Microsoft currently has 3 different versions: The MS SOAP toolkit; the .Net implementation; and the new forthcoming WSDL specification, which supposedly will be supported by both .Net and MS SOAP at some future point. The current Wizard generates SDL compatible with MS SOAP, because that is the only shipping SDL format that you're likely to find at the moment.

The generated SDL file for our Stock Service looks as follows:

<?xml version='1.0' ?>
<!-- Generated by West Wind SOAP Helper 10/23/2000 07:05:34 PM -->
<serviceDescription name='stockservice' 
    xmlns='urn:schemas-xmlsoap-org:sdl.2000-01-25' 
    xmlns:dt='http://www.w3.org/1999/XMLSchema' 
    xmlns:stockservice='stockservice' 
>
<import namespace='stockservice' location='#stockservice'/>

    <soap xmlns='urn:schemas-xmlsoap-org:soap-sdl-2000-01-25'>
        <interface name='stockservice'>
         <requestResponse name="GetStockQuoteSimple">
            <request ref="stockservice:GetStockQuoteSimple"/>
            <response ref="stockservice:GetStockQuoteSimpleResponse"/>
            <parameterorder>lcSymbol</parameterorder>
         </requestResponse>
         <requestResponse name="GetStockQuotes">
            <request ref="stockservice:GetStockQuotes"/>
            <response ref="stockservice:GetStockQuotesResponse"/>
            <parameterorder>lcXMLSymbols</parameterorder>
         </requestResponse>
         <requestResponse name="GetStockQuote">
            <request ref="stockservice:GetStockQuote"/>
            <response ref="stockservice:GetStockQuoteResponse"/>
            <parameterorder>lcSymbol</parameterorder>
         </requestResponse>

        </interface>
        <service>
            <addresses>
                <address uri='http://localhost/wconnect/soap/stockservice.wwsoap'/>
            </addresses>
            <implements name='stockservice'/>
        </service>
    </soap>

    <stockservice:schema id='stockservice' targetNamespace='stockservice' 
                         xmlns='http://www.w3.org/1999/XMLSchema'>;
      <element name="GetStockQuoteSimple">
         <type>
            <element name="lcSymbol" type="dt:string"/>
         </type>
      </element>
      <element name="GetStockQuoteSimpleResponse">
         <type>
            <element name="return" type="dt:integer"/>
         </type>
      </element>
      <element name="GetStockQuotes">
         <type>
            <element name="lcXMLSymbols" type="dt:string"/>
         </type>
      </element>
      <element name="GetStockQuotesResponse">
         <type>
            <element name="return" type="dt:string"/>
         </type>
      </element>
      <element name="GetStockQuote">
         <type>
            <element name="lcSymbol" type="dt:string"/>
         </type>
      </element>
      <element name="GetStockQuoteResponse">
         <type>
            <element name="return" type="dt:object"/>
         </type>
      </element>

    </stockservice:schema>

</serviceDescription>

There are a couple of important elements in an SDL file. The top section describes the interface of the Web Service, providing the names of the methods and the parameters expected, as well as pointers to the implementation section of the schema. The implementation follows, and shows in detail the types of the parameters and return values.

Note that the GetStockQuote method of the Web Service returns an object parameter that MS SOAP does not understand, so it will fail on this call. You can work around this by changing the type manually to dt:string, but realize that you'd have to parse the XML yourself.

The other important thing in the SDL file is the <address> fragment which specifies the location of the Web Service request handler. It's important that you remember to either change this value or re-run the Wizard when you move your Web Service between a development machine and the live Web Server, because the URLs will change. Here, I generated it to localhost for testing, but when I put it online I'll want to call the service on www.west-wind.com.

We can test this quickly with the MS SOAP Toolkit, using the following code:

oWire=CREATEOBJECT("Rope.WireTransfer")
lcXML = oWire.GetPageByURI("http://west-wind.com/soap/stockservice.xml")

*** Work around VFP COM case bug
lcXML = STRTRAN(lcXML,"GetStockQuoteSimple","getstockquotesimple")

oProxy = CREATEOBJECT("Rope.Proxy")
? oProxy.LoadServicesDescription(2, lcXML) && 1 

lvResult = oProxy.GetStockQuoteSimple("MSFT")

? "Result Type: ",VARTYPE(lvResult)
? lvResult

oWire = .F.
oProxy = .F.

Although it will correctly return the stock quote, MS SOAP will always return a string regardless of the type specified in the SDL file.

The big SOAP down

We've used SOAP on a couple of projects now and have found that building Web Services is a huge timesaver when building server functionality. We're also finding that although SOAP greatly simplifies distributed applications in many cases, we still end up using XML extensively as parameters for method calls to avoid continuous server roundtrips. Most sample Web Services shown today return unrealistic types of information (mostly single values), and do not reflect real-life applications. For SOAP to be really useful, objects or other compound data must be passed. But, this is difficult to implement, because the client application must deserialize the object into an existing structure, requiring some coupling. Object support varies greatly among the various SOAP implementations, and some (notably the MS SOAP Toolkit) don't support objects at all, so passing data in XML format continues to be a common requirement even with SOAP.

There is also a mental hurdle to overcome with SOAP for those of us who have used XML in distributed applications. Raw XML is simple and elegant and, with the right tools, takes very little code to build XML solutions without SOAP. In fact, when I first put out my SOAP implementation, I asked for demo ideas that would highlight a SOAP interface as opposed to a custom XML implementation. Lots of suggestions came up, but the reality is that none of them screamed out and said, "SOAP is the clear choice." The examples that were perfect for SOAP mostly were simplistic examples that passed in single values and returned single results. Most real-world applications don't work that way! This means that you need objects (with varying support) or you're back to using XML (with manual parsing). I can live with that, since we use XML extensively in every application and have tools like wwXML to make the use of XML nearly transparent.

The big benefit of SOAP is a common, universal interface. SOAP clients are already available for almost every platform and most let you make the SOAP call with just a couple of lines of code. .Net takes this one step further and directly integrates remote calls, which (based on compiler settings) can tell whether an object is local or remote and can transparently call the remote object. As nice as that sounds, right now .Net is the only technology that can talk to .Net, even though it uses SOAP underneath the covers, because the implementation is proprietary and relies on a custom SDL file format. This will likely change by the time .Net ships, but right now it's just another frustrating piece in the puzzle.

The downside of SOAP, compared to a plain XML solution, is that SOAP has more overhead. For many applications that pass complex data around, double XML parsing occurs. First, you must create your XML messages (objects packaged as parameters or return values) or XML string parameters. Then, there's the creation of the SOAP package and content. Request times over the Internet are pretty good, with most examples taking under 1 second on a slow dial-up connection. This will be fine for some distributed applications, but it's not a wise replacement for a high performance DCOM application, which on the same network would run around 1,000 times faster on the same method call.

Still, getting familiar with SOAP and Web Services now is a good idea, because they're here to stay. Exposing your application logic with Web Services today will assure that those services can be consumed by future applications built around SOAP. In the future, SOAP tools will become easier. Languages will natively support SOAP, so that objects can be accessed locally or globally with the same syntax. Today, we have to do a little more work, but we also get the benefit of full control and can actually see what's happening under the hood.

SOAP underlies much of Microsoft's new technology, especially .Net, so I encourage you to learn more about it. The tools are here today, so you can use and integrate this technology now. Let's get to it!