Club Tech iSeries Programming Tips Newsletter

Article ID: 20090
An iSeries Network Publication           
http://www.iSeriesNetwork.com
Home of iSeries NEWS Magazine                    
Editor: Scott Klement
Issue 200                                               
March 24, 2005
Thanks to this issue's sponsors -- SEQUEL and Linoma Software 

SPONSORED IN PART BY SEQUEL
          * * *  Why You Should RETIRE QUERY/400  * * *                    
      Read the TOP TEN REASONS why SEQUEL is the BETTER way!  
       See video demos or get a FREE trial of SEQUEL today.
http://iseries.pentontech.com/t?ctl=5EDF:103C2

THIS WEEK: 
> Using the Expat XML Parser from an RPG Program, Part 2
> How to Retry an Object Lock
Pro and Pro VIP Edition Items: 
> JSF DataTables Make HTML Lists Easier
> How to Print a Company Logo on an Invoice

1. USING THE EXPAT XML PARSER FROM AN RPG PROGRAM, PART 2

In last week's newsletter, I introduced the open source Expat XML parser. I demonstrated how to create handlers for the starting and ending elements of the XML tags in a document. In this article, I will take it a bit further and demonstrate how to read the character data from an XML document.

MORE ABOUT HANDLERS AND %PADDR()

As demonstrated in the previous article, you register "handlers" with Expat to get the output from the parsing function. These handlers are subprocedures that you write in your program. You tell Expat to call them when it finds a particular thing in the XML source. For example, Expat's XML_SetStartElementHandler() API can be called to register a subprocedure that's called when a starting XML element is encountered in the document.

In response to last week's article, I received questions from readers asking me to explain how handlers work and what the %paddr() BIF does in conjunction with setting a handler.

The handler routines utilize a feature of ILE called a "procedure pointer." Think about how a computer runs a program. That program is a series of machine opcodes and data that are loaded into the computer's memory and executed. If executable code is loaded into memory, it has to have an address, or location, in memory where it can be found. A procedure pointer is a variable that contains this address.

You can use this procedure pointer to call a subprocedure. Consider the following sample program:

     H DFTACTGRP(*NO)

     D TestSubproc     PR
     D    Data                       50A   const

     D MyPointer       s               *   procptr
     D CallPtr         PR                  Extproc(MyPointer)
     D    Message                    50A   const

     C                   eval      MyPointer = %paddr(TestSubproc)
     c                   callp     CallPtr('Olives are good.')
     c                   eval      *inlr = *on

     P TestSubproc     B
     D TestSubproc     PI
     D    Data                       50A   const
     D wait            s              1A
     c     data          dsply                   wait
     P                 E

In this example, CallPtr() will actually call the TestSubproc() subprocedure because MyPointer contains its address. The reason it contains its address is because I used the %paddr() BIF to return the address of the TestSubproc() subprocedure.

To take that a step further, what would happen if you passed MyPointer as a parameter to a subprocedure that's in a service program? Naturally, you'd have to put the prototype for CallPtr() in that service program. Now, TestSubproc() would be called by that service program, since that's what %paddr() references.

Well, that's exactly what Expat does. You tell it which subprocedure to call for a particular event, pass it in the address of the subprocedure that you want it to call using the %paddr() BIF, and it'll call that subprocedure at the appropriate time.

CHARACTER DATA HANDLER

Last week I demonstrated handlers that are called for the starting and ending element of an XML document. Consider the following document:

<?xml version="1.0"?>
<invoice id="54-12343">
   <ShipTo>
     <name>Scott Klement</name>
     <address type="residence">
        <addrLine1>123 Sesame St</addrLine1>
        <city>New York</city>
        <state>NY</state>
        <zipCode>54321</zipCode>
     </address>
   </ShipTo>
. . .
</invoice>

The subprocedure that you registered for the start of an element would be called for the <invoice>, <shipto>, <name>, <address>, etc., XML tags. The subprocedure registered for the end of an element would be called for each of the </name>, </addrLine1>, etc., XML tags.

To get the character data that's located in between these elements, you register a character data handler by calling Expat's XML_SetCharacterDataHandler() API. This handler will be called whenever Expat encounters character data inside an XML tag.

