Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Object parsing for Web Services called by Visual FoxPro


:P
On this page:

I’ve been struggling with the concepts of parsing FoxPro objects into XML for use in Web Services as part of wwSOAP for quite a while. My wwXML utility makes it relatively painless to generate XML from an object, but this XML is limited to lower case characters for entity names because VFP does not support proper case objects.

 

To generate this XML is pretty simple with:

 

oInv = CREATEOBJECT("cInvoice")

? oInv.Load(        3144)

lcXML = oXML.ObjectToXML(oInv,"InvoiceDocument")

 

The XML generated from a complex object might look like this:

 

<?xml version="1.0"?>

<xdoc>

      <InvoiceDocument type="object" class="cinvoice">

            <cshipcountry>US</cshipcountry>

            <cshipstate></cshipstate>

            <ocustomer type="object" class="ccustomer">

                  <odata type="object" class="empty">

                        <address>32 Kaiea Place</address>

                        <city>Paia</city>

                        <company>West Wind Technologies</company>

                        <country>United States</country>

                        <countryid>US</countryid>

                        <custtype>0</custtype>

                        <zip4></zip4>

                  </odata>

            </ocustomer>

            <odata type="object" class="empty">

                  <cc>4111 1231 1231 1231</cc>

                  <ccerror></ccerror>

                  <ccexp>1/2004</ccexp>

                  <ccresult>FAILED</ccresult>

                  <ccresultx></ccresultx>

                  <cctype>VI</cctype>

                  <completed>2004-09-30</completed>

                  <custpk>156256</custpk>

                  <taxrate>0</taxrate>

                  <weight>0</weight>

            </odata>

            <olineitems type="object" class="clineitems">

                  <arows>

                        <arows_item type="object" class="empty">

                              <custpk>0</custpk>

                              <descript>West Wind Html Help Builder 4.0</descript>

                              <discount>0</discount>

                              <extratext></extratext>

                              <invpk>3144</invpk>

                              <weight>0</weight>

                              <xml></xml>

                        </arows_item>

                        <arows_item type="object" class="empty">

                              <custpk>0</custpk>

                              <descript>West Wind Web Connection Training Montreal</descript>

                              <discount>0</discount>

                              <extratext></extratext>

                              <invpk>3144</invpk>

                              <itempk>0</itempk>

                              <weight>0</weight>

                        </arows_item>

                  </arows>

            </olineitems>

            <oorigdata>NULL</oorigdata>

            <osql>NULL</osql>

            <vresult></vresult>

      </InvoiceDocument>

</xdoc>

 

It works but as you can see all the property names  lower case, which is a problem if you need to send this object to a server that uses proper names. Which is likely if you’re calling a .NET or Java Web Service.

 

A number of people last week at DevCon were asking about how to properly convert objects and my answer has been to just bite the bullet and hand create the XML. You can do this fairly easily especially if you use wwXML’s AddElement method to do the proper type conversions for you. But this is still a major hassle.

 

Sooooo… after some more thought about this I decided that there is another way to do this: You can use the WSDL type definition to parse an object and instead of using the property names from VFP to generate the element names we can use the property names defined in the WSDL file. This is not too terribly complicated, but it does get a bit tricky for array and collection handling. Luckily when calling .NET services at least collections must be typed properly and persist very much the same way as arrays so the structural layout for both of these is similar.

 

By doing this you are limited to the structure that is in the WSDL file so you have to create an object that maps property names to what’s available in the Web Service. In the end I created a parser in the free wwSOAP class that looks something like this:

 

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

* wwSOAP :: CreateObjectXmlFromSchema

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

***  Function: Creates XML from an object using the schema to

***            create the nodes.

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

FUNCTION CreateObjectXmlFromSchema(loObject,lcTypeName,;

                                   lcPropertyName,lnIndent)

LOCAL lcXML, lnCount, x, lcFieldName, lcFoxType

 

IF EMPTY(lcPropertyName)

   lcPropertyName = lcTypeName

ENDIF

