Using Caché XML Tools
Importing XML into Caché Objects
[Back] [Next]
   
Server:docs1
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

This chapter describes how to use the %XML.Reader class to import XML documents into Caché objects. It discusses the following topics:

Note:
The XML declaration of any XML document that you use should indicate the character encoding of that document, and the document should be encoded as declared. If the character encoding is not declared, Caché uses the defaults described in Character Encoding of Input and Output,” earlier in this book. If these defaults are not correct, modify the XML declaration so that it specifies the character set actually used.
You can also use %XML.Reader to read an arbitrary XML document and return a DOM (Document Object Model); see the chapter Representing an XML Document as a DOM,” later in this book.
Overview of Creating an XML Reader
Caché provides tools for reading an XML document and creating one or more instances of XML-enabled Caché objects that correspond to elements of that document. The essential requirements are as follows:
The %XML.Reader class works with the methods provided by the %XML.Adaptor class to do the following:
Note that the object instances created by %XML.Reader are not stored within the database; they are in-memory objects. If you want to store objects within the database, you must call the %Save() method (for persistent objects) or copy the relevant property values to a persistent object and save it. The application must also decide when to insert new data and when to update existing data; %XML.Reader has no way of making this distinction.
The following Terminal session shows a simple example. Here we read an XML file into a new object, examine that object, and then save that object:
GXML>Set reader = ##class(%XML.Reader).%New()
 
GXML>Set file="c:\sample-input.xml"
 
GXML>Set status = reader.OpenFile(file)
 
GXML>Write status
1
GXML>Do reader.Correlate("Person","GXML.Person")
 
GXML>Do reader.Next(.object,.status)
 
GXML>Write status
1
GXML>Write object.Name
Worthington,Jeff R.
GXML>Write object.Doctors.Count()
2
GXML>Do object.%Save()
This example uses the following sample XML file:
<Person GroupID="90455">
  <Name>Worthington,Jeff R.</Name>
  <DOB>1976-11-03</DOB>
  <Address>
    <City>Elm City</City>
    <Zip>27820</Zip>
  </Address>
  <Doctors>
    <Doctor>
      <Name>Best,Nora A.</Name>
    </Doctor>
    <Doctor>
      <Name>Weaver,Dennis T.</Name>
    </Doctor>
  </Doctors>
</Person>
Creating an Import Method
Overall Method Structure
Your method should do some or all of the following, in this order:
  1. Create an instance of the %XML.Reader class.
  2. Optionally specify the Format property of this instance, to specify the format of the file that you are importing; see Reader Properties.”
    By default, Caché assumes that XML files are in literal format. If your file is in SOAP-encoded format, you must indicate this so that the file can be read correctly.
  3. Optionally set other properties of this instance; see Reader Properties.”
  4. Open the source. To do so, use one of the following methods of %XML.Reader:
    In each case, you can optionally specify a second argument for the method, to override the value of the Format property.
  5. Correlate one or more XML element tags in this file with Caché XML-enabled classes that have the corresponding structures. There are two ways to do this:
  6. Instantiate the class instances as follows:
    The Next() method returns 0 when it reaches the end of the file. If you call Next() again after this, you will loop through the objects in the file again, starting at the top of the file. (The correlations you specified are still in effect.)
Error Checking
Most of the methods mentioned in the previous section return a status. You should check the status after each step and quit if appropriate.
Basic Import Example
Consider the following XML file called test.xml:
<Root>
   <Person>
      <Name>Elvis Presley</Name>
   </Person>
   <Person>
      <Name>Johnny Carson</Name>
   </Person>
</Root>
We start by defining an XML-enabled class, MyApp.Person, that is an object representation of a person:
Class MyApp.Person Extends (%Persistent,%XML.Adaptor)
{
Parameter XMLNAME = "Person";

Property Name As %String;
}
To import this file into an instance of a MyAppPerson class, we could write the following method:
ClassMethod Import()
{
    // Create an instance of %XML.Reader
    Set reader = ##class(%XML.Reader).%New()

    // Begin processing of the file
    Set status = reader.OpenFile("c:\test.xml")
    If $$$ISERR(status) {do $System.Status.DisplayError(status)}

    // Associate a class name with the XML element name
    Do reader.Correlate("Person","MyApp.Person")
    
    // Read objects from xml file
    While (reader.Next(.object,.status)) {
        Write object.Name,!
    }
    
    // If error found during processing, show it
    If $$$ISERR(status) {do $System.Status.DisplayError(status)}

}
This method performs several tasks:
As mentioned above, this example does not store objects to the database. Because MyPerson is a persistent object, you could do this by adding the following lines within the While loop:
    Set savestatus = object.%Save()
    If $$$ISERR(savestatus) {do $System.Status.DisplayError(savestatus)}