In the example above, it will be called with the string "Scott Klement", then with the string "123 Sesame St", and so on. To try this out, I've created a new program called CHARDATA1 that extends the OUTLINE program from last week. In the new program, I've added a line that sets a character data handler, as follows:

  XML_SetCharacterDataHandler(p: %paddr(chardata));

Here's the code for the CharData() subprocedure that Expat will call when character data is received:

     P chardata        B
     D chardata        PI
     D   data                          *   value
     D   string                   65535A   const options(*varsize)
     D   len                         10I 0 value

     D x               s             10I 0
     D val             s            132A
     D newval          s            132A   varying
      /free
         if (len < 1);
            return;
         endif;

         val = %subst(string:1:len);
         QDCXLATE( len
                 : val
                 : 'QTCPEBC' );

         for x = 1 to len;
            if ( %subst(string:x:1) >= x'40' );
                newval = newval + %subst(val:x:1);
            endif;
         endfor;

         if (%len(newval)<1 or newval = *blanks);
            return;
         endif;

         printme = %subst(blanks: 1: depth)
                 + 'Char: '
                 + newval;
         except print;

      /end-free
     P                 E

Note that there's special code in this subprocedure that strips out any unprintable characters (EBCDIC characters less than x'40' are unprintable) and code that prevents the data from being printed if it's blank. The reason for this is that Expat will call the character data handler for all character data, even if I don't want it to!

If you look back at the XML data that I posted above, there are carriage return and line feed characters following the <invoice> tag. Expat calls the character data handler for those characters, even though I wouldn't want to print them on the outline. Likewise, it calls CharData() for the blank spaces before the <ShipTo> tag, and I don't really want to print those. That's why the CharData() routine will strip them out.

If you run the CHARDATA1 program, the outline will now look like this:

  invoice id="54-12343"
   ShipTo
    name
    Char: Scott Klement
    address type="residence"
     addrLine1
     Char: 123 Sesame St
     city
     Char: New York    
     state
     Char: NY
     zipCode
     Char: 54321

Now that you have a way to receive the character data, you need a way to keep track of the XML element that it belongs to. This is important because the goal of parsing an XML document in RPG is usually to load the fields in the document into variables in your program. To know which variable to load things into, you have to know which tag it belongs to.

Unfortunately, this isn't as simple as it sounds. You can't simply assume that the last tag you received in the starting element handler will be the one that the character data belongs to. Consider the following XML:

<para>
  <title>Life is good</title>
  On March 14, 2005, IBM and COMMON presented Scott Klement with
  the iSeries Innovation Award in the Intellectual category.
</para>

As Expat cycles through this XML document, it'll first call the start element handler for <para> (which is short for "paragraph"). It will then call the start element handler for <title>, then the character data handler with the string "Life is good", then the end element handler for </title>, and finally the character data handler with the text of the paragraph.

The problem is, when you get the text for the paragraph, it should belong to the <para> element. But the last element that the start handler was called for was actually the <title> element!

To keep track of the elements properly, you have to implement a stack. Each time the start element handler is called, you have to put a new element on the stack. Each time the end element handler is called, you have to take the top element off of the stack so that the top element now points to the previous tag again.

The easiest way to do this in RPG is with an array and a variable that keeps track of which array element is on "top" of the stack. To do that, I've defined the following in the mainline of my CHARDATA2 program:

     D stack           s             50A   varying dim(50)
     D Depth           s             10I 0                     

To add elements to the stack, I've changed my start element handler, a subprocedure called start(), to read as follows:

     P start           B
     D start           PI
     D   data                          *   value
     D   elem                          *   value
     D   attr                          *   dim(32767) 
options(*varsize)

     D elemName        s             50A

      /free

         depth = depth + 1;

         elemName = %str(elem);
         QDCXLATE( %len(%trimr(elemName))
                 : elemName
                 : 'QTCPEBC' );

         stack(depth) = %trimr(elemName);

      /end-free
     P                 E

If you look at the code, above, you'll see that each time a starting element is found in the XML document, it adds a new level to the stack by adding 1 to the DEPTH variable. The element name can then be stored in the array at this new depth. When the ending element handler is called, all I have to do to remove that element name from the stack is subtract 1 from the DEPTH variable, as follows:

     P end             B
     D end             PI
     D   data                          *   value
     D   elem                          *   value
      /free
          depth = depth - 1;
      /end-free
     P                 E

