A worthwhile but minimal eXpat example usage

"Nick MacDonald" <[email protected]> Fri, 8 Aug 2008 14:41:41 -0400
Newsgroups gmane.text.xml.expat.general
Message-ID <[email protected]>
I have been using eXpat for a couple or more years, and I have been on
this mailing list for much of that time.  I notice that we seem to get
asked some of the same questions over and over, and that for some
reason the examples included with eXpat don't appear to be instructive
enough.

I wonder if I might supply the attached example program as a solution
to those people who need a more fleshed out example.  Its designed to
find a pattern in the XML and extract a subset.  Its deliberately kept
simplified to avoid introducing too much complexity which would be
distracting for a beginner.

I would be perfectly happy to have this file included with eXpat or I
would like a recommendation from others here on the list if there is
something specific that would make it useful.  I don't have my own
page on which I can host it, but others may be willing to do so.

Thanks,
  Nick

_______________________________________________
Expat-discuss mailing list
[email protected]
http://mail.libexpat.org/mailman/listinfo/expat-discuss
eXpatExample.c (application/octet-stream, 18 KB)
/*
  This is an example XML parser implemented using eXpat.

  This example parses the following XML file, and extracts all the
  addresses of guys who's last name are Smith.

  This code was written by:
    Nick MacDonald
    2008-Aug-07

  This code was written and tested against eXpat 2.0.1.

  This code is not bullet proof, because it is only a demo and too
  much error checking would distract from the purpose of instruction.
  XML can be pretty complictated to process, for example, what would
  you expect from the following bit of input:
    <Residence Type="Bungalow" SquareFeet="1025">
      <Address>
        123 Any Street
        <Owner FirstName="Bob" LastName="Jones"/>
        Bathurst, NB
      </Address>
    </Residence>
  Note how there is a new tag right in the middle of the address.  For
  this particular program, this does not make a lot of sense, but it
  does still parse correctly, but it adds an extra line break in the middle
  of the address where the <Owner> tag is extacted.  If you want your
  program to handle anything that can be put in a valid XML file and thrown
  at it, then you have a very complex design task ahead of you.  I think
  its probably better to focus on getting something to work and then work
  on detecting all the rest that you can't handle.


  This code is public domain, but I expect to receive proper credit
  if you base your code on mine.  This code was not written for any
  purpose other than to be an example.  This code is not sponsored
  by me or by my employer--it comes with no warranty or support, nor
  any promise to meet any particular purpose or function...  you use
  it at your sole risk.

  This is the sample input file:
<?xml version="1.0"?>
<!-- This is a sample file to be parsed by eXpat as part of
     example code written by Nick MacDonald.
     The example program will extract the street addresses of
     all guys who's last name is Smith.
  -->

<AddressList>
  <Residence Type="Bungalow" SquareFeet="1200">
    <Owner LastName="Smith" FirstName="Tom"/>
    <Address>
      123 Any Street
      Moncton, NB, Canada
    </Address>
  </Residence>

  <Residence Type="Apt" SquareFeet="875">
    <Owner LastName="Smythe" FirstName="Bill"/>
    <Address>
      402 Any Street
      Apt. 28786
      Chatham, NB, Canada
    </Address>
  </Residence>

  <Residence Type="TwoStory" SquareFeet="2750">
    <Owner LastName="Smith" FirstName="Danny"/>
    <Address>
      9876 Any Street
      Saint John, NB, Canada
    </Address>
  </Residence>
</AddressList>

  The output produced is:
Match found:
  Name:  Tom, Smith
  Address: 
      123 Any Street
      Moncton, NB, Canada
    
Match found:
  Name:  Danny, Smith
  Address: 
      9876 Any Street
      Saint John, NB, Canada
    
XML file parsing successful, 2 matches found for Smith

*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "expat.h"


#define BUFFERSIZE    4*1024
#define MAX_ADDRESS      512
#define MAX_FIRST_NAME    32
#define MAX_LAST_NAME     32
#define MAX_STACK_DEPTH   10


// Elements of the <Residence Type="" SquareFeet=""> tag
#define XML_TAG_RESIDENCE                  "Residence"
#define XML_TAG_RESIDENCE_PARM_TYPE        "Type"
#define XML_TAG_RESIDENCE_PARM_SQUAREFEET  "SquareFeet"

// Elements of the <Owner FirstName="" LastName=""/> tag
#define XML_TAG_OWNER                      "Owner"
#define XML_TAG_OWNER_PARM_FIRSTNAME       "FirstName"
#define XML_TAG_OWNER_PARM_LASTNAME        "LastName"

// Elements of the <Address></Address> tag
#define XML_TAG_ADDRESS                    "Address"

/* When the XML parsing finds data that it is seeking, it
   passes the data to a callback function of this type for
   the processing (which is simply output in this example.)
*/
typedef void (*tCallbackToProcessFoundRecords)(
  const char*  ownerFirstName,
  const char*  ownerLastName,
  const char*  address
);