IF EMPTY(lnIndent)

   lnIndent = 0

ENDIF

 

*** Retrieve the properties from the type in the WSDL

LOCAL ARRAY laTypeProperties[1]

lnCount = this.aGetTypeProperties(@laTypeProperties,lcTypeName)

 

LOCAL loXML as wwXML

loXML = CREATEOBJECT("wwXML")

 

lcXML = REPLICATE(CHR(9),lnIndent) + "<" + lcPropertyName + ">" + CRLF

FOR x = 1 TO lnCount

   lcFieldName = laTypeProperties[x,1]  

 

   lcFoxType = TYPE("loObject." + lcFieldName)

   IF lcFoxType = "U"

      LOOP

   ENDIF

 

   DO CASE

  

   *** Array Checking first

   #IF wwVFPVERSION > 8

   CASE  TYPE([ALEN(loObject.] + lcFieldName + [)]) = "N"

   #ELSE

   CASE TYPE("loObject." + lcFieldName,1) = "A"

   #ENDIF

      LOCAL y, lnSize, lvValue, lcFieldType, lcChildType

     

      *** Must copy the array

      DIMENSION la_array[1]

      ACOPY(loObject.&lcFieldname,la_array)

 

      lcChildType = THIS.RetrieveChildClassFromWSDLObject(laTypeProperties[x,2])

     

      IF EMPTY(lcChildType)

         lcXML = lcXML + REPLICATE(CHR(9) ,lnIndent+1) + "<" + lcFieldName + "/>" + CRLF

      ELSE

         lnSize = ALEN(la_array,1)

         lcXML = lcXML + REPLICATE(CHR(9) ,lnIndent+1) + "<" + lcFieldName + ">" + CRLF

         FOR y = 1 TO lnSize

            lcXML = lcXML +  this.CreateObjectXmlFromSchema( ;

                                la_Array[y],lcChildType,lcChildType,lnIndent+3 )        

         ENDFOR

         lcXML = lcXML +  REPLICATE(CHR(9) ,lnIndent+1) + "<" + lcFieldName + ">" + CRLF

      ENDIF

 

   *** Check for Collections

   CASE TYPE("loObject." + lcFieldName + ".KeySort") = "N"     

         lcChildType = THIS.RetrieveChildClassFromWSDLObject(laTypeProperties[x,2])

     

         loCol = EVALUATE("loObject." + lcFieldName)

         lnSize = loCol.Count

        

         lcXML = lcXML + REPLICATE(CHR(9) ,lnIndent+1) + "<" + lcFieldName + ">" + CRLF

         FOR y = 1 TO lnSize

            lcXML = lcXML +  this.CreateObjectXmlFromSchema( ;

                                 loCol.Item[y],lcChildType,lcChildType,lnIndent+3 )        

         ENDFOR

         lcXML = lcXML +  REPLICATE(CHR(9) ,lnIndent+1) + "<" + lcFieldName + ">" + CRLF

  

   *** Objects: Recurse into the object

   CASE lcFoxType = "O"

      lcXML = lcXML + this.CreateObjectXmlFromSchema( EVALUATE("loObject."+lcFieldName),;

                       this.StripNameSpace(laTypeProperties[x,2]),lcFieldName,lnIndent+1 )

   OTHERWISE

      lcXML = lcXML + loXML.Addelement(lcFieldName,EVALUATE("loObject." + lcFieldName),lnIndent+1,"",lcFoxType)

   ENDCASE

ENDFOR

lcXML = lcXML + REPLICATE(CHR(9),lnIndent) + "</" + lcPropertyName + ">"  + CRLF

 

RETURN lcXML

ENDFUNC

*  wwSOAP :: CreateObjectXmlFromSchema

 

This code uses wwXML and AddElement quite extensively but it turned out to be a relatively short piece of code. If it wasn’t for the handling of arrays this code would have been pretty clean and short. The difficult part here is that we have to make sure that we can figure out the name of the child elements in arrays.

 

