[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ] [ 9 ]

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 6 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 Envelope
    The envelop contains the SOAP header that define the SOAP and related namespaces such as xsi and xsd used for data types. The envelop can also contain an optional <headers> section that lets you provide custom headers to a request. Custom SOAP server implantations can read info from the header and use it internally.

  • The SOAP Body
    This is the meat of the SOAP packet. It contains the method call interface. In the request this consists of 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 with wwSOAP are typed with XML types.

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 alsoincludes 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)
     
    IF oSOAP.lError
      MESSAGEBOX(oSOAP.cErrorMsg)
      RETURN
    ENDIF
     
    ? "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 simply have the server return objects that you can serialize over the wire with SOAP and rebuild these objects on the client side by deserializing them into existing objects.

[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ] [ 9 ]