Accessing a Document at an HTTPS URL
For the OpenURL() method, if the document is at a URL that requires SSL/TLS, do the following:
  1. Use the Management Portal to create an SSL/TLS configuration that contains the details of the needed connection. For information, see the chapter Using SSL/TLS with Caché in the Caché Security Administration Guide.
    This is a one-time step.
  2. When you use %XML.Reader, set the SSLConfiguration property of the reader instance. For the value, specify the name of the SSL/TLS configuration that you created in the previous step.
Or, when you use %XML.Reader, also do the following:
  1. Create an instance of %Net.HttpRequest.
  2. Set the SSLConfiguration property of that instance equal to the configuration name of the SSL/TLS configuration that you created in the Management Portal.
  3. Use that instance of %Net.HttpRequest as the third argument to OpenURL().
For example:
 set reader=##class(%XML.Reader).%New() 
 set httprequest=##class(%Net.HttpRequest).%New() 
 set httprequest.SSLConfiguration="mysslconfigname" 
 set status=reader.OpenURL("https://myurl",,httprequest) 
Checking for Required Elements and Attributes
By default, the Next() method does not check for the existence of tags and attributes that correspond to properties that are marked as Required. To cause the reader to check for the existence of such tags and attributes, set the CheckRequired property of the reader to 1 before calling Next(). The default value for this property is 0, for compatibility reasons.
If you set CheckRequired to 1, and if you call Next() and the imported XML is missing a required element or attribute, the Next() method sets the sc argument to an error code. For example:
SAMPLES>set next= reader.Next(.object,.status)
 
SAMPLES>w next
0
SAMPLES>d $system.Status.DisplayError(status)
 
ERROR #6318: Property required in XML document: ReqProp
Handling Unexpected Elements and Attributes
Because the source XML documents might contain unexpected tags and attributes, the %XML.Adaptor class provides parameters to specify how to react when you import such a document. For details, see the chapter Special Topics in Projecting Objects to XML.
Controlling How Empty Elements and Attributes Are Imported
When you XML-enable an object, you specify how null values and empty strings are projected to XML (see Projecting Objects to XML).
One of the options is to set XMLIGNORENULL equal to "RUNTIME" (not case-sensitive) in the XML-enabled class. In this case, when you use %XML.Reader to read an XML file and create Caché objects, Caché uses the value of the IgnoreNull property of the reader to determine how to handle empty elements or attributes, as follows:
The IgnoreNull property of the reader has no effect unless the XMLIGNORENULL is "RUNTIME" in the XML-enabled class; the other possible values for XMLIGNORENULL are 0 (the default), 1, and "INPUTONLY"; see Projecting Objects to XML.
Example: IgnoreNull Is 0 (Default)
First, consider the following class:
Class EmptyStrings.Import Extends (%Persistent, %XML.Adaptor)
{

Parameter XMLNAME="Test";

///Reader will set IgnoreNull property
Parameter XMLIGNORENULL = "RUNTIME";

Property PropertyA As %String;

Property PropertyB As %String;

Property PropertyC As %String;

Property PropertyD As %String(XMLPROJECTION = "ATTRIBUTE");

Property PropertyE As %String(XMLPROJECTION = "ATTRIBUTE");
}
Also consider the following XML file:
<?xml version="1.0" encoding="UTF-8"?>
<Test PropertyD="">
  <PropertyA></PropertyA>
  <PropertyB xsi:nil="true" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</Test>