/* Set this to one to see extra debugging messages */
static int  debugOutputEnabled =0;


/*
  A pointer to one of these structures is passed into eXpat and then
  along to any callback function calls it makes.  This allows the
  callback functions to have persistant state during the event based
  parsing of the XML file.
*/
struct sXMLParseState
{
  unsigned int                    numberMatchesFound;
  int                             parsingResidenceTag;
  int                             currentlyParsingAddress;
  int                             lastNameMatches;
  char                            lastNameToMatch[MAX_LAST_NAME];
  char                            currentAddress[MAX_ADDRESS];
  char                            currentFirstName[MAX_FIRST_NAME];
  char                            currentLastName[MAX_LAST_NAME];
  int                             errorEncountered;
  tCallbackToProcessFoundRecords  callbackToProcessFoundRecords;

  XML_Parser                      eXpatXMLParser;

  unsigned int                    stackPointer;
  char*                           tagStack[MAX_STACK_DEPTH];
  char*                           tagContent[MAX_STACK_DEPTH];
  unsigned int                    tagContentSize[MAX_STACK_DEPTH];
};


/* A utility function to initialize the stack state
*/
static void initStack(
  struct sXMLParseState*  XMLParseState
)
{
  unsigned int  i;

  XMLParseState->stackPointer =0;
  for(i=0; i<MAX_STACK_DEPTH; i++)
  {
    XMLParseState->tagStack[i]       =NULL;
    XMLParseState->tagContent[i]     =NULL;
    XMLParseState->tagContentSize[i] =0;
  }
}


/* A utility function to push (add) a new element onto the top of the stack
*/
static int pushTagOntoStack(
  struct sXMLParseState*  XMLParseState,
  const char*             tagToPush
)
{
  int rv =0;

  if (XMLParseState->stackPointer+1 < MAX_STACK_DEPTH)
  {
    XMLParseState->stackPointer++;
    XMLParseState->tagStack[XMLParseState->stackPointer] =strdup(tagToPush);
    rv =1;
  }
  else
  {
    fprintf(stderr, "%s: not enough room on stack for new tag (%s)\n",
                    __FUNCTION__, tagToPush);
  }

  return rv;
}


/* A utility function to pop (remove) an element from the top of the stack
*/
static void popTagFromStack(
  struct sXMLParseState*  XMLParseState
)
{
  if (XMLParseState->stackPointer > 1)
  {
    if (NULL != XMLParseState->tagStack[XMLParseState->stackPointer])
    {
      free(XMLParseState->tagStack[XMLParseState->stackPointer]);
      XMLParseState->tagStack[XMLParseState->stackPointer] =NULL;
    }
    if (NULL != XMLParseState->tagContent[XMLParseState->stackPointer])
    {
      free(XMLParseState->tagContent[XMLParseState->stackPointer]);
      XMLParseState->tagContent[XMLParseState->stackPointer] =NULL;
    }
    XMLParseState->tagContentSize[XMLParseState->stackPointer] =0;
    XMLParseState->stackPointer--;
  }
}