This code assumes that collections or arrays contain objects, not individual values. Further this code doesn’t deal well with recursive references. If the object in the tree has child objects that point back at another object higher up it can easily get hung up.

 

However, you can now create objects on the fly that match fairly complex objects. For example, here the sample I was doing at DevCon (minus the automatic XML creation):

 

loAuthor = GetAuthorProxyObject()

loAuthor.au_lname = "Strahl"

loAuthor.au_fname = "Rick"

loAuthor.Phone = "808 123-1211"

loAuthor.Address = "32 Kaiea Place"

loAuthor.City = "Paia"

loAuthor.State = "HI"

loAUthor.Zip = "96779"

loAuthor.Contract = 0

 

loAuthor.Invoices[1] = loInv

loAuthor.Invoices[2] = loInv2

 

 

FUNCTION GetAuthorProxyObject()

 

*** Manunally parse the result back into an object

loAuthor = CREATEOBJECT("Relation")

loAuthor.AddProperty("Au_id","")

loAuthor.AddProperty("Au_lname","")

loAuthor.AddProperty("Au_fname","")

loAuthor.AddProperty("Phone","")

loAuthor.AddProperty("Address","")

loAuthor.AddProperty("City","")

loAuthor.AddProperty("State","")

loAuthor.AddProperty("Zip","")

loAuthor.AddProperty("Contract",.f.)

 

loAuthor.AddProperty("PhoneNumbers",CREATEOBJECT("RELATION"))

loAuthor.PhoneNumbers.AddProperty("HomePhone","")

loAuthor.PhoneNumbers.AddProperty("WorkPhone","")

loAuthor.PhoneNumbers.AddProperty("Fax","")

loAuthor.PhoneNumbers.AddProperty("ServiceStarted",DATETIME())

 

 

*** Code to add an array for invoices

loAuthor.AddProperty("Invoices")

*DIMENSION loAuthor.Invoices[2]

 

 

*LOCAL loInvoices as Collection

loAuthor.Invoices = CREATEOBJECT("Collection")

 

loInv = CREATEOBJECT("Empty")

ADDPROPERTY(loInv,"InvoiceNumber","9")

ADDPROPERTY(loInv,"InvoiceDate",{05/10/2004 :})

ADDPROPERTY(loInv,"InvoiceTotal",102.00)

 

loAuthor.Invoices.Add(loInv)

*loAuthor.Invoices[1] = loInv

 

 

loInv = CREATEOBJECT("Empty")

ADDPROPERTY(loInv,"InvoiceNumber","10")

ADDPROPERTY(loInv,"InvoiceDate",{01/10/2004 :})

ADDPROPERTY(loInv,"InvoiceTotal",199.00)

 

*loAuthor.Invoices[2] = loInv

loAuthor.Invoices.Add(loInv)

 

RETURN loAuthor

 

The whole thing then yields:

 

<AuthorEntity>

   <PhoneNumbers>

      <HomePhone />

      <WorkPhone />

      <Fax />

      <ServiceStarted>2004-10-07T00:35:08</ServiceStarted>

   </PhoneNumbers>

   <Au_id />

   <Au_lname>Strahl</Au_lname>

   <Au_fname>Rick</Au_fname>

   <Phone>80_ 123-1211</Phone>

   <Address>32 Kaiea Place</Address>

   <City>Paia</City>

   <State>HI</State>

   <Zip>96779</Zip>

   <Contract>0</Contract>

   <Invoices>

      <Invoice>

         <InvoiceNumber>9</InvoiceNumber>

         <InvoiceTotal>102</InvoiceTotal>

         <InvoiceDate>2004-05-10T00:00:00</InvoiceDate>

      </Invoice>

      <Invoice>

         <InvoiceNumber>10</InvoiceNumber>

         <InvoiceTotal>199.90</InvoiceTotal>

         <InvoiceDate>2004-01-10T00:00:00</InvoiceDate>

      </Invoice>

      <Invoices>

</AuthorEntity>

 

 

You can then take this entire XML string and turn it into a NodeList and send it back up to the server using the SOAP toolkit or wwSOAP:

 

