Subprocedure Parameters, part 4 - validating parameters

Now that you've seen how the parameter list of a typical subprocedure is defined, it's time to see how the subprocedure actually works with the parameter list.

Think of a call to your subprocedure as being like your in-laws or other relatives arriving at your house, with plans to stay for a week or so. There's usually a great deal of hustle and bustle. You can quickly see that they've brought any required items with them (such as eyeglasses and other things they're wearing). You then check to see if they've brought other necessities. For example, if they forgot to bring their own toothpaste, they can usually use yours (you can provide a default value for them). If they forgot their toothbrush, you probably won't provide your own toothbrush as a default. If you don't have a spare handy, you might need to go get one from the local WalMart.

In the same way, when someone calls your subprocedure, you should think of it is a major event. Think of what you'll be coding: subprocedures in modules that will be the basis of service programs. You have no idea how or where the service program will be used in the future. If it is a useful service program, it may be around for a long time. Lots of different programs and applications will be using it. At this point, you don't know what those programs or applications will do with your subprocedure, so you need to put in all of your parameter validation, testing and default handling at this point.

This is a way to soften you up for the code I'll be describing in this article. As you'll see, there is a great deal of simplistic code that, in the end, doesn't do very much. However, if you don't want to provide code like this (or code that provides similar functionality), then you'll need to stick with required procedure parameters. An excuse of laziness, as in "I don't want to write all of that code right now", is a poor reason to condemn your procedures to long term inflexibility. You might as well put the required code in when you first create the procedure. That is one of the reasons why I start creating shell procedures as soon as possible, so that I can work on the validation code early in the development process. 

The good news is, once you've coded the parameter validations and handled any default parameter values, you can turn your complete attention to writing the code for the body of the subprocedure. You're also well on your way to developing powerful defense mechanisms for your subprocedures, in that you'll be able to easily reject garbage and send it back to the caller.

The sample code

Most of the code for the MESSAGES_getMessage subprocedure is shown here. The code shows how the parameters that are passed to the procedure are handled:



     P MESSAGES_getMessage...
     P                 b                   export


     D MESSAGES_getMessage...
     D                 pi                  likeds(MESSAGES_Message)

     D   p_messageID...
     D                                     like(Types.MSGID)
     D                                     const
     D   p_substData...
     D                                     like(Types.CHAR512)
     D                                     const
     D                                     options(*nopass : *omit)
     D   p_messageFile...
     D                                     like(Types.NAME)
     D                                     const
     D                                     options(*nopass : *omit)
     D   p_messageLibrary...
     D                                     like(Types.NAME)
     D                                     const
     D                                     options(*nopass : *omit)


     D messageFile...
     D                 s                   like(p_messageFile)
     D messageID...
     D                 s                   like(p_messageID)
     D messageLibrary...
     D                 s                   like(p_messageLibrary)
     D substData...
     D                 s                   like(p_substData)

      /free


         //***********************************************************
         // validate / set message ID
         //***********************************************************
         if (validateMessageID(p_messageID) <> MESSAGES_OK);
             MESSAGES_Message.returnCode = MESSAGES_INVALID_MESSAGE_ID;
             return MESSAGES_Message;
         endif;

         messageID = p_messageID;

         MESSAGES_Message.messageID = messageID;


         //***********************************************************
         // set substitution data
         //***********************************************************
         if ((%parms >= 2) and (%addr(p_substData) <> *null));
             substData = p_substData;
         endif;


         //***********************************************************
         // validate / set message file name
         //***********************************************************
         messageFile = MESSAGES_getMessageFile();

         if ((%parms >= 3) and (%addr(p_messageFile) <> *null));
             messageFile = p_messageFile;
         endif;


         if (validateMessageFile(messageFile) <> MESSAGES_OK);
             MESSAGES_Message.returnCode = MESSAGES_INVALID_MESSAGE_FILE;
             return MESSAGES_Message;
         endif;


         //***********************************************************
         // validate / set message library name
         // set to default MESSAGES_LIBRARY_LIBL if blank
         //***********************************************************
         messageLibrary = MESSAGES_getMessageLibrary();


         if ((%parms >= 4) and (%addr(p_messageLibrary) <> *null));
             messageLibrary = p_messageLibrary;
         endif;


         if (validateMessageLibrary(messageLibrary) <> MESSAGES_OK);
             messageLibrary = MESSAGES_LIBRARY_LIBL;
         endif;


         //***********************************************************
         // retrieve message from message file
         //***********************************************************

         // lots of code not shown 


         return MESSAGES_Message;


      /end-free

     P MESSAGES_getMessage...
     P                 e

Parameter definitions

The parameter definitions in the procedure interface are the same as the definitions in the procedure prototype. The parameter definitions were extensively described in previous articles, starting here.

Parameter work fields

Because all of the parameters passed to the procedure are defined as read-only reference (CONST) parameters, I can't change the parameter values within the subprocedure. However, I will need to set values for default and omitted parameters, so I'll need work fields. I define the parameter work fields immediately after the procedure interface parameter list. The work field names are simply the parameter name without the p_ prefix. I define each work field with the LIKE option, setting each work field to be like the parameter. That way, if I change the definition of a parameter, its corresponding work field definition is also changed when the module is recreated.


     D messageFile...
     D                 s                   like(p_messageFile)
     D messageID...
     D                 s                   like(p_messageID)
     D messageLibrary...
     D                 s                   like(p_messageLibrary)
     D substData...
     D                 s                   like(p_substData)

Use a required parameter

The first parameter (p_messageID) is a required parameter. It must be passed when the procedure is called. This is not hard to understand; after all, it does not make sense for me to try to guess what a good default message ID will be, as I am creating this subprocedure to be used in a general purpose message handling module.


         //***********************************************************
         // validate / set message ID
         //***********************************************************
         if (validateMessageID(p_messageID) <> MESSAGES_OK);
             MESSAGES_Message.returnCode = MESSAGES_INVALID_MESSAGE_ID;
             return MESSAGES_Message;
         endif;

         messageID = p_messageID;

         MESSAGES_Message.messageID = messageID;

The code in this snippet checks to see if a value was passed in the p_messageID parameter. For example, a caller may have called the procedure and passed a value of blanks to the first parameter. The validateMessageID subprocedure follows subprocedure names rule #2, so it is obvious that the subprocedure is a private subprocedure in this module. At this point, we don't know exactly what type of validation or how extensive the validation is for the message ID parameter. For example, validateMessageID could simply check for a non-blank value, or it might perform a more complete check and verify that the message ID follows OS/400 message naming rules. In any event, if the return value from the subprocedure is not MESSAGES_OK (a constant that is defined for the MESSAGES module), a return code is set and a return operation is used to exit the subprocedure. This is an example of rejecting a garbage parameter and returning to the caller. Again, because this subprocedure is designed for use in a general purpose message handling module, there is no intelligent default value that I can assign to the message ID. If the caller can't supply a proper message ID, there is no reason to go to any great lengths to try to mitigate their error. Just report it and give it back to them. Because I have "promised" (via the CONST passing convention) to not change the actual parameter that is passed, I can confidently write code like this. There is no argument about where the invalid message ID originates; it is from the caller.

On the other hand, if the message ID parameter is OK, I simply assign the value that was passed to my subprocedure-internal work variable (messageID). The compiler doesn't consider this variable to be anything special, so I can freely modify it like any other variable.

In fact, in this subprocedure, I don't really need the messageID work variable. Since I won't be modifying the parameter value that was passed, I could have just used the parameter throughout the subprocedure. However, I've come to prefer the technique of moving a subprocedure parameter into its corresponding work field as soon as possible, that is, as soon as the parameter has been validated. If you start using the actual parameter all over the place in your subprocedure, you'll quickly resent the CONST restriction, which prevents you from modifying its value. It's a slippery slope from there to using VALUE and finally to abandonding all restraint, and just passing everything by reference. Because I've already explained my parameter passing technique at length, I'll simply end by pointing out that I move all parameters to work fields. As I have explained in other articles, I am completely uninterested in any speculation as to the "performance impact" of moving values to work fields. It is a complete non-issue within the context of this usage.

Handle an optional parameter, no default value

The second parameter is the substitution data for the message. Because this parameter is defined with the *NOPASS and *OMIT options, it is an optional parameter. The code to handle this parameter is as follows:


         //***********************************************************
         // set substitution data
         //***********************************************************
         if ((%parms >= 2) and (%addr(p_substData) <> *null));
             substData = p_substData;
         endif;

This code uses two RPG built-in functions (BIFs) to determine how the parameter was passed:

If a value was passed in the second parameter and the value is not the special value *OMIT, the value is assigned to the subprocedure work variable substData. The question is, what is substData set to if parameter 2 is not passed or if it is passed as *OMIT? The answer is RPG 101: the value is "set" to blanks, which is what it was initialized to when the variable was defined. Because substData is defined as a local variable to the procedure (it is defined inside the procedure), its value is initialized to blanks each time the procedure is called.

The substData variable is an example of an automatic variable, meaning that storage for it is automatically allocated and initialized each time the procedure is called. Because the variable is automatic, its value also "disappears" when the procedure returns. The opposite of an automatic variable is a static variable, which is allocated and initialized the first time the procedure is called. When the procedure returns, the value of the static variable is retained. The next time the procedure is called, the variable is "already there", with its previous value intact. It is rather unusual to use static variables. You should carefully assess whether or not a static variable is appropriate for your requirements before defining a variable as static. In a later article in this series, you'll see how to use module level variables, which can be used instead of static variables in most cases.

With all that, if you are more comfortable with explicitly assigning blanks to the procedure variable, you should go ahead and do so. It is harmless to use the RPG compiler to initialize the field, and it is equally harmless to make the assignment yourself. 

More about %parms

The %parms BIF returns the count of the number of parameters passed to the subprocedure. It includes any parameters that are passed with the special value *OMIT. For example, these three procedure calls would each report 3 as the value returned by %parms:

msg1 = MESSAGES_getMessage('RJL8901' : 'Test data' : 'RJLMSGF');

 

msg1 = MESSAGES_getMessage('RJL9933' : *OMIT : 'RJLMSGF');

 

msg1 = MESSAGES_getMessage('RJL129A' : 'Other data' : *OMIT);

Based on these examples, you can see why I coded the %parms test for the substitution data parameter as %parms >= 2. If I had coded the test like this:


         //***********************************************************
         // set substitution data
         //***********************************************************
         if ((%parms = 2) and (%addr(p_substData) <> *null));

the test would fail for the three valid subprocedure calls shown above.

The rule for a %parms test is, test for the number of the parameter and greater than, not for equality.

More about %addr

Testing a parameter to see if the special value *OMIT was passed is quite ugly. Rather than create a proper BIF to test for that special value, the RPG compiler writers dump you back into low-level land. The actual test to determine if the value passed is *OMIT looks like this:

         if (%addr(p_substData) = *null);

This test is based on the way the compiler arranges for the parameter to be passed from the caller to the subprocedure. Because you can only use *OMIT for a parameter passed by reference (specified by not using a parameter-passing keyword or by using CONST), what is actually passed to the procedure is a pointer to the parameter value. If you use the *OMIT special value in the parameter list, the pointer is set to a null pointer. So if you test the address of (%addr) a subprocedure parameter and find that the address is a null pointer (special value *NULL), the parameter has been omitted.

As they, say, that's more information than you needed to know. In my code, I typically write the test for *NOPASS and *OMIT in the same IF condition, like this:

         if ((%parms >= 2) and (%addr(p_substData) <> *null));

That test says "if a parameter WAS passed AND the value is not *OMIT". In other words, if I have a "live value", meaning real data was passed in the parameter.

Handle an optional parameter with a default value

The third and fourth parameters (message file name and message file library) are handled the same. Both parameters can be passed, not passed, or passed as the special value *OMIT. The rules for handling this type of parameter are simple:

The code for handling the third parameter (message file name) is shown here:


         //***********************************************************
         // validate / set message file name
         //***********************************************************
         messageFile = MESSAGES_getMessageFile();

         if ((%parms >= 3) and (%addr(p_messageFile) <> *null));
             messageFile = p_messageFile;
         endif;


         if (validateMessageFile(messageFile) <> MESSAGES_OK);
             MESSAGES_Message.returnCode = MESSAGES_INVALID_MESSAGE_FILE;
             return MESSAGES_Message;
         endif;

An alternative way of coding the first four statements is as an if/else construct:


         if ((%parms >= 3) and (%addr(p_messageFile) <> *null));
             messageFile = p_messageFile;
         else;
             messageFile = MESSAGES_getMessageFile();
         endif;

The code in this first section is used to assign the default value to the parameter work field, or assign the actual parameter value to the parameter work field if a parameter value was passed.

The default value is obtained from the MESSAGES_getMessageFile subprocedure. This is an example of using an accessor subprocedure, which will be discussed in a later article. Without looking at the accessor subprocedure, you don't really know what the default value is. As it turns out, in this module, MESSAGES_getMessageFile returns a value of blanks if you have not previously set a default message file name. That return value makes sense, as there is no way that I can code the MESSAGES module with a hard-coded default message file name. It's not my job to guess what the message file name should be for my future callers.

After a value is assigned to the parameter work field, it is validated in the validateMessageFile subprocedure. If the parameter value is garbage, it is returned to the caller.

Sometimes you can use an intelligent default value, as shown in the code for the fourth parameter (the message file library):


         //***********************************************************
         // validate / set message library name
         // set to default MESSAGES_LIBRARY_LIBL if blank
         //***********************************************************
         messageLibrary = MESSAGES_getMessageLibrary();


         if ((%parms >= 4) and (%addr(p_messageLibrary) <> *null));
             messageLibrary = p_messageLibrary;
         endif;


         if (validateMessageLibrary(messageLibrary) <> MESSAGES_OK);
             messageLibrary = MESSAGES_LIBRARY_LIBL;
         endif;

In this case, if the messageLibrary variable does not pass the validation test, its value is set to the constant MESSAGES_LIBRARY_LIBL. This technique can be argued either way (it should / should not set the value). In this case, I came down on the side of the most typical expected use.

There are three cases that I considered for the message file library parameter

A lot depends upon the depth of the validation done in the validateMessageLibrary subprocedure. In this case, the validateMessageLibrary subprocedure simply checks for a blank library name. If the name is blank, the parameter work field is set to the constant MESSAGES_LIBRARY_LIBL.

You should consider how "deep" your parameter validation should be when you design a module. Rather than simply checking for a non-blank library name, you might want to validate any of these other factors:

At some point, you have decide when "enough is enough". In my module, I decided to simply check for a blank library name. If the name is blank, the validation fails and the default (MESSAGES_LIBRARY_LIBL) is assigned. If the caller passes in an invalid, non existent or unauthorized library, OS/400 will catch the error when the message file is accessed. In this subprocedure, the code that is not shown calls an ILE CL program that performs the actual retrieval of the message from the message file. If there is an error when attempting to retrieve the message, I return the actual OS/400 error message to the caller. The actual OS/400 error message clearly states the cause of the error (for example, an invalid library name is specified). If I had coded extensive validation code in my subprocedure, I would simply be duplicating code that OS/400 performs when I attempt the message retrieval.

Craig Pelkie
October 31, 2004