/* A utility function to add data to the content being accumulated
   in the current top of stack item.  It is not optimized for any
   memory management, so it assumes that the memory manager handles
   malloc() and realloc() in a reasonably efficient way.  If this is
   not true, and there is a lot of memcpy()ing going on, then it should
   be simple enough to add an extra element for current size, and make
   initial malloc()s and subsequent realloc()s larger than needed to 
   reduce the number of reallocations that might occur.
*/
int addDataToCurrentlyStackedTag(
        struct sXMLParseState*  XMLParseState,
  const char*                   textToAdd,
        int                     lengthOfTextToAdd
)
{
  int  rv =0;

  if (NULL == XMLParseState->tagContent[XMLParseState->stackPointer])
  {
    if (debugOutputEnabled)
    {
      printf("%s: lengthOfTextToAdd=%d\n", __FUNCTION__, lengthOfTextToAdd);
    }
    XMLParseState->tagContentSize[XMLParseState->stackPointer] =lengthOfTextToAdd;
    XMLParseState->tagContent[XMLParseState->stackPointer] =malloc(lengthOfTextToAdd+1);
    if (NULL != XMLParseState->tagContent[XMLParseState->stackPointer])
    {
      strncpy(XMLParseState->tagContent[XMLParseState->stackPointer], textToAdd, lengthOfTextToAdd);
      XMLParseState->tagContent[XMLParseState->stackPointer][lengthOfTextToAdd] ='\0';
      rv =1;
    }
  }
  else
  {
    unsigned int  oldLength =XMLParseState->tagContentSize[XMLParseState->stackPointer];
    unsigned int  newLength =oldLength+lengthOfTextToAdd;

    XMLParseState->tagContentSize[XMLParseState->stackPointer] =newLength;
    if (debugOutputEnabled)
    {
      printf("%s: oldLength=%u, lengthOfTextToAdd=%d newLength=%u\n", __FUNCTION__, oldLength, lengthOfTextToAdd, newLength);
    }
    XMLParseState->tagContent[XMLParseState->stackPointer] =realloc(XMLParseState->tagContent[XMLParseState->stackPointer], newLength+1);
    if (NULL != XMLParseState->tagContent[XMLParseState->stackPointer])
    {
      strncpy(XMLParseState->tagContent[XMLParseState->stackPointer]+oldLength, textToAdd, lengthOfTextToAdd);
      XMLParseState->tagContent[XMLParseState->stackPointer][newLength] ='\0';
      rv =1;
    }
  }

  return rv;
}


/* A utility function that returns a pointer to the data current
   accumlated at the stop of the stack.
*/
static const char* getTopTagContentPtr(
  struct sXMLParseState*  XMLParseState
)
{
  return XMLParseState->tagContent[XMLParseState->stackPointer];
}


/* A utility function that looks at the pairings of parameter tags
   and values passed to the Start Tag callback, and looks for a
   specific parameter to get its value.
*/
static const char*  findParameter(
  const char*  parms[],
  const char*  parmToLookFor
)
{
  const char*   rv =NULL;
  unsigned int  i;

  for(i=0; NULL != parms[i]; i+=2)
  {
    if (0 == strcmp(parms[i], parmToLookFor))
    {
      rv =parms[i+1];
      break;
    }
  }

  return rv;
}


