Try OpenEdge Now
skip to main content
Working with XML
Writing XML Documents with the Simple API for XML (SAX) : Examples : Concurrently reading and writing XML documents
 

Concurrently reading and writing XML documents

One common use case for the ABL SAX objects is to read a source XML document element by element, transform the elements, and output the updated elements to a new XML document. During this process, you might want to:
*Decide if the element should be included or excluded in the output
*Alter the attributes of the element
*Alter the content of the element
*Add new elements
*Rearrange elements
In this example, a simple XML document that represents address data is read in and transformed to create a new XML address list. You can read the comments embedded in the code to see examples of the kinds of changes that are possible. The example is provided as a single procedure which serves as both the SAX driver and the SAX handler (includes internal callback procedures).
The program transforms the source XML document in a single parse. This technique forces custom processing logic down into the callback procedures. This can quickly lead to added complexity as you mingle your transformation logic with the logic of the parser life cycle. In reality, you will likely use a progressive scan parse for anything more than simple adjustments to source XML. In this scenario, you would use the SAX-reader to feed your driver procedure the next piece of XML data, use the logic in your driver procedure to process the data, and output the desired XML data with the SAX writer object.
The following is a snippet of the source XML document sampledata2.xml.

sampledata2.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<QAddress>
<QHeader Revision="1.0" Mime="text/xml">
<Queue OperatorID="299" Name="Agent Addresses" Region="New England"/>
<Comments Text="This is a sample list of addresses for mailing labels or
   envelopes."/>
</QHeader>
<QBody>
<Address Row="1" Catalog="No">
<Contact>Joe Smith</Contact>
<Name>Pedal Power Cycles</Name>
<Urbanization>P.O. Box 1719</Urbanization>
<Street>304 Hancock Street</Street>
<City>Bangor</City>
<State>ME</State>
<Country>US</Country>
<Zip>04402</Zip>
</Address>
. . .
</QBody>
</QAddress>
This is the sample code:

sax-readwrite.p

/* This sample uses the SAX-reader, SAX-attributes and SAX-writer objects.
   SAX-reader reads the XML document. The SAX parser passes the attributes of
   each new element to the StartElement callback procedure in a SAX-attributes
   object. The StartElement transforms the XML document by manipulating
   attributes and passes the altered SAX-attributes object to the SAX-writer
   object. The SAX-writer object writes the data out to a new XML document. */

DEFINE VARIABLE hSAXReader   AS HANDLE    NO-UNDO.
DEFINE VARIABLE hSAXWriter   AS HANDLE    NO-UNDO.

/* Keep the current element name in a global scope. */
DEFINE VARIABLE CurrentTag   AS CHARACTER NO-UNDO.

/* For a domestic mailing, do not use <Country> elements in new XML. */
DEFINE VARIABLE NoCountry    AS LOGICAL   NO-UNDO INITIAL TRUE.

/* For customers with PO Boxes, do not use <Street> elements in new XML. */
DEFINE VARIABLE NoStreet     AS LOGICAL   NO-UNDO.

/* New envelope line to request address correction. */
DEFINE VARIABLE EnvelopeSlug AS CHARACTER NO-UNDO
  INITIAL "Attention Post Master: Address Correction Requested.".

CREATE SAX-WRITER hSAXWriter.
hSAXWriter:FORMATTED = TRUE.
hSAXWriter:STANDALONE = TRUE.
hSAXWriter:ENCODING = "UTF-8".
hSAXWriter:SET-OUTPUT-DESTINATION("FILE", "sax-readwrite.xml").

hSAXWriter:START-DOCUMENT( ).
CREATE SAX-READER hSAXReader.
hSAXReader:SET-INPUT-SOURCE("FILE", "sampledata.xml").
hSAXReader:SAX-PARSE( ).
hSAXWriter:END-DOCUMENT( ).

DELETE OBJECT hSAXWriter.
DELETE OBJECT hSAXReader.

/******************************************************************/
/* Callback procedures for SAX parser (SAX-reader object) */
/******************************************************************/
PROCEDURE StartElement:
  DEFINE INPUT PARAMETER namespaceURI   AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER localName      AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER qName          AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER hSAXAttributes AS HANDLE    NO-UNDO.

  ASSIGN CurrentTag = localName.

  IF localName = "Qheader" THEN
    hSAXAttributes:INSERT-ATTRIBUTE("MailDate", STRING(TODAY)).

  IF localName = "Address" THEN DO:
    hSAXAttributes:REMOVE-ATTRIBUTE("Row").
    hSAXAttributes:UPDATE-ATTRIBUTE("Catalog", "Yes").
  END.

  /* This address will use a PO Box instead of a Street Address */
  IF localName = "Urbanization" THEN ASSIGN NoStreet = TRUE.
  /* Only call SAX-writer for elements wanted in new XML. */
  CASE localName:
    WHEN "Country" THEN IF NOT NoCountry THEN
      hSAXWriter:START-ELEMENT(localName, namespaceURI, hSAXAttributes).
    WHEN "Street" THEN IF NOT NoStreet THEN
      hSAXWriter:START-ELEMENT(localName, namespaceURI, hSAXAttributes).
    OTHERWISE hSAXWriter:START-ELEMENT(localName, namespaceURI,
      hSAXAttributes).
  END CASE.
END PROCEDURE.