If you create an instance of %XML.Reader and use it to import this file into the previous class, you get the following result:
For example, if you wrote a routine to examine each property, inspect its value, and write output, you might get something like the following:
PropertyA is $char(0)
PropertyB is null
PropertyC is null
PropertyD is $char(0)
PropertyE is null
Example: IgnoreNull Is 1
In a variation of the preceding example, we set the IgnoreNull property of the reader equal to 1. In this case, all properties equal "".
For example, if you wrote a routine to examine each property, inspect its value, and write output, you might get something like the following:
PropertyA is null
PropertyB is null
PropertyC is null
PropertyD is null
PropertyE is null
Skipping Earlier Parts of the Input Document
An XML document consists of a set of nodes; the root node is numbered 0, the first element within that is numbered 1, and so on. You can specify the node at which to start reading; this is particularly useful for large documents. To do so, set the Node property of the reader. For the value, specify an integer.
An example method follows:
ClassMethod DemoSkippingAhead(nodenumber as %Integer=0)
{
    Set cls="GXML.Person"
    Set filename="c:\GXML.Person.xml"
    Set element="Person"

    // Create an instance of %XML.Reader
    Set reader = ##class(%XML.Reader).%New()

    // Begin processing of the file
    Set status = reader.OpenFile(filename)
    If $$$ISERR(status) {Do $System.Status.DisplayError(status)}

    // Associate a class name with the XML element name
    Do reader.Correlate(element,cls)
    
    //skip ahead in the file
    Set reader.Node=nodenumber

    // Read objects from xml file
    While (reader.Next(.object,.status)) {
        Write "Node number "_reader.Node_" contains "_object.Name,!
    }
}
This method assumes a specific input file, class name, and element name. By default, this method starts at the beginning of the file. For example:
GXML>d ##class(Readers.ReadFile).DemoSkippingAhead()
Node number 3 contains Emerson,Chad I.
Node number 30 contains O'Rielly,Patricia L.
Node number 63 contains Hanson,Brendan T.
Node number 120 contains Orwell,Tara H.
Node number 195 contains Gold,Elvis O.
Node number 234 contains Klein,Brenda U.
Node number 252 contains Yezek,Kristen Q.
Node number 327 contains Quine,Angelo B.
Node number 378 contains Vivaldi,Milhouse J.
Node number 453 contains Yezek,Vincent D.
Node number 471 contains Xander,Juanita D.
Node number 522 contains Winters,Kyra R.
Node number 555 contains Woo,Michelle J.
Node number 636 contains Ihringer,Yan A.
Node number 654 contains West,Hannah N.
Node number 729 contains Xiang,Bob G.
Node number 762 contains Ximines,Howard H.
Node number 789 contains Quixote,Jocelyn P.
Node number 864 contains Hills,Charles E.
Node number 897 contains Evans,Milhouse R.
In contrast, we could skip ahead as follows:
GXML>d ##class(Readers.ReadFile).DemoSkippingAhead(700)
Node number 729 contains Xiang,Bob G.
Node number 762 contains Ximines,Howard H.
Node number 789 contains Quixote,Jocelyn P.
Node number 864 contains Hills,Charles E.
Node number 897 contains Evans,Milhouse R.
Other Useful Methods
On occasion, you may need to use the following additional methods of %XML.Reader:
Reader Properties
You can set the following properties of %XML.Reader to control the overall behavior of your method:
%XML.Reader also provides properties that you can use to examine the document you are reading:
For information on the EntityResolver, SAXFlags, and SAXSchemaSpec properties, see the chapter Customizing How the SAX Parser Is Used. Note that the SAXMask property is ignored.
Redefining How the Reader Handles Correlated Objects
When an instance of %XML.Reader finds an XML element that is correlated to an XML-enabled class, the reader calls the XMLNew() method of that class, which in turn calls %New() by default. That is, when a reader finds a correlated element, it creates a new object of the correlated class. The new object is populated with the data that you read from the XML document.
You can customize this behavior by redefining XMLNew() in your XML-enabled class (or perhaps in your own custom XML adaptor). For example, this method can instead open an existing instance of that class. The existing instance then receives the data that you read from the XML document.
The following examples show how you can modify XMLNew() to update existing instances with new data from an XML document.
In both examples, for simplicity, we assume that a node in the XML document contains an ID that we can compare to the IDs in the extent for the class. We could of course compare the XML document to the existing objects in other ways.
When %XML.Reader Calls XMLNew()
For reference, %XML.Reader calls the XMLNew() method automatically in two circumstances:
Example 1: Modifying XMLNew() in an XML-Enabled Class
First consider the following XML file:
<?xml version="1.0" encoding="UTF-8"?>
<root>
  <Person>
    <CacheID>4</CacheID>
    <Name>Quine,Maria K.</Name>
    <DOB>1964-11-14</DOB>
    <Address>
      <City>Hialeah</City>
      <Zip>94999</Zip>
    </Address>
    <Doctors>
      <Doctor>
        <Name>Vanzetti,Debra B.</Name>
      </Doctor>
    </Doctors>
  </Person>