I've modified my character data handler so that it'll print out each element in the XML document and its value. It does this by taking the element name that's currently on top of the stack and printing it next to the value that it receives for the character data. It now looks like this:

     P chardata        B
     D chardata        PI
     D   data                          *   value
     D   string                   65535A   const options(*varsize)
     D   len                         10I 0 value

     D x               s             10I 0
     D val             s            132A
     D newval          s            132A   varying
      /free
         if (len < 1);
            return;
         endif;

         val = %subst(string:1:len);
         QDCXLATE( len
                 : val
                 : 'QTCPEBC' );

         newval = '';
         for x = 1 to len;
            if ( %subst(val:x:1) >= x'40' );
                newval = newval + %subst(val:x:1);
            endif;
         endfor;

         if (%len(newval)<1 or newval = *blanks);
            return;
         endif;

         printme = stack(depth) + ' = ' + newval;
         except print;

      /end-free
     P                 E

An excerpt from the output of this routine follows:

   name = Scott Klement
   addrLine1 = 123 Sesame St
   city = New York
   state = NY
   zipCode = 54321

If you wanted to, you could add code to the CharData() routine that would map these values into variables so that you could put them on a screen, use them for calculations, or do whatever you need to do. All you'd have to do is add a SELECT group to the CharData() routine. For example, you might do the following:

select;
         when stack(depth) = 'name';
           myNameVar = val;
         when stack(depth) = 'addrLine1';
           myAddrVar = val;
         when stack(depth) = 'city';
           myCityVar = val;
         when stack(depth) = 'state';
           myStateVar = val;
         when stack(depth) = 'zipCode';
           myZipVar = val;
         endsl;

In the next article in this series, I'll take this concept of mapping variables a bit further by explaining how to calculate an "XPath" from the element names and how to eliminate the need for global variables to represent the stack.

You can download the code for this article.

The source code for my iSeries port of the Expat XML parser can be found on my Web site at the following link: scottklement.com/expat/

The previous article in this series can be viewed from the iSeries Network Web site: Using the Expat XML Parser from an RPG Program, Part 1

2. HOW TO RETRY AN OBJECT LOCK

Q: Recently we received error CPF4128 while running an RPG program that generates reports. It's a simple program that opens a logical file for input only, reads it, and outputs it to a printer file. This is the system error that it receives:

  Not able to allocate objects needed for file GLTRANL1 in library 
  MYLIB member or program device GLTRANL1.

When we receive this error, we usually answer it with a "C" for cancel, and then the report won't be generated. How can we solve this? Is there a way to make it retry? If I wanted to run this program in batch, is there a way to make it retry automatically?

A: Whenever you access any type of object on the iSeries, you place some sort of "lock" on the object. These locks affect what other people can do with the object while you're using it.

For example, when you open a file for reading, you put a "shared" lock on the file. This means that other jobs on the system are allowed to share the file with you, but are not allowed to do something that would interfere with that sharing, such as deleting the file.

What's happening in your case is that another job has gained exclusive use of the file. It might be deleting the file in order to re-build it, or something else that can only be done with nobody else is using the file.

To handle this problem in your program, what you need to do is pre- allocate the file. This involves manually placing a lock on the object with the ALCOBJ command. If that fails, try again until it's successful. Once it's successful, you can run your RPG report program without worries.

For example, the following CL program tries every second to see if it can get a successful shared lock on the file named GLTRANL1. If it can't, it puts a message on the screen so the user knows what he's waiting for. As soon as the file is available, it'll run the RPG program. It will deallocate the file when it's done:

PGM