PROCEDURE Characters:
  DEFINE INPUT PARAMETER charData AS MEMPTR  NO-UNDO.
  DEFINE INPUT PARAMETER numChars AS INTEGER NO-UNDO.

  DEFINE VARIABLE cData AS CHARACTER NO-UNDO.

  ASSIGN cData = GET-STRING(charData, 1, GET-SIZE(charData)).

  /* Only use 5 digit zip codes. */
  IF CurrentTag = "Zip" THEN
    cData = SUBSTRING(cData, 1, 5).
  /* Only write content with SAX-writer if a start tag is waiting for content.
     "5" is a status of "SAX-WRITE-ELEMENT", which indicates that an end tag
     was last written and this current data is associated with a element not
     wanted in the output mailing list. */
  IF hSAXWriter:WRITE-STATUS NE 5 THEN
    hSAXWriter:WRITE-CHARACTERS(cData).
END PROCEDURE.

PROCEDURE EndElement:
  DEFINE INPUT PARAMETER namespaceURI AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER localName    AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER qName        AS CHARACTER NO-UNDO.

 /* Only call SAX-writer for elements wanted in output mailing list. */
  CASE localName:
    WHEN "Country" THEN IF NOT NoCountry THEN
      hSAXWriter:END-ELEMENT(localName, namespaceURI).
    WHEN "Street" THEN IF NOT NoStreet THEN
      hSAXWriter:END-ELEMENT(localName, namespaceURI).
    OTHERWISE hSAXWriter:END-ELEMENT(localName, namespaceURI).
  END CASE.

  /* Add another envelope line after Zip Code. */
  IF localName = "Zip" THEN DO:
    hSAXWriter:START-ELEMENT("EnvelopeSlug", namespaceURI).
    hSAXWriter:WRITE-CHARACTERS(EnvelopeSlug).
    hSAXWriter:END-ELEMENT("EnvelopeSlug", namespaceURI).
  END.

  /* Reset check for PO Box versus Street address. */
  IF localName = "Address" THEN
    ASSIGN NoStreet = FALSE.
END PROCEDURE.
The following partial sax-readwrite.xml output file shows key changes in bold:

sax-readwrite.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<QAddress>
<QHeader Revision="1.0" Mime="text/xml" MailDate="10/25/06">
<Queue OperatorID="299" Name="Agent Addresses" Region="New England">
  </Queue>
<Comments Text="Sample list of addresses...">
  </Comments>
  </QHeader>
<QBody>
<Address Catalog="Yes">
<Contact>Joe Smith</Contact>
<Name>Pedal Power Cycles</Name>
<Urbanization>P.O. Box 1719</Urbanization>
<City>Bangor</City>
<State>ME</State>
<Zip>04402</Zip>
    <EnvelopeSlug>Attention Post Master: Address Correction
              Requested.

          </EnvelopeSlug>

    </Address>
.
.
.
  </QBody>
</QAddress>
Note that the SAX-writer object Start-Element( ) method takes a SAX-attributes object as an optional parameter. In this variation of the last example, all the transformation logic is stripped out and a SAX-attributes object is created and populated with attributes at the procedure's top (global) scope. The calls to the SAX-writer object in the callback procedure ignore the SAX-attributes object created by the parser for SAX-reader and only pass the global SAX-attributes object to the SAX-writer if the current element is address. The end result is that all attribute data in the source XML is lost and only the attribute data created by the procedure is output to the new XML document. Note the changes shown in bold:

sax-readwrite2.p

/* This sample shows a programatically created SAX-attributes object used
to override the attributes of the input XML document. */
DEFINE VARIABLE hSAXWriter       AS HANDLE NO-UNDO.
DEFINE VARIABLE hSAXReader       AS HANDLE NO-UNDO.
DEFINE VARIABLE hMySAXAttributes AS HANDLE NO-UNDO.

CREATE SAX-WRITER hSAXWriter.
hSAXWriter:FORMATTED = TRUE.
hSAXWriter:STANDALONE = TRUE.
hSAXWriter:ENCODING = "UTF-8".
hSAXWriter:SET-OUTPUT-DESTINATION("FILE", "sax-readwrite2.xml").

hSAXWriter:START-DOCUMENT( ).
CREATE SAX-ATTRIBUTES hMySAXattributes.

hMySAXAttributes:INSERT-ATTRIBUTE("MailDate", STRING(TODAY)).

hMySAXAttributes:INSERT-ATTRIBUTE("Catalog", "Yes").

CREATE SAX-READER hSAXReader.
hSAXReader:SET-INPUT-SOURCE("FILE", "sampledata.xml").
hSAXReader:SAX-PARSE( ).
hSAXWriter:END-DOCUMENT( ).

DELETE OBJECT hSAXWriter.
DELETE OBJECT hSAXReader.
DELETE OBJECT hMySAXAttributes.


/* Callback procedures for SAX parser (SAX-reader object) */
PROCEDURE StartElement:
  DEFINE INPUT PARAMETER namespaceURI   AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER localName      AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER qName          AS CHARACTER NO-UNDO.
  DEFINE INPUT PARAMETER hSAXAttributes AS HANDLE    NO-UNDO.

  /* Only call SAX-writer with attributes for address elements. */
  IF locaLNAME = "Address" THEN

    hSAXWriter:START-ELEMENT(localName, namespaceURI, hMySAXAttributes).

  ELSE

    hSAXWriter:START-ELEMENT(localName, namespaceURI).

END.
...
The following partial output XML document is the result:

sax-readwrite2.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
. . .
<Address MailDate="10/25/06" Catalog="Yes">
<Contact>Joe Smith</Contact>
<Name>Pedal Power Cycles</Name>
<Urbanization>P.O. Box 1719</Urbanization>
<Street>304 Hancock Street</Street>
<City>Bangor</City>
<State>ME</State>
<Country>US</Country>
<Zip>04402</Zip>
</Address>
. . .
</QBody>
</QAddress>