...
This file maps to the following Caché class (partly shown):
Class GXML.PersonWithXMLNew Extends (%Persistent,%Populate,%XML.Adaptor)
{

Parameter XMLNAME="Person";

/// make sure this is the same as the XMLNAME of the property 
/// in this class that is of type %XML.Id
Parameter CACHEID As %String = "CacheID";

Property IdForExport 
As %XML.Id(XMLNAME="CacheID",XMLPROJECTION="ELEMENT") [Private,Transient];

Property Name As %Name;

Property DOB As %Date(FORMAT = 5, MAXVAL = "+$h");

Property Address As GXML.Address;

Property Doctors As list Of GXML.Doctor;

}
In this class, the purpose of the IdForExport property is to project the Caché internal ID to an element (CacheID) when objects of this class are exported. (In this particular example, this enables us to easily generate suitable files for import. Your class does not have to contain such a property.)
The CACHEID parameter is used to indicate the element that we use for the Caché ID when we export objects of this class. This is included only for the convenience of the customized XMLNew() method that we have also added to this class. This method is defined as follows:
ClassMethod XMLNew(doc As %XML.Document, node As %Integer, 
contOref As %RegisteredObject = "") As GXML.PersonWithXMLNew
{
    Set id=""
    Set tmpnode=doc.GetNode(node)
    Do tmpnode.MoveToFirstChild()
    Do {
        //compare data node to the string given by the CACHEID parameter
        //which indicates the XMLNAME of the ids for this object
        If tmpnode.NodeData=..#CACHEID {
            //get the text from this node; this corresponds to an id in Cache
            Do tmpnode.GetText(.id)}
        } While tmpnode.MoveToNextSibling()
    
    //if there is no id in the given node, create a new object
    If id="" {
        Write !, "Creating a new object..."
        Quit ..%New()}
    
    //open the given object
    Set result=..%OpenId(id)
    
    //if the id doesn't correspond to an existing object, create a new object
    If result=$$$NULLOREF {
        Write !, "Creating a new object..." 
        Quit ..%New()}

    Write !, "Updating an existing object..."

    Quit result
}
%XML.Reader calls this method when it reads an XML document and correlates a node to GXML.PersonWithXMLNew. This method looks at the value of the CACHEID parameter in this class, which is CacheID. It then examines the node in the document with the element CacheID and gets its value.
If this ID corresponds to an existing object of this class, the method opens that instance. Otherwise, the method opens a new instance of this class. In both cases, the instance receives the properties as specified in the XML document.
Finally, the following utility class includes a method that opens the XML file and calls %XML.Reader:
Class GXML.DemoXMLNew Extends %RegisteredObject
{

ClassMethod ReadFile(filename As %String = "c:\temp\sample.xml")
{
    Set reader=##class(%XML.Reader).%New()
    Set sc=reader.OpenFile(filename)
    If $$$ISERR(sc) {Do $system.OBJ.DisplayError(sc) Quit  }
    
    Do reader.Correlate("Person","GXML.PersonWithXMLNew")
    
    //loop through elements in file 
    While reader.Next(.person,.sc) {
        Write !,person.Name,!
        Set sc=person.%Save()
        If $$$ISERR(sc) {Do $system.OBJ.DisplayError(sc) Quit  }
    }

    Quit
}

}
When you run the preceding method, one of the following occurs for each <Person> element in the file:
For example:
GXML>d ##class(GXML.DemoXMLNew).ReadFile()
 
Updating an existing object...
Zampitello,Howard I.
 
Creating a new object...
Smyth,Linda D.
 
Creating a new object...
Vanzetti,Howard I.
 