/* Every time eXpat parses the start of a new XML tag, it
   calls this callback to allow the code to update its internal
   state, and determine what further processing might be
   necessary
*/
static void XMLCALL startElementCallback(
  void*         userData,
  const char*   tagText,
  const char**  atts
)
{
  struct sXMLParseState*  XMLParseState =(struct sXMLParseState*)userData;

  if (debugOutputEnabled)
  {
    printf("Matched start tag:  %s\n", tagText);
    printf("Seeking last name=%s\n", XMLParseState->lastNameToMatch);
  }

  if (!pushTagOntoStack(XMLParseState, tagText))
  {
    XMLParseState->errorEncountered =1;
  }
  else
  {
    if (0 == strcmp(tagText, XML_TAG_RESIDENCE))
    {
      // Starting a new <Residence> tag, so clear any previous state
      XMLParseState->lastNameMatches =0;
      memset(XMLParseState->currentAddress,   '\0', sizeof(XMLParseState->currentAddress));
      memset(XMLParseState->currentFirstName, '\0', sizeof(XMLParseState->currentFirstName));
      memset(XMLParseState->currentLastName,  '\0', sizeof(XMLParseState->currentLastName));

      if (0 != XMLParseState->parsingResidenceTag)
      {
        fprintf(stderr, "Found nested Residence tag, which is not allowed\n");
        XMLParseState->errorEncountered =1;
      }
      else
      {
        XMLParseState->parsingResidenceTag =1;
      }
    }
    else
    {
      // The remaining tags of interest are only interesting when they are inside of
      // a residence tag, so check that now
      if (0 != XMLParseState->parsingResidenceTag)
      {
        if (0 == strcmp(tagText, XML_TAG_OWNER))
        {
          const char*  firstName =NULL;
          const char*  lastName  =NULL;

          firstName =findParameter(atts, XML_TAG_OWNER_PARM_FIRSTNAME);
          lastName  =findParameter(atts, XML_TAG_OWNER_PARM_LASTNAME);

          if ( (NULL != firstName) && (NULL != lastName) )
          {
            strncpy(XMLParseState->currentFirstName, firstName, MAX_FIRST_NAME);
            strncpy(XMLParseState->currentLastName,  lastName,  MAX_LAST_NAME);

            if (0 == strcmp(lastName, XMLParseState->lastNameToMatch))
            {
              XMLParseState->lastNameMatches =1;
            }
          }
        }

        if (0 == strcmp(tagText, XML_TAG_ADDRESS))
        {
          XMLParseState->currentlyParsingAddress =1;
        }
      }
    }
  }

  if (0 != XMLParseState->errorEncountered)
  {
    XML_StopParser(XMLParseState->eXpatXMLParser, 0);
  }
}


/* Every time eXpat parses the end of an XML tag, it calls
   this callback to notify the code of the event.  This
   function tries to determine if the current state is
   relevant to overall goal, and if so, makes a callback of
   its own to process the extracted data/state.
*/
static void XMLCALL endElementCallback(
  void*        userData,
  const char*  tagText
)
{
        struct sXMLParseState*  XMLParseState =(struct sXMLParseState*)userData;
  const char*                   currentTagContent =NULL;

  if (debugOutputEnabled)
  {
    printf("Matched end tag:  %s\n", tagText);
    printf("Seeking last name=%s\n", XMLParseState->lastNameToMatch);
  }

  currentTagContent =getTopTagContentPtr(XMLParseState);

  if (0 == strcmp(tagText, XML_TAG_RESIDENCE))
  {
    XMLParseState->parsingResidenceTag =0;
  }

  if (0 != XMLParseState->parsingResidenceTag)
  {
    if (0 == strcmp(tagText, XML_TAG_ADDRESS))
    {
      XMLParseState->currentlyParsingAddress =0;

      strncpy(XMLParseState->currentAddress, currentTagContent, sizeof(XMLParseState->currentAddress));

      if (0 != XMLParseState->lastNameMatches)
      {
        // Found a match for the type of record we're searching for
        XMLParseState->numberMatchesFound++;

        if (NULL != XMLParseState->callbackToProcessFoundRecords)
        {
          (*XMLParseState->callbackToProcessFoundRecords)( XMLParseState->currentLastName,
                                                           XMLParseState->currentFirstName,
                                                           XMLParseState->currentAddress );
        }
      }
    }
  }

  popTagFromStack(XMLParseState);

  if (0 != XMLParseState->errorEncountered)
  {
    XML_StopParser(XMLParseState->eXpatXMLParser, 0);
  }
}


/* All the content/data that is between the XML tags is passed into
   this callback.  eXpat seems to make a number of small chunks of the
   data, rather than larger blocks.  The blocks are also not terminated
   (with the usual C \0 terminator) so you need to handle the in some
   way to concantenate them all together.  Additionally, because tags
   are legallaly allowed to nest in the middle of such content, you
   need to implement a stack based approach to be able to collect the
   data before and after an embedded tag and treat the data as one
   complete block (if that is appropriate for your parsing needs.)
*/
static void XMLCALL elementTextCallback(
  void*        userData,
  const char*  text,
  int          len
)
{
  struct sXMLParseState*  XMLParseState =(struct sXMLParseState*)userData;

  if (debugOutputEnabled)
  {
    printf("%s:  len=%d\n", __FUNCTION__, len);
  }

  if (len > 0)
  {
    if (!addDataToCurrentlyStackedTag(XMLParseState, text, len))  XMLParseState->errorEncountered =1;
  }

  if (0 != XMLParseState->errorEncountered)
  {
    XML_StopParser(XMLParseState->eXpatXMLParser, 0);
  }
}