TRYAGAIN:
       ALCOBJ OBJ((GLTRANL1 *FILE *SHRRD *FIRST)) WAIT(1)
       MONMSG MSGID(CPF1002) EXEC(DO)
             SNDPGMMSG  MSGID(CPF9897) MSGF(QCPFMSG) MSGDTA('Waiting +
                          for access to GLTRANL1 file') +
                          TOPGMQ(*EXT) MSGTYPE(*STATUS)
          GOTO TRYAGAIN
       ENDDO

       CALL PGM(MYRPGPGM)

       DLCOBJ OBJ((GLTRANL1 *FILE *SHRRD *FIRST))

ENDPGM

The *FIRST value that I used on the ALCOBJ command tells it to check the first member in the file. That means that even if other jobs are gaining exclusive use through the physical file or a different logical file, this will still catch the problem and allow it to keep retrying.

There may be times when you want the program to notify you that it's been hung up for a long time, since this may indicate a problem. It's easy to keep track of how long it's been waiting and send you a message after two minutes have passed.

The following code demonstrates how to do this:

PGM

       DCL VAR(&SECS) TYPE(*DEC) LEN(5 0) VALUE(0)

TRYAGAIN:
       ALCOBJ OBJ((GLTRANL1 *FILE *SHRRD *FIRST)) WAIT(1)
       MONMSG MSGID(CPF1002) EXEC(DO)

           SNDPGMMSG  MSGID(CPF9897) MSGF(QCPFMSG) MSGDTA('Waiting +
                          for access to GLTRANL1 file') +
                          TOPGMQ(*EXT) MSGTYPE(*STATUS)

           CHGVAR VAR(&SECS) VALUE(&SECS + 1)
           IF (&SECS *GE 120) DO
             SNDMSG MSG('GLTRANL1 not available after two +
                     minutes!') TOUSR(KLEMSCOT)
             CHGVAR VAR(&SECS) VALUE(0)
           ENDDO

           GOTO TRYAGAIN
       ENDDO

       CALL PGM(MYRPGPGM)

       DLCOBJ OBJ((GLTRANL1 *FILE *SHRRD *FIRST))

ENDPGM

Naturally, you could substitute the SNDDST command for the SNDMSG command if you wanted to send e-mail instead. The important part is to notify someone who will know how to fix the problem so that a batch job doesn't get hung up indefinitely.

***************** ABOUT ISERIES NETWORK NEWSLETTERS ******************

WHY DID I GET ONLY TWO TIPS WHEN FOUR ARE LISTED?

The last two tips in this newsletter are premium content that is 
available only to Pro and Pro VIP members of the iSeries Network. 

If you're not a Pro or Pro VIP member, you will see the titles of 
these two extra tips but will not be able to read their content. 
Please consider upgrading your membership. If you do, you will receive 
not only the two bonus tips but also a subscription to iSeries NEWS 
magazine -- and the cost can be as low as USD $29/year!

More info about the benefits is here:
http://systeminetwork.com/about-the-network

The Subscribe/Join page is here:
http://systeminetwork.com/sinetwork/subscriptions

If you're a Pro or Pro VIP member and you're not getting the extra 
tips, contact Customer Service at mailto:service@systeminetwork.com

***************** ABOUT ISERIES NETWORK NEWSLETTERS ******************

Club Tech iSeries Programming Tips Newsletter is published weekly on 
Thursday, except for the first Thursday of the month. Club Tech 
iSeries Systems Management Newsletter is published on alternate 
Wednesdays. NEWS Wire Daily brings you iSeries industry news, tech 
tips, product news, and IBM announcements Monday through Thursday. 
All are *FREE OF CHARGE*!

IF YOU HAVE a technical question, please submit it to 
mailto:programmingtips@systeminetwork.com or post it in the 
appropriate iSeriesNetwork.com forum. This issue of the Programming 
Tips newsletter was edited by Scott Klement, at 
mailto:programmingtips@systeminetwork.com 

FOR NEW SUBSCRIPTIONS, you can subscribe by joining the iSeries 
Network with our handy Web form at 
http://systeminetwork.com/newsletters


IF YOU HAVE PROBLEMS subscribing or unsubscribing or have questions 
about your subscription, e-mail customer service at 
mailto:service@systeminetwork.com

IF YOU WANT TO SPONSOR a Club Tech iSeries Programming Tips 
Newsletter, please contact your iSeries Network sales manager. Click 
here for details: 
http://systeminetwork.com/advertise

iSeries is a trademark of International Business Machines (IBM) 
Corporation and is used by Penton Media, Inc., under license. iSeries 
NEWS, iSeriesNetwork.com, and the iSeriesNetwork.com newsletters are 
published independently of IBM, which is not responsible in any way 
for the content. Penton Media, Inc., is solely responsible for the 
editorial content and control of iSeries NEWS, iSeriesNetwork.com, and 
the iSeriesNetwork.com newsletters.
___________________________
Copyright 2005
Penton Technology Media, 221 E. 29th St., Loveland, CO 80538 
(800) 793-5714 or (970) 203-2894
http://www.iSeriesNetwork.com

ProVIP Sponsors

ProVIP Sponsors