Example 2: Modifying XMLNew() in a Custom XML Adaptor
In the second example, we create a custom XML adaptor to perform the same actions as shown in the first example. The adaptor class is as follows:
Class GXML.AdaptorWithXMLNew Extends %XML.Adaptor
{

/// make sure this is the same as the XMLNAME of the property in this class
/// that is of type %XML.Id
Parameter CACHEID As %String = "CacheID";

Property IdForExport 
As %XML.Id(XMLNAME="CacheID",XMLPROJECTION="ELEMENT") [Private,Transient];

ClassMethod XMLNew(document As %XML.Document, node As %Integer, 
containerOref As %RegisteredObject = "") As %RegisteredObject 
[CodeMode=objectgenerator,GenerateAfter=%XMLGenerate,ServerOnly=1]
{
 If %compiledclass.Name'="GXML.AdaptorWithXMLNew" {
  Do %code.WriteLine(" Set id=""""")
  Do %code.WriteLine(" Set tmpnode=document.GetNode(node)")
  Do %code.WriteLine(" Do tmpnode.MoveToFirstChild()")
  Do %code.WriteLine(" Do {")
  Do %code.WriteLine("     If tmpnode.NodeData=..#CACHEID ")
  Do %code.WriteLine("       {Do tmpnode.GetText(.id)}")
  Do %code.WriteLine(" } While tmpnode.MoveToNextSibling() ")
  Do %code.WriteLine(" If id="""" {")
  Do %code.WriteLine("  Write !,""Creating new object...""")
  Do %code.WriteLine("  Quit ##class("_%class.Name_").%New()}")
  Do %code.WriteLine(" set result=##class("_%class.Name_").%OpenId(id)")
  Do %code.WriteLine(" If result=$$$NULLOREF {")
  Do %code.WriteLine("     Write !,""Creating new object...""")
  Do %code.WriteLine("     Quit ##class("_%class.Name_").%New() }")
  Do %code.WriteLine(" Write !,""Updating existing object ...""")
  Do %code.WriteLine(" Quit result")

  }

 QUIT $$$OK
}

}
The IdForExport property and the CACHEID parameter establish a convention for how we project the Caché internal ID to an element when objects of subclasses are exported. The intent is that if you redefine IdForExport in a subclass, you redefine CACHEID correspondingly.
In this class, the XMLNew() method is a method generator. When this class (or any subclass) is compiled, Caché writes the code shown here into the body of this method. See the chapter Method Generators in Using Cache Objects.
The following class extends our custom adaptor:
Class GXML.PersonWithXMLNew2 
Extends (%Persistent, %Populate, GXML.AdaptorWithXMLNew)
{

Parameter XMLNAME = "Person";

Property Name As %Name;

Property DOB As %Date(FORMAT = 5, MAXVAL = "+$h");

Property Address As GXML.Address;

Property Doctors As list Of GXML.Doctor;

}
When you run the sample ReadFile method shown earlier, for each <Person> element in the file, the method either creates and saves a new record or opens and updates an existing record.
Additional Examples
This section contains some additional examples.
Flexible Reader Class
The following example shows a more flexible reader method that accepts the filename, directory, class, and element as input arguments. The method saves each object that it reads.
Class Readers.BasicReader Extends %RegisteredObject
{

ClassMethod Read(mydir, myfile, class, element)
{
   set reader=##class(%XML.Reader).%New()
   if $extract(mydir,$length(mydir))'="/" {set mydir=mydir_"/"}
   set file=mydir_myfile
   set status=reader.OpenFile(file)
   if $$$ISERR(status) {do $System.Status.DisplayError(status)}

   do reader.Correlate(element,class)

   while reader.Next(.object,.status)
   {
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      set status=object.%Save()
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      }
}

}
Notice that when you read in a Person object, you automatically read in its corresponding Address object. (And when you save the Person object, you automatically save the corresponding Address object as well.) This is a fairly crude technique that would be suitable only for bulk load of data; it does not make any attempt to compare to or update existing data.
In order to use this method, you would need an XML-enabled class whose projection matched the incoming XML document. Suppose that you had XML-enabled classes named MyApp.PersonWithAddress and MyApp.Address. Also suppose that you had an XML document as follows:
<?xml version="1.0" encoding="UTF-8"?>
<Root>
   <Person>
      <Name>Able, Andrew</Name>
      <DOB>1977-10-06</DOB>
      <Address>
         <Street>6218 Clinton Drive</Street>
         <City>Reston</City>
         <State>TN</State>
         <Zip>87639</Zip>
      </Address>
   </Person>
</Root>
To read the objects in this file and save them to disk, you would do something like the following:
 set dir="C:\XMLread-these"
 set file="PersonData.txt"
 set cls="MyApp.PersonWithAddress"
 set element="Person"
 do ##class(Readers.BasicReader).Read(dir,file,cls,element)
Reading a String
The following method accepts an XML string, class, and element as input arguments. It saves each object that it reads.
Class Readers.BasicReader Extends %RegisteredObject
{

ClassMethod ReadString(string, class, element)
{
   set reader=##class(%XML.Reader).%New()
   set status=reader.OpenString(string)
   if $$$ISERR(status) {do $System.Status.DisplayError(status)}

   do reader.Correlate(element,class)

   while reader.Next(.object,.status)
   {
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      set status=object.%Save()
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      }
}

}
To use this method, you would do something like the following:
 set cls="MyApp.Person"
 set element="Person"
 do ##class(Readers.BasicReader).ReadString(string,cls,element)