loDom = CREATEOBJECT("MSXML2.DomDocument")

loDom.LoadXml(lcXML)

 

*** Using MSSOAP Toolkit

loSOAP = CREATEOBJECT("MSSOAP.SoapClient30")

loSOAP.MSSoapInit(lcWSDL)

llResult = loSOAP.UpdateAuthorEntity(loDom.DocumentElement.ChildNodes)

? llResult

? loSOAP.FaultString

 

 

So now, wwSOAP and wwXML provide the ability to both generate objects to XML and parse them back into objects (although this requires a ‘blue print object’).

 

On the inbound side I’ve also enhanced ParseObject which uses the WSDL file to dynamically construct an object on the fly and assign the properties into it. This routine goes the other way by looking at the WSDL file and adding properties to an empty object, then reading the value from the XML document into it. This has worked for the DevCon sessions and samples, but I’ve also added the ability to parse arrays (and by that route – collections).

 

In fact the code with wwSOAP to parse Web Service response from above looks like this:

 

oSOAP = CREATEOBJECT("wwSOAP")

oSOAP.nHttpConnectTimeout = 5

oSOAP.lParseReturnedObjects = .T.

 

*** Retrieve authors - returns DataSet returned as NodeList

oSOAP.AddParameter("Id","486-29-1786")

 

loAuthor = oSOAP.CallWSDLMethod("GetAuthorEntity",lcWSDL)

 

 

? loauthor.au_lname

? loAuthor.PhoneNumbers.HomePhone

? loAuthor.PhoneNUmbers.ServiceStarted

 

*** Array properties
? loAuthor.Invoices[1].InvoiceNumber

? loAuthor.Invoices[2].InvoiceTotal

 

This also works with MSSOAP. You can call the ParseObject method directly, but it requires picking up the WSDL file again so there’s some extra overhead there:

 

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

loNL = o.GetAuthorEntity("486-29-1786")

 

*** Retrieve the Root Node

loRoot = loNL.Item(0).ParentNode

 

loSOAP = CREATEOBJECT("wwSOAP")

loSOAP.ParseServiceWSDL(lcWSDL,.t.)

 

loAuthor = loSOAP.ParseObject(loRoot,"AuthorEntity")

 

 

This is pretty cool, even though this code is probably not 100% foolproof. For one thing you can run into problems if you end up with property names that VFP will not allow or get confused by. This code could be made more reliable by making it VFP 8 and later using the EMPTY object and the ADDPROPERTY() function instead of the AddProperty() method of the RELATION object.

 

I haven’t posted the update to wwSOAP yet, but look for it over the next few days. I’ll also update the Web Service .NET Interop article with some of these new functions which will reduce the code a bit and provide a solution to one of the missing pieces namely passing object up to the server.


The Voices of Reason


 

Rick Strahl
October 07, 2004

# re: Object parsing for Web Services called by Visual FoxPro

BTW, the code for the latter demo is part of the article at: http://www.west-wind.com/presentations/foxpronetwebservices/foxpronetwebservices.asp which includes the latest update of wwSOAP and wwXML.

Paul Mrozowski
October 08, 2004

# re: Object parsing for Web Services called by Visual FoxPro

Cool. I'll have to look this up again when I get back to another project. I hated having to change my variable names in .NET to all lowercase just because VFP couldn't handle/pass up the mixed-case version.

Rick Strahl
October 09, 2004

# re: Object parsing for Web Services called by Visual FoxPro

Yeah, no kidding. unfortunately there are still a few open issues, like how to deal with embedded entity objects such as datasets or XML nodes. The WSDL parser will choke on those. There's no standard way that these things are in the WSDL, so it's really hard to tell what they are at parse time.

Jeroen van Kalken
October 20, 2004

# re: Object parsing for Web Services called by Visual FoxPro

When are you releasing the updated wwSOAP?

I'm trying to get my feet wet in webservices.
It seems I need this update to correctly interpret the XML result containing an objectarray (and complex types)

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024