Getting the Message, Part 1
October 14, 2009 Paul Tuohy
When we look at modernizing (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 is 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.
That’s 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 series of articles, I will take a look at a technique that allows for handling messages between any interface. A library containing the code used in these articles is available for download here.
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 to define 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 that is included in every program. Therefore, every program has a list of all available message IDs. Figure 1 shows an example of some of these message IDs. I use the convention (common in most programming languages) of all uppercase for constant names.
D ERR_NOTFOUND C 'ALL9001' D ERR_CHANGED C 'ALL9002' D ERR_DUPLICATE C 'ALL9003' D ERR_CONSTRAINT C 'ALL9004' D ERR_TRIGGER C 'ALL9005' D ERR_UNKNOWN C 'ALL9006' D ERR_NOT_NUMBER C 'ALL9007' D ERR_NOT_DATE C 'ALL9008'
Figure 1: Defining message IDs as constants.
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 toward a message queue or a database file; but neither of these is 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.
Figure 2 shows the format of the message data structure. The data structure is defined in a copy member that is included in all programs. In V6R1, use the TEMPLATE keyword as opposed to the QUALIFIED and BASED keywords.
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.
// Format in which error messages are stored. D def_MsgFormat Ds Qualified D Based(dummy_Ptr) D msgId 7a D msgText 80a D severity 10i 0 D help 500a D forField 25a
Figure 2: The message format.
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 are shown in Figure 3. The global definitions consist of:
/Copy QCpySrc,StdHSpec H NoMain // To create the required service program... // Current library set to MESSAGES // CRTRPGMOD MODULE(UTILMSGS) // CRTSRVPGM SRVPGM(UTILITY) MODULE(UTIL*) /Copy QCpySrc,BaseInfo D messages DS LikeDS(Def_MsgFormat) D Dim(200) D Inz D msgCount S 10i 0 // Message File used for retrieving message D msgF Ds D msgFile 10a Inz('APPMSGF') D msgFileLib 10a Inz('MESSAGES') // Prototype for QMHRTVM API D RetrieveMessageFromMsgF... D PR ExtPgm('QMHRTVM') D msgInfo 3000a Options(*VarSize) D msgInfoLen 10i 0 Const D formatName 8a Const D msgId 7a Const D msgF 20a Const D replacement 500a Const D replacementLen... D 10i 0 Const D replaceSubVals... D 10a Const D returnFCC 10a Const D usec 256a // Prototype for QMHRCVPM API D receiveMsg PR ExtPgm('QMHRCVPM') D msgInfo 3000a Options(*VarSize) D msgInfoLen 10i 0 Const D formatName 8a Const D callStack 10a Const D callStackCtr 10i 0 Const D msgType 10a Const D msgKey 4a Const D waitTime 10i 0 Const D msgAction 10a Const D errorForAPI Like(APIError) // Prototype for internal send constraint message // procedure D sendConstraintMsg... D PR
Figure 3: Global definitions.
The clearMessages() subprocedure (shown in Figure 4) simply does what it says on the box–it clears the message format data structure array and sets the message count to zero.
P clearMessages B Export D PI /free msgCount = 0; clear messages; /end-Free P E
Figure 4: The clearMessages() subprocedure.
The addMessage() subprocedure (shown in Figure5) adds the required message to the message format data structure array and increments the message count. 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 nonexistent message ID).
P addMessage B Export D PI D msgId 7a Const D forFieldIn 25a Const D Options(*Omit:*NoPass) D msgData 500a Const D Options(*NoPass) // Format RTVM0300 for data returned from QMHRTVM D RTVM0300 Ds Qualified D bytesreturned 10i 0 D bytesAvail 10i 0 D severity 10i 0 D alertIndex 10i 0 D alertOption 9a D logIndicator 1a D messageId 7a D 3a D noSubVarFmts 10i 0 D CCSIDIndText 10i 0 D CCSIDIndRep 10i 0 D CCSIDTextRet 10i 0 D dftRpyOffset 10i 0 D dftRpyLenRet 10i 0 D dftRpyLenAvl 10i 0 D messageOffset 10i 0 D messageLenRet 10i 0 D messageLenAvl 10i 0 D helpOffset 10i 0 D helpLenRet 10i 0 D helpLenAvl 10i 0 D SVFOffset 10i 0 D SVFLenRet 10i 0 D SVFLenAvl 10i 0 D*** reserved D*** defaultReply D*** message D*** messageHelp // Based variable used to retrieve text from RTVM0300 D textPtr S * D text S 500a Based(textPtr) D repData S 500a D forField S like(forFieldIn) /free if %Parms() > 2; repData = msgData; endIf; if %Parms()> 1; if %Addr(ForFieldIn) <> *Null; forField = forFieldIn; endIf; endIf; retrieveMessageFromMsgF(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-Free P E
Figure 5: The addMessage() subprocedure.
Get Message Count
The messageCount() subprocedure (shown in Figure 6) simply returns the number of currently stored messages.
P messageCount B Export D PI 10i 0 /free return msgCount; /end-Free P E
Figure 6: The messageCount() subprocedure.
Get a Stored Message
The getMessage() subprocedure (shown in Figure 7) retrieves the required message indicted by the first parameter. The data returned is a message format data structure. Of course, the subprocedure checks that a valid stored message is being requested.
P getMessage B Export PI D forMessage 10i 0 Const D msgFormat LikeDs(def_MsgFormat) /free if forMessage > 0 and forMessage <= msgCount; msgFormat = messages(forMessage); else; clear msgFormat; msgFormat.msgText = '*** Message Not Found ***'; endIf; return; /end-Free P E
Figure 7: The getMessage() subprocedure.
To Be Continued
In part two of this series, I will demonstrate how these message subprocedures may be used in an application. I will also look at a couple of other message subprocedures that may be useful.
Paul Tuohy is CEO of ComCon, an iSeries consulting company, and is one of the co-founders of System i Developer, which hosts the RPG & DB2 Summit conferences. He is an award-winning speaker who also speaks regularly at COMMON conferences, and is the author of “Re-engineering RPG Legacy Applications,” “The Programmers Guide to iSeries Navigator,” and the self-study course called “iSeries Navigator for Programmers.” Send your questions or comments for Paul to Ted Holt via the IT Jungle Contact page.