/* When a record is found, this callback function (part of the
   design of the example, not a fundemental part of the design
   of eXpat itself) is called.  In this simple case, it just
   produces a printout of the found data.
*/
static void callbackToProcessFoundRecords(
  const char*  ownerFirstName,
  const char*  ownerLastName,
  const char*  address
)
{
  fprintf(stdout, "Match found:\n");
  fprintf(stdout, "  Name:  %s, %s\n", ownerLastName, ownerFirstName);
  fprintf(stdout, "  Address: %s\n", address);
}


/* Setup calls to eXpat to be able to find all the residences
   owned by an owner with the last name of "Smith", and print
   out the owners name, and the address of the residence.
*/
static void ParseXMLToExtractAddressesForLastName(
  FILE*        fileToParse,
  const char*  lastNameToExtractBy
)
{
  char                   XMLFileReadBuffer[BUFFERSIZE];
  int                    done      =0;
  XML_Parser             XMLparser =NULL;
  struct sXMLParseState  XMLParseState;

  memset(&XMLParseState,    '\0', sizeof(XMLParseState));
  memset(XMLFileReadBuffer, '\0', sizeof(XMLFileReadBuffer));

  strncpy(XMLParseState.lastNameToMatch, lastNameToExtractBy, sizeof(XMLParseState.lastNameToMatch));
  XMLParseState.parsingResidenceTag           =0;
  XMLParseState.currentlyParsingAddress       =0;
  XMLParseState.numberMatchesFound            =0;
  XMLParseState.errorEncountered              =0;
  XMLParseState.callbackToProcessFoundRecords =callbackToProcessFoundRecords;
  initStack(&XMLParseState);
 
  XMLparser =XML_ParserCreate(NULL);
  if (NULL != XMLparser)
  {
    XMLParseState.eXpatXMLParser =XMLparser;

    XML_SetUserData(XMLparser, &XMLParseState);
    XML_SetElementHandler(XMLparser, startElementCallback, endElementCallback);
    XML_SetCharacterDataHandler(XMLparser, elementTextCallback);

    do
    {
      size_t  len = fread(XMLFileReadBuffer, 1, sizeof(XMLFileReadBuffer), fileToParse);
      done = len < sizeof(XMLFileReadBuffer);
      if (XML_Parse(XMLparser, XMLFileReadBuffer, len, done) == XML_STATUS_ERROR)
      {
        fprintf(stderr,
                "%s at line %ld\n",
                XML_ErrorString(XML_GetErrorCode(XMLparser)),
                XML_GetCurrentLineNumber(XMLparser));
        XMLParseState.errorEncountered =1;
        break;
      }
    } while (!done);

    XML_ParserFree(XMLparser);
  }

  if (0 == XMLParseState.errorEncountered)
  {
    printf("XML file parsing successful, %u matches found for %s\n",
           XMLParseState.numberMatchesFound, XMLParseState.lastNameToMatch);
  }
}


/* This code was compiled as a standalone executable, so it needs a main() */
int main(int argc, char* argv[])
{
  int  rv =0;
  FILE*  xmlFileToWorkOn =NULL;

  if (2 == argc)
  {
    xmlFileToWorkOn =fopen(argv[1], "r");
    if (NULL != xmlFileToWorkOn)
    {
      ParseXMLToExtractAddressesForLastName( xmlFileToWorkOn, "Smith" );
      fclose(xmlFileToWorkOn);
      xmlFileToWorkOn =NULL;
    }
    else
    {
      fprintf(stderr, "unable to open XML file %s for read\n", argv[1]);
      rv =2;
    }
  }
  else
  {
    fprintf(stderr, "You must supply a parameter to indicate which XML file to parse\n");
    rv =1;
  }

  return rv;
}