Retrieve, display, and generally work with messages your way.
In the last article, "Reducing the Use of Compile-Time Arrays," we looked at using the Retrieve Message API QMHRTVM to randomly access the first-level message text associated with a message description. In this article, we'll look at an optional parameter of the QMHRTVM API. This parameter, Retrieve option, enables us to sequentially read messages from a message file in addition to random access to messages. We'll use this capability to write a sample program that finds all messages, within a specified message file, that contain one or more specific words (or more accurately, a sequence of letters) within the first-level text of the message.
The actual "find" function will be implemented in the next article due to space reasons. This article will provide a basic message description work-with program, one that we will then enhance next month. Today's program will accept a qualified message file name and then display the messages found within the message file. From this display, you can change one or more message descriptions, display the details of one or more message descriptions, and/or remove one or more message descriptions. The APIs that we will be using initially are these:
- Retrieve Message (QMHRTVM)—Depending on the function being performed, the API will retrieve the first message description from a message file, the next message description following a previously read message description, or a random message description.
- Send Program Message (QMHSNDPM)—Send an escape message in an error condition.
- Process Commands (QCAPCMD)—Depending on the function being performed, run the CL command Change Message Description (CHGMSGD), Display Message Description (DSPMSGD), or Remove Message Description (RMVMSGD).
- Retrieve Object Description (QUSROBJD)—Resolve qualified message file names containing special values (such as *CURLIB and *LIBL) to actual library names.
The provided sample program is functional though hardly complete. For instance, there is no checking for when the subfile displaying the found message descriptions is full (more than 9,999 message descriptions within a message file), a message file not found condition results in an escape message being sent rather than a recoverable error message being shown to the user, etc. The sample program is, however, sufficient to demonstrate the use of various APIs.
To run the Find Messages (FNDMSGS) program, we will create a command also called FNDMSGS. Using the message file TEXTMSGS first created in the article "Still Using Compile-Time Arrays?," run the following four Add Message Description commands.
ADDMSGD MSGID(CMD0001) MSGF(TEXTMSGS) MSG('Find Messages')
ADDMSGD MSGID(CMD0002) MSGF(TEXTMSGS) MSG('Message file')
ADDMSGD MSGID(CMD0003) MSGF(TEXTMSGS) MSG('Library')
ADDMSGD MSGID(DSP5001) MSGF(TEXTMSGS) MSG('*** Message Removed ***')
The first three message descriptions are used as prompt text in creating the FNDMSGS command. The fourth message, DSP5001, is used by the FNDMSGS command processing program to indicate that the user has removed a message from the currently displayed message file.
This is the source for the FNDMSGS command:
CMD PROMPT(CMD0001)
PARM KWD(MSGF) TYPE(QUALNAME) PROMPT(CMD0002)
QUALNAME: QUAL TYPE(*NAME) LEN(10) DFT(TEXTMSGS)
QUAL TYPE(*NAME) DFT(*LIBL) SPCVAL((*LIBL) +
(*CURLIB)) PROMPT(CMD0003)
To create the FNDMSGS command, you can use this command:
CRTCMD CMD(FNDMSGS) PGM(FNDMSGS) PMTFILE(TEXTMSGS)
The FNDMSGS program uses a *DSPF, named FNDMSGSDF, to provide a work-with interface to the user. The source for the *DSPF follows:
CA03(03 'Exit')
CF05(05 'Refresh')
R MSG_SFL SFL
OPTION 1 B 9 3VALUES(' ' '2' '4' '5')
40 DSPATR(PC)
MSGID 7 O 9 6
MSGTXT 60 O 9 14
R MSG_CTL SFLCTL(MSG_SFL)
OVERLAY
SFLPAG(12)
SFLSIZ(13)
31 SFLDSP
N32 SFLDSPCTL
32 SFLCLR
35 SFLEND
N35 PAGEDOWN(41)
SFL_DSPPAG 4S 0H SFLRCDNBR
1 26'Work with Found Messages'
COLOR(WHT)
3 2'Message file:'
MSG_FILE 10 B 3 18
3 33'Library:'
MSG_LIB 10 B 3 44
5 2'Type options, press Enter.'
COLOR(BLU)
6 4'2=Change'
COLOR(BLU)
6 15'4=Delete'
COLOR(BLU)
6 26'5=Display message'
COLOR(BLU)
8 2'Opt'
COLOR(WHT)
8 6'MsgID'
COLOR(WHT)
8 14'Message text'
COLOR(WHT)
R MSG_KEYS
23 2'F3=Exit'
COLOR(BLU)
23 15'F5=Refresh'
COLOR(BLU)
To create the *DSPF, use this command:
CRTDSPF FILE(FNDMSGSDF) SRCFILE(QDDSSRC)
The command processing program, FNDMSGS, is shown below. Note that the referenced binding directory, MSGSPTBD, was created in the previous article "Reducing the Use of Compile-Time Arrays."
h dftactgrp(*no) bnddir('MSGSPTBD')
fFndMsgsDF cf e workstn sfile(Msg_Sfl :MsgSflRRN1)
dFndMsgs pr
d QualMsgF_in 20a const
dFndMsgs pi
d QualMsgF_in 20a const
dGetNewMsgF pr
dGetNxtMsgs pr
d NbrMsgsToGet 5u 0 const
dGetCurMsg pr
d MsgID 7a const
dRtvMsgTxt pr
dMsgRcv 65535a options(*varsize)
dLenMsgRcv 10i 0 const
dMsgID 7a const
dRunCmd pr n
d Cmd 512a const
dSndEscape pr
dSetUp pr
dRtvMsgAPI pr extpgm('QMHRTVM')
d MsgInfo 65535a options(*varsize)
d LenMsgInfo 10i 0 const
d Format 8a const
d MsgID 7a const
d QualMsgF 20a const
d RplDta 65535a const
d LenRplDta 10i 0 const
d RplOpt 10a const
d RtnFmtOpt 10a const
d ErrCde likeds(QUSEC)
d RtvOpt 10a const options(*nopass)
d ToCCSID 10i 0 const options(*nopass)
d RplDtaCCSID 10i 0 const options(*nopass)
dSndMsg pr extpgm('QSYS/QMHSNDPM')
d MsgID 7 const
d QualMsgF 20 const
d MsgDta 65535 const options(*varsize)
d LenMsgDta 10i 0 const
d MsgType 10 const
d CallStackEntry 65535 const options(*varsize)
d CallStackCntr 10i 0 const
d MsgKey 4
d QUSEC likeds(QUSEC)
d LenCSE 10i 0 const options(*nopass)
d CSEQual 20 const options(*nopass)
d DSPWaitTime 10i 0 const options(*nopass)
d CSEType 10 const options(*nopass)
d CCSID 10i 0 const options(*nopass)
dProcessCmd pr extpgm('QCAPCMD')
d SourceCmd 65535a const options(*varsize)
d LenSrcCmd 10i 0 const
d CtlBlk 65535a const options(*varsize)
d LenCtlBlk 10i 0 const
d Format 8a const
d ChgCmd 1a options(*varsize)
d LenAvlChgCmd 10i 0 const
d LenRtnChgCmd 10i 0
d QUSEC likeds(QUSEC)
dRtvObjD pr extpgm('QUSROBJD')
d ObjInfo 65535a options(*varsize)
d LenObjInfo 10i 0 const
d Format 8a const
d QualObjN 20a const
d ObjType 10a const
d QUSEC likeds(QUSEC) options(*nopass)
d ASPCtl 65535a const options(*nopass)
/copy qsysinc/qrpglesrc,qmhrtvm
/copy qsysinc/qrpglesrc,qcapcmd
/copy qsysinc/qrpglesrc,qusrobjd
/copy qsysinc/qrpglesrc,qusec
dRcvVar ds qualified
d API likeds(QMHM030000)
d MsgDta 4096a
dObjInfo ds qualified
d API likeds(QUSD0100)
dQualMsgF ds
d Msg_File 10
d Msg_Lib 10
dErrCde ds qualified
d Common likeds(QUSEC)
d MsgTxt 512
dMsgSflRRN1 s 4 0
dTopSflRRN1 s 4 0
dSizeOfSFLPage s 5u 0 inz(12)
dTopMsgID s 7a
dRun s n
dUserAction s n
dPrvMsg_File s like(Msg_File)
dPrvMsg_Lib s like(Msg_Lib)
dNotUsedChr s 1
dNotUsedInt s 10i 0
/free
SetUp();
dow (not *in03);
select;
when ((Msg_File <> PrvMsg_File) or
(Msg_Lib <> PrvMsg_Lib));
GetNewMsgF();
when *in05;
GetNewMsgF();
when *in41;
GetNxtMsgs(SizeOfSFLPage);
when TopSflRRN1 > 0;
UserAction = *off;
readc Msg_Sfl;
dow (not %eof(FndMsgsDF));
select;
when Option = '2';
GetCurMsg(MsgID);
Run = RunCmd('?CHGMSGD ?*MSGID(' + MsgID +
') ?*MSGF(' + %trimr(Msg_Lib) +
'/' + %trimr(Msg_File) +
') MSG(''' +
%subst(RcvVar
:(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04) +
''')');
if Run;
GetCurMsg(MsgID);
MsgTxt = %subst(RcvVar
:(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04);
endif;
UserAction = *on;
when Option = '4';
Run = RunCmd('RMVMSGD MSGID(' + MsgID +
') MSGF(' + %trimr(Msg_Lib) +
'/' + %trimr(Msg_File) + ')');
if Run;
RtvMsgTxt(MsgTxt :%size(MsgTxt)
:'DSP5001');
endif;
UserAction = *on;
when Option = '5';
Run = RunCmd('DSPMSGD RANGE(' + MsgID +
') MSGF(' + %trimr(Msg_Lib) +
'/' + %trimr(Msg_File) + ')');
UserAction = *on;
endsl;
if *in03;
leave;
endif;
Option = *blanks;
update Msg_Sfl;
readc Msg_Sfl;
enddo;
if (not UserAction);
leave;
endif;
other;
// Nothing left to do
endsl;
write Msg_Keys;
*in31 = (TopSflRRN1 > 0);
exfmt Msg_Ctl;
enddo;
*inlr = *on;
return;
/end-free
pGetNxtMsgs b
dGetNxtMsgs pi
d NbrMsgsToGet 5u 0 const
dmyX s 5u 0
/free
if TopMsgID <> RcvVar.API.QMHMID;
// Need to refresh the first message to retrieve
GetCurMsg(TopMsgID);
endif;
*in40 = *on;
MsgSflRRN1 = TopSflRRN1;
for myX = 1 to NbrMsgsToGet;
MsgID = RcvVar.API.QMHMID;
MsgTxt = %subst(RcvVar :(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04);
MsgSflRRN1 += 1;
write Msg_Sfl;
*in40 = *off;
RtvMsgAPI(RcvVar :%size(RcvVar) :'RTVM0300'
:MsgID :QualMsgF
:' ' :0 :'*NO' :'*NO' :QUSEC
:'*NEXT' :0 :0);
if RcvVar.API.QMHBAVL09 = 0;
// End of message descriptions if bytes avail = 0
*in35 = *on;
leave;
else;
TopMsgID = RcvVar.API.QMHMID;
endif;
endfor;
TopSflRRN1 = MsgSflRRN1;
Sfl_DspPag = MsgSflRRN1;
/end-free
pGetNxtMsgs e
*
pGetNewMsgF b
dGetNewMsgF pi
/free
*in32 = *on;
write Msg_Ctl;
*in32 = *off;
MsgSflRRN1 = 0;
TopSflRRN1 = 0;
*in35 = *off;
RtvMsgAPI(RcvVar :%size(RcvVar) :'RTVM0300'
:' ' :QualMsgF
:' ' :0 :'*NO' :'*NO' :ErrCde
:'*FIRST' :0 :0);
select;
when ErrCde.Common.QUSBAvl > 0;
SndEscape();
when RcvVar.API.QMHBAVL09 = 0;
// No messages in message file
*in35 = *on;
other;
TopMsgID = RcvVar.API.QMHMID;
GetNxtMsgs(SizeOfSFLPage);
endsl;
if %subst(Msg_Lib :1 :1) = '*';
// Resolve special values like *LIBL and *CURLIB
RtvObjD(ObjInfo :%size(ObjInfo) :'OBJD0100'
:QualMsgF :'*MSGF' :QUSEC);
Msg_Lib = ObjInfo.API.QUSRL01;
endif;
PrvMsg_File = Msg_File;
PrvMsg_Lib = Msg_Lib;
/end-free
pGetNewMsgF e
*
pGetCurMsg b
dGetCurMsg pi
d MsgID 7a const
/free
RtvMsgAPI(RcvVar :%size(RcvVar) :'RTVM0300'
:MsgID :QualMsgF
:' ' :0 :'*NO' :'*NO' :QUSEC
:'*MSGID' :0 :0);
/end-free
pGetCurMsg e
*
pRunCmd b
dRunCmd pi n
d Cmd 512a const
/free
ProcessCmd(Cmd :%len(%trimr(Cmd))
:QCAP0100 :%size(QCAP0100) :'CPOP0100'
:NotUsedChr :0 :NotUsedInt :ErrCde);
if ErrCde.Common.QUSBAvl > 0;
select;
when ErrCde.Common.QUSEI = 'CPF6801';
if %subst(ErrCde.MsgTxt :1 :3) = 'F3 ';
*in03 = *on;
return *off;
else;
return *off;
endif;
other;
SndEscape();
endsl;
else;
return *on;
endif;
/end-free
pRunCmd e
*
pSetUp b
dSetUp pi
/free
QUSBPrv = 0;
ErrCde.Common.QUSBPrv = %size(ErrCde);
QCAP0100 = *loval; // init API ctls to nulls
QCACMDPT = 0; // Run command
QCABCSDH = '0'; // Ignore DBCS
QCAPA = '2'; // Prompt if controls found
QCACMDSS = '0'; // Use i OS syntax
QualMsgF = QualMsgF_in;
GetNewMsgF();
write Msg_Keys;
*in31 = (TopSflRRN1 > 0);
exfmt Msg_Ctl;
/end-free
pSetUp e
*
pSndEscape b
dSndEscape pi
dLenMsgDta s 10i 0
dMsgKey s 4a
/free
if ErrCde.Common.QUSBAVL < 16;
LenMsgDta = 0;
else;
LenMsgDta = ErrCde.Common.QUSBAVL - %size(ErrCde.Common);
endif;
SndMsg(ErrCde.Common.QUSEI :'QCPFMSG *LIBL'
:ErrCde.MsgTxt :LenMsgDta
:'*ESCAPE' :'*PGMBDY' :1 :MsgKey :QUSEC);
/end-free
pSndEscape e
The program can be created using the following command:
CRTBNDRPG PGM(FNDMSGS)
To run the program, you can enter a command such as this:
FNDMSGS
This command, which defaults to the qualified message file name *LIBL/TEXTMSGS per the command definition source, allows you to work with the message descriptions found in the specified message file.
The program, upon entry, runs the SetUp procedure. The SetUp procedure does the following:
- Initializes two instances of the API error code structure. One instance, QUSEC, has the Bytes provided field set to 0, indicating that any errors encountered are to be returned as escape messages. The second instance, ErrCde, has the Bytes provided field set to a non-0 value, indicating that any errors encountered are to be returned within the ErrCde structure. By convention, I use the QUSEC instance when I do not intend to provide error recovery and ErrCde when some error handling is being provided by the application.
- Initializes the Options control block of the Process Commands API (QCAPCMD) to run commands with optional prompting.
- Sets the data structure QualMsgF to the value of the FNDMSGS command MSGF keyword parameter.
- Runs the procedure GetNewMsgF.
- Displays up to the first 12 messages (the subfile page size) found in the message file identified by the QualMsgF data structure (these messages are loaded by way of the previously run GetNewMsgF procedure).
- Returns to its caller.
Before seeing what happens when SetUp returns to the mainline, let's review the processing of the procedures called by SetUp. The GetNewMsgF procedure is run whenever a new message file is to be used. This could be when the program is initially called or when the user changes the message file name and/or library name on the display record format MSG_CTL. The GetNewMsgF procedure does the following:
- Clears the subfile and initializes variables MsgSflRRN1 and TopSflRRN1 to 0. MsgSflRRN1 is used to identify the subfile record number currently being processed (the SFILE keyword of the *DSPF file specification); TopSflRRN1 is used to keep track of the current size of the subfile.
- Retrieves the first message description found in the message file identified by the QualMsgF data structure using the ErrCde data structure.
- Enters into a SELECT group. If an error is encountered retrieving the first message description, GetNewMsgF resends the escape message to the user. This is where, if desired, you might inform the user of a problem using a message subfile. If no error is encountered, GetNewMsgF checks the Bytes available variable of the QMHRTVM returned data structure for a value of 0. This indicates that, although the message file was found, there are no message descriptions defined for the message file. In this case, indicator 35 (SFLEND) is set on. If no error is encountered and a non-zero Bytes available variable is received from QMHRTVM, GetNewMsgF sets variable TopMsgID to the value of the message returned and runs the procedure GetNxtMsgs. The TopMsgID variable is used to identify the message ID, which should start the next subfile page when adding subfile entries.
- If the library name of the qualified message file starts with an asterisk, the program calls the Retrieve Object Description API to determine the name of the actual library used.
- Sets the variables PrvMsg_File and PrvMsg_Lib to reflect the resolved name of the message file being used.
- Returns to its caller.
The GetNxtMsgs procedure performs these actions:
- Determines if the message ID identified by the TopMsgID variable is still the message found in the QMHRTVM receiver variable. When called by the GetNewMsgF procedure, this will always be the case. When called by other procedures, the contents of the QMHRTVM receiver variable may or may not reflect the next message to be loaded to the subfile. That is, the QMHRTVM receiver variable may reflect (as you will see later) the last message displayed, changed, or removed by the user. If the QMHRTVM receiver variable does not contain the message description for TopMsgID, GetNxtMsgs runs the GetCurMsg procedure to re-access the message description for the message identified by TopMsgID.
- Sets on indicator 40, which is used to position the cursor to the first entry of the subfile page being loaded.
- Sets the current subfile relative record number (MsgSflRRN1) to the last written subfile relative record number (TopSflRRN1).
- Enters into a FOR loop, which will load the next page of the subfile with message descriptions from the user-specified message file. Within the FOR loop, the procedure moves the message ID and first-level text of the message description currently found in the QMHRTVM receiver variable to the subfile variables MsgID and MsgTxt. It then increments the current subfile relative record number by 1, writes the subfile record, sets off indicator 40, and attempts to retrieve the next message description found in the message file identified by the QualMsgF data structure, using QMHRTVM and the QUSEC error code data structure. If there is no "next" message description (Bytes available of the QMHRTVM receiver variable is 0), then indicator 35 (SFLEND) is set on and the FOR loop is exited. If there is a next message description, then TopMsgID is set to the message ID of the retrieved message and the FOR loop is run again. Upon exiting the FOR loop, TopSflRRN1 is set to reflect the relative record number of the last subfile record written and the subfile page to be displayed is set to be the page containing the last written subfile record. Assuming that the entire subfile page was written (that is, 12 message descriptions were successfully retrieved), the last message description retrieved is not written to the subfile. Rather, it is used as a resumption point for the next call to the GetNxtMsgs and to enable an accurate setting of SFLEND.
- Returns to its caller.
Having reviewed what happened when the SetUp procedure runs, SetUp now returns to the mainline of the FNDMSGS program. The mainline logic is a DO WHILE loop exited with command key 3. Within the DOW is a SELECT group used to determine what processing should be done.
If the user changes the message file name, the message file library name, or both, then the GetNewMsgF procedure is used to clear the current subfile and load a new first subfile page using the newly specified qualified message file name.
If command key 5 (indicator 5) is used, then the GetNewMsgF procedure is used to clear the current subfile and reload a new first subfile page using the current qualified message file name.
If PageDown (indicator 41) is used, then the GetNxtMsgs procedure is used to load the next subfile page of the current qualified message file name.
If none of the previous three conditions occurred, and there are message descriptions loaded in the subfile (TopSflRRN1 > 0), the program then sets indicator UserAction to indicate that no user action has (yet) been detected and enters a DOW to determine if the user has selected to change, remove, or display any of the message descriptions currently loaded in the subfile.
If the user selected option 2 (change), the GetCurMsg procedure is used to re-access the message description for the message identified by the MsgID variable of the read subfile record, and the RunCmd procedure is used to run the CHGMSGD command. The CHGMSGD command is prompted due to the leading question mark (?), rendering the user unable to change the MSGID or MSGF keyword values (the "?*" prompt controls). The first-level text of the message to be changed is included in the prompt, which I find a lot more useful than the default value of *SAME. The RunCmd procedure returns an indicator setting, which is stored in variable Run. If variable Run is set to on after the RunCmd procedure returns, then the program again uses the GetCurMsg procedure, this time to get the (presumably) changed first-level text of the message. This text will later be used to update the current subfile record to reflect the changed message text. As the user has performed some action (even if the user then canceled the prompted request), indicator UserAction is set to on.
The RunCmd procedure uses the Process Command (QCAPCMD) API, discussed in previous columns starting with "Running CL Commands from RPG," to run the command specified by its caller. After running the command, RunCmd checks to see if an error was returned and, if there was, if the error message is CPF6801 (Command prompting ended when user pressed &1). This error can be returned if the CL command (such as the CHGMSGD command) is prompted and the user then uses F3 or F12 to end the command prompt without actually running the command. If F3 was used, RunCmd sets on indicator 3 and returns an "off" indicator with the RETURN operation. If F12 was used, RunCmd simply returns an "off" indicator with the RETURN operation. If any error was returned by the API other than CPF6801, RunCmd resends the escape message using the SndEscape procedure. If no error was returned by the API, then RunCmd returns an "on" indicator with the RETURN operation.
If the user selected option 4 (remove) the RunCmd procedure is used to run the RMVMSGD command (without prompting). If the command runs successfully, then the text of message DSP5001 ("*** Message Removed ***"), added earlier in this column, is retrieved using the RtvMsgTxt procedure defined in the previous article "Reducing the Use of Compile-Time Arrays." This text will be used to later update the current subfile record text. As with the change option, indicator UserAction is also set on to indicate that the user has performed a function.
If the user selected option 5 (display), the RunCmd procedure is used to run the DSPMSGD command, and again the UserAction indicator is set to on.
After running the selected option, a check is made for indicator 3 being on. This is done as the user could have used F3 to exit from the command prompt for CHGMSGD. If indicator 3 is on, the DOW processing of the subfile is exited. If indicator 3 is off, the current subfile record is updated by setting the option value to blanks and writing the current text associated with the message (previously set by the processing associated with either option 2 or option 4). The next changed subfile record is then read and the DOW rerun.
After all changed subfile records have been processed, a check is made to determine if any user action was detected while processing the subfile. If not (variable UserAction is still off), then the outer DOW loop (conditioned by indicator 3) is exited and the program ends. Otherwise, the program redisplays the subfile and waits for the next user request.
That's our basic work-with message descriptions application. Next month, we'll add the ability to find and display only those messages where one or more words are found in the first-level text of the message description. And needless to say, we'll be using an API to help us in this task.
In the meantime, if you have any API questions, send them to me at
as/400, os/400, iseries, system i, i5/os, ibm i, power systems, 6.1, 7.1, V7,
LATEST COMMENTS
MC Press Online