Guru: Getting The Message, Part 1
August 1, 2018 Paul Tuohy
Author’s Note: This article was originally published in October 2009. Since then I have worked on many modernization projects with many clients and, in every one of those projects, we have used some form of the contents of this (and the following) article. The content of the article has been updated for free form RPG and some of the coding enhancements that have been introduced, into RPG, since the original publication of this piece.
When we look at modernizing applications (or writing new applications) one of the basic principles is to tier the application — i.e., separate the interface — the business logic and the database processing, the concept being that any of the components can be changed without affecting the others and, more importantly, you can have multiple interfaces making use of the same business logic and database routines.
All well and good, but there are a couple of minor hiccups that have to be handled. What happens when a business logic or database routine hits an error? How does it notify the interface that an error has occurred?
In other words, how do we send messages between the different components when the components have no knowledge of each other?
In our traditional green screen world messaging was tightly integrated between the process logic and the screen. We may have been using the ERRMSG or ERRMSGID keywords in DDS or making use of program message queues and message subfiles. But will a program message queue technique work with a web request or an SQL subprocedure?
In this article (and a following article), I will take a look at a technique that allows for handling messages no matter what the interface. A library containing the code used in these articles is available for download at http://www.systemideveloper.com/downloads/messagesV2.zip
But Before We Begin . . .
I have always been an enormous fan of message files and I intend to keep using them in this “new” structure. I really like the ability of defining second level message text, severity codes and variable parameters.
But one of the things I don’t like about message files is when I see the message ID hard coded in an RPG program. Of course you have to use the message ID in the program, but I prefer to define my message IDs as named constants and place them in a copy member which is included in every program. Therefore, every program has a list of all available message IDs. The following piece of code shows an example of some of these message IDs. I use the convention (common in most programming languages) of all upper case for constant names.
dcl-C ERR_NOTFOUND 'ALL9001'; dcl-C ERR_CHANGED 'ALL9002'; dcl-C ERR_DUPLICATE 'ALL9003'; dcl-C ERR_CONSTRAINT 'ALL9004'; dcl-C ERR_TRIGGER 'ALL9005'; dcl-C ERR_UNKNOWN 'ALL9006'; dcl-C ERR_NOT_NUMBER 'ALL9007'; dcl-C ERR_NOT_DATE ‘ALL9008';
Storing Messages
Bearing in mind that each component of our application cannot have any knowledge of another component, it is not possible to send messages between the components.
Instead, messages are stored and subprocedures are provided to indicate how many messages are currently stored and corresponding subprocedures to retrieve the messages.
How do we store the messages? First inclinations might lead us towards a message queue or a database, but none of these are necessary. We can simply store our messages in a data structure array. All of the message subprocedures will be coded in a single module and the message format data structure array will be maintained in the same module.
This is the format of the message data structure. This data structure is defined in a copy member and included in all programs.
// Format in which error messages are stored. dcl-Ds def_MsgFormat qualified template; msgId char(7); msgText char(80); severity int(10); help char(500); forField char(25); end-Ds;
The message data structure contains the message ID, the first level message text, the message severity, the second level message text, and the name of the field for which the message was stored.
The Message Module
Let’s have a look at the global definitions and each of the message subprocedures.
The global definitions in the message module (shown below) consist of:
- An array of message formats and a count of how many messages are currently stored. I think 200 messages are more than enough but feel free to increase them as required.
- A data structure that identifies the name of the message file that contains our messages.
- Prototypes for the Retrieve Message from Message File (QMHRTVM) and Receive Message from Message Queue (QMHRCVPM) APIs.
**free /include QCpySrc,StdHSpec ctl-Opt NoMain; // To create the required service program... // Current library set to MESSAGES // CRTRPGMOD MODULE(UTILMSGS) // CRTSRVPGM SRVPGM(UTILITY) MODULE(UTIL*) // SRCFILE(UTILITY) TEXT('Utility Procedures') /include utility,putilMsgs dcl-Ds messages LikeDS(Def_MsgFormat) dim(200) inz; dcl-S msgCount int(10); // Message File used for retrieving message dcl-Ds msgF; msgFile char(10) Inz('APPMSGF'); msgFileLib char(10) Inz('MESSAGES'); end-Ds; // Prototype for QMHRTVM API dcl-Pr retrieve_MessageFromMsgF extPgm('QMHRTVM'); msgInfo char(3000) options(*varSize); msgInfoLen int(10) const; formatName char(8) const; msgId char(7) const; msgF char(20) const; replacement char(500) const; replacementLength int(10) const; replaceSubValues char(10) const; returnFCC char(10) const; usec char(256); end-Pr; // Prototype for QMHRCVPM API dcl-Pr receive_Msg extPgm('QMHRCVPM'); msgInfo char(3000) options(*VarSize); msgInfoLen int(10) const; formatName char(8) const; callStack char(10) const; callStackCtr int(10) const; msgType char(10) const; msgKey char(4) const; waitTime int(10) const; msgAction char(10) const; errorForAPI like(APIError); end-Pr;
Clear Messages
The u_clear_Messages() subprocedure simply does what it says on the box — it clears the message format data structure array and sets the message count to zero.
//-------------------------------------------------- // Clear messages dcl-Proc u_clear_Messages export; dcl-Pi *n end-Pi; msgCount = 0; clear messages; end-Proc;
Add Messages
The u_add_Message() subprocedure adds the required message to the message format data structure array and increments the message count.
//-------------------------------------------------- // Add Message - Add a pre-defined message to the message list // - Field Name is optional and may be omitted // - Message data is optional dcl-Proc u_add_Message export; dcl-Pi *n; msgId char(7) const; forFieldIn char(25) const options(*Omit:*noPass); msgData char(500) const options(*noPass); end-Pi; // Format RTVM0300 for data returned from QMHRTVM dcl-Ds RTVM0300 qualified; bytesreturned int(10); bytesAvail int(10); severity int(10); alertIndex int(10); alertOption char(9); logIndicator char(1); messageId char(7); *n char(3); noSubVarFmts int(10); CCSIDIndText int(10); CCSIDIndRep int(10); CCSIDTextRet int(10); dftRpyOffset int(10); dftRpyLenRet int(10); dftRpyLenAvl int(10); messageOffset int(10); messageLenRet int(10); messageLenAvl int(10); helpOffset int(10); helpLenRet int(10); helpLenAvl int(10); SVFOffset int(10); SVFLenRet int(10); SVFLenAvl int(10); end-Ds; //** reserved //** defaultReply //** message //** messageHelp // Based variable used to retrieve text from RTVM0300 dcl-S textPtr pointer; dcl-S text char(500) Based(textPtr); dcl-S repData char(500); dcl-S forField like(forFieldIn); if %parms() > 2; repData = msgData; endIf; if %parms() > 1; if %addr(forFieldIn) <> *null; forField = forFieldIn; endIf; endIf; retrieve_MessageFromMsgF(RTVM0300 :%Len(RTVM0300)+350 :'RTVM0300' :MsgId :MsgF :RepData :%Len(%Trim(RepData)) :'*YES' :'*YES' :APIError); msgCount += 1; messages(msgCount).msgId = msgId; messages(msgCount).forField = forField; if (APIError.bytesAvail = 0); messages(msgCount).severity = RTVM0300.severity; if (RTVM0300.messageLenRet > 0); textPtr = %Addr(RTVM0300) + RTVM0300.messageOffset; messages(msgCount).msgText = %SubSt(text: 1: RTVM0300.messageLenRet); endIf; if (RTVM0300.helpLenRet > 0); textPtr = %Addr(RTVM0300) + RTVM0300.helpOffset; messages(msgCount).help = %SubSt(Text: 1: RTVM0300.helpLenRet); endIf; else; messages(msgCount).severity = 99; messages(msgCount).msgText = '*** Expected Message Not Found ***'; endIf; end-Proc;
The subprocedure accepts three parameters but only the first (message ID) is required. The second parameter identifies the name of the field to which the message relates and the third parameter contains any variable data for the message.
The subprocedure uses the QMHRTVM API with the RTVM0300 format to retrieve the indicated message from the message file (identified in the msgF data structure in the global definitions). If message data was supplied on the call to addMessage() then the message data is automatically inserted during the retrieve. As you can see, a little bit of pointer manipulation is required to retrieve the message text and the second level text.
Of course, the routine checks to ensure that the requested message exists (not that anyone would ever request a non-existent message ID).
Get Message Count
The u_message_Count() subprocedure simply returns the number of currently stored messages.
//-------------------------------------------------- // Message Count - returns the number of stored messages dcl-Proc u_message_Count export; dcl-Pi *n int(10) end-Pi; return msgCount; end-Proc;
Get A Stored Message
The u_get_Message() subprocedure retrieves the required message indicted by the first parameter. The data returned is a message format data structure. The subprocedure checks that a valid stored message is being requested.
//-------------------------------------------------- // Clear messages //-------------------------------------------------- // Get Message - Get the Message identified by the No. dcl-Proc u_get_Message export; dcl-Pi *n; forMessage int(10) const; msgFormat likeDs(def_MsgFormat); end-Pi; if forMessage > 0 and forMessage <= msgCount; msgFormat = messages(forMessage); else; clear msgFormat; msgFormat.msgText = '*** Message Not Found ***'; endIf; return; end-Proc;
To Be Continued
In my next article we will see how these message subprocedures may be used in an application and we will also look at a couple of other message subprocedures that may be useful.
Paul Tuohy, IBM Champion and author of Re-engineering RPG Legacy Applications, is a prominent consultant and trainer for application modernization and development technologies on the IBM Midrange. He is currently CEO of ComCon, a consultancy firm in Dublin, Ireland, and partner at System i Developer. He hosts the RPG & DB2 Summit twice per year with partners Susan Gantner and Jon Paris.