Guru: Getting the Message, Part 2
August 29, 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 2009. The original articles also showed examples of direct calls to RPG subprocedures from PHP. Given that we now have many languages (Node.js, Python etc.) that interact with RPG, I changed the mechanism to make the calls using stored procedures.
In Getting The Message, Part 1,we saw the definition of a number of message routines (clearMessages(), addMessage(), messageCount() and getMessage()). In this article we will see how the routines may be used in both an RPG and a PHP environment. We will also look at another couple of message routines that may be useful.
Remember, a library containing the code used in these articles may be downloaded at http://www.systemideveloper.com/downloads/messagesV2.zip
A Test Procedure
This is a small subprocedure (fillMessages()) used to demonstrate that messages may be added/stored at any level in the job. The subprocedure simply adds a filler message the number of times requested on the passed parameter.
dcl-Proc d_fill_Messages export; dcl-Pi *n; timesToSend int(10) const; end-Pi; dcl-S i int(10); for i = 1 to timesToSend; u_add_Message(APP_FILLER : 'FILL' : %char(i)); endFor; return; end-Proc;
In RPG
This is an RPG program used to demonstrate the message routines. The program performs the following:
- Calls u_clear_Messages() to clear any stored messages
- Calls u_add_Message() twice to add two messages (note the use of the named constants to identify the required message IDs)
- Calls d_fill_Messages() to add three filler messages
- Based on the value returned by u_message_Count(), the program loops through calls to u_get_Message() and displays the message text returned for each message
**free /include QCpySrc,StdHSpec // To create the program... // Current library set to MESSAGES // CRTBNDRPG PGM(SHOWMSGS) /include utility,putilMsgs dcl-ds message likeDS(def_MsgFormat); dcl-s i int(10); u_clear_Messages(); u_add_Message(ERR_NOTFOUND : 'TEST1'); u_add_Message(ERR_CHANGED : 'TEST2'); d_fill_Messages(3); for i = 1 to u_message_Count(); u_get_Message(i : message); dsply %subst(message.msgText :1 :40); endFor; *InLR = *on;
The expected results from SHOWMSGS is:
DSPLY An expected record was not found for upd DSPLY Record already altered. Update/Delete ig DSPLY This is filler message 1 DSPLY This is filler message 2 DSPLY This is filler message 3
Of course this program simply shows the format of the calls. The real key is that the program wants to do with the messages it retrieves.
If this were a green screen program the returned messages could be sent to a message subfile or if it were a CGIDEV2 program the returned messages might be used to populate message information on a web page.
For example, when I am using CGIDEV2 I identify errors on a web page using two divisions/variables. ERRTEXT contains the message text to be displayed and ERRVARS (a non-display division on the page) contains the names of the fields in error – a JavaScript routine that runs on page load uses the contents of ERRVARS to highlight the fields in error. This is the setCGIMessages() routine used to set the errors on a web page.
**free dcl-Proc setCGIMessages export; dcl-pi *n; end-pi; dcl-s i Int(10); dcl-ds msgFormat likeDs(Def_MsgFormat); dcl-s errVars varchar(32767); dcl-s errText varchar(32767); errVars = ' '; errText = ' '; if u_message_Count() > 0; for i = 1 to u_message_Count(); u_get_Message(i: msgFormat); if (i > 1); errVars = errVars + '%%'; errText = errText + '<br />'; endIf; errVars = errVars + %trim(msgFormat.ForField); errText = errText + %trim(msgFormat.MsgText); endFor; endIf; updHTMLvar( 'ErrVars': errVars); updHTMLvar( 'ErrText': errText); end-Proc;
SQL Stored Procedures
In order to make the message subprocedures accessible through SQL, we can wrap them as stored procedures. Unfortunately, SQL stored procedures have a very simple parameter interface and do not provide an easy means of returning values or passing complex structures (data structures). Therefore, we have to write a couple of “wrapper” subprocedures that will be called from the stored procedures.
- SQL_message_Count() is a wrapper for u_message_Count() and returns the number of stored messages as a parameter
- SQL_get_Message() is a wrapper for u_get_Message() and returns the contents of the message data structure as individual parameters
dcl-Proc SQL_message_Count export; dcl-Pi *n; numMessages int(10); end-Pi; numMessages = u_message_Count(); return; end-Proc; dcl-Proc SQL_get_Message export; dcl-Pi *n; forMessage int(10) const; msgId char(7); msgText char(80); severity int(10); help char(500); forField char(25); end-Pi; if (forMessage > 0 and forMessage <= u_message_Count() ); msgId = messages(forMessage).msgId; msgText = messages(forMessage).msgText; severity = messages(forMessage).severity; help = messages(forMessage).help; forField = messages(forMessage).forField; endIf; return; end-Proc;
With all of the required subprocedures in place, we can now create the required SQL stored procedures. Each stored procedure simply calls the required message subprocedure.
-- Create Stored Procedures for Message Functions CREATE OR REPLACE PROCEDURE MESSAGES.D_FILL_MESSAGES ( IN NUMMESSAGES INTEGER ) LANGUAGE RPGLE SPECIFIC MESSAGES.D_FILL_MESSAGES NOT DETERMINISTIC NO SQL CALLED ON NULL INPUT EXTERNAL NAME 'MESSAGES/UTILITY(d_fill_Messages)' PARAMETER STYLE SQL ; GRANT EXECUTE ON SPECIFIC PROCEDURE MESSAGES.D_FILL_MESSAGES TO PUBLIC ; CREATE OR REPLACE PROCEDURE MESSAGES.U_ADD_MESSAGE ( IN MSGID CHAR(7) , IN FORFIELD CHAR(25) , IN MSGDATA CHAR(500) ) LANGUAGE RPGLE SPECIFIC MESSAGES.U_ADD_MESSAGE NOT DETERMINISTIC NO SQL CALLED ON NULL INPUT EXTERNAL NAME 'MESSAGES/UTILITY(u_add_Message)' PARAMETER STYLE SQL ; GRANT EXECUTE ON SPECIFIC PROCEDURE MESSAGES.U_ADD_MESSAGE TO PUBLIC ; CREATE OR REPLACE PROCEDURE MESSAGES.U_CLEAR_MESSAGES ( ) LANGUAGE RPGLE SPECIFIC MESSAGES.U_CLEAR_MESSAGES NOT DETERMINISTIC NO SQL CALLED ON NULL INPUT EXTERNAL NAME 'MESSAGES/UTILITY(u_clear_Messages)' PARAMETER STYLE SQL ; GRANT EXECUTE ON SPECIFIC PROCEDURE MESSAGES.U_CLEAR_MESSAGES TO PUBLIC ; CREATE OR REPLACE PROCEDURE MESSAGES.U_GET_MESSAGE ( IN FORMESSAGE INTEGER , OUT MSGID CHAR(7) , OUT MSGTEXT CHAR(80) , OUT SEVERITY INTEGER , OUT HELP CHAR(500) , OUT FORFIELD CHAR(25) ) LANGUAGE RPGLE SPECIFIC MESSAGES.U_GET_MESSAGE NOT DETERMINISTIC NO SQL CALLED ON NULL INPUT EXTERNAL NAME 'MESSAGES/UTILITY(SQL_get_Message)' PARAMETER STYLE SQL ; GRANT EXECUTE ON SPECIFIC PROCEDURE MESSAGES.U_GET_MESSAGE TO PUBLIC ; CREATE OR REPLACE PROCEDURE MESSAGES.U_MESSAGE_COUNT ( OUT NUMMESSAGES INTEGER ) LANGUAGE RPGLE SPECIFIC MESSAGES.U_MESSAGE_COUNT NOT DETERMINISTIC NO SQL CALLED ON NULL INPUT EXTERNAL NAME 'MESSAGES/UTILITY(SQL_message_Count)' PARAMETER STYLE SQL ; GRANT EXECUTE ON SPECIFIC PROCEDURE MESSAGES.U_MESSAGE_COUNT TO PUBLIC ;
In PHP
The first step was to write PHP functions that would issue calls to the corresponding subprocedures in the UTILTY service program. Each of these functions is simply a PHP function that issues a call to the corresponding stored procedure. In other words, these are PHP wrappers to SQL stored procedures to call the RPG subprocedures.
This is the content of the PHP script func_messages.php.
<?php define('APP_ERR_NOTFOUND','ALL9001'); define('APP_ERR_CHANGED','ALL9002'); define('APP_ERR_DUPLICATE','ALL9003'); define('APP_ERR_CONSTRAINT','ALL9004'); define('APP_ERR_TRIGGER','ALL9005'); define('APP_ERR_UNKNOWN','ALL9006'); define('APP_ERR_NOT_NUMBER','ALL9007'); define('APP_ERR_NOT_DATE','ALL9008'); function clearMessages($conn) { $sql = 'CALL messages.u_clear_Messages()'; $stmt = db2_prepare($conn, $sql); db2_execute($stmt); } function addMessage($conn, $msgId, $forField = " ", $msgData = " ") { $sql = 'CALL messages.u_add_Message(?, ?, ?)'; $stmt = db2_prepare($conn, $sql); db2_bind_param($stmt, 1, "msgId", DB2_PARAM_IN); db2_bind_param($stmt, 2, "forField", DB2_PARAM_IN); db2_bind_param($stmt, 3, "msgData", DB2_PARAM_IN); db2_execute($stmt); } function messageCount($conn) { $msgCount = 0; $sql = 'CALL messages.u_message_Count(?)'; $stmt = db2_prepare($conn, $sql); db2_bind_param($stmt, 1, "msgCount", DB2_PARAM_OUT); db2_execute($stmt); return $msgCount; } function getMessage($conn, $forMessage) { $msgId = ''; $msgText = ''; $severity = 0; $help = ''; $forField = ''; $sql = 'CALL messages.u_get_Message(?, ?, ?, ?, ?, ?)'; $stmt = db2_prepare($conn, $sql); db2_bind_param($stmt, 1, "forMessage", DB2_PARAM_IN); db2_bind_param($stmt, 2, "msgId", DB2_PARAM_OUT); db2_bind_param($stmt, 3, "msgText", DB2_PARAM_OUT); db2_bind_param($stmt, 4, "severity", DB2_PARAM_OUT); db2_bind_param($stmt, 5, "help", DB2_PARAM_OUT); db2_bind_param($stmt, 6, "forField", DB2_PARAM_OUT); db2_execute($stmt); return $msgText; } function fillMessages($conn, $numMessages) { $sql = 'CALL messages.d_fill_Messages(?)'; $stmt = db2_prepare($conn, $sql); db2_bind_param($stmt, 1, "numMessages", DB2_PARAM_IN); db2_execute($stmt); return; } ?>
Note that each of the functions is passed a parameter, $conn, which identifies the connection to be used on the call to the subprocedure. This parameter identifies the job that calls the subprocedures. Also note the definition of constants for the message IDs.
This is the PHP script phpmessage.php that corresponds to the SHOWMSGS program. The script performs the following:
- Makes a connection to the i ($conn)
- Calls clearMessages() to clear any stored messages
- Calls addMessage() twice to add two messages (note the use of the named constants to identify the required message IDs)
- Calls fillMessages() to add three filler messages
- Based on the value returned by messageCount(), the script loops through calls to getMessage() and display the message text returned for each message
- Closes the connection to the i
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Playing with PHP Program Calls</title> </head> <body> <h1>Playing with PHP Messages</h1> <p>This example demonstrates use of the message routines</p> <?php error_reporting(E_ALL); ini_set("display_errors", 1); require 'func_messages.php'; $schema="MESSAGES"; $toDatabase = ""; // Set Database Name $profile = ""; // Set Required Profile and Password $profilePW = ""; $libl = "MESSAGES"; $conn=""; $options = array("i5_lib"=>$schema, "i5_commit"=>DB2_I5_TXN_NO_COMMIT, "i5_naming"=>DB2_I5_NAMING_ON,); if (!$conn = db2_pconnect($toDatabase, $profile, $profilePW, $options)) { echo 'connection failed.<br />'; die(db2_conn_errormsg().'<br />'); } echo "Clear Messages <br />"; clearMessages($conn); echo "Add Messages <br />"; addMessage($conn, APP_ERR_NOTFOUND, 'TEST1'); addMessage($conn, APP_ERR_CHANGED, 'TEST2'); echo "Fill 3 Messages <br />"; fillMessages($conn, 3); echo "Get Message count <br />"; $msgCount = messageCount($conn); echo "Returned message count is ".$msgCount." <br />"; echo "Get Messages <br />"; for ($i = 1; $i <= $msgCount; $i++) { echo getMessage($conn, $i)." <br />"; } db2_close($conn); ?> <p> Page complete </p> </body> </html>
The expected results from phpmessage.php are:
Other Message Subprocedures
But we don’t have to stop with just these subprocedures. These are some other message subprocedures that you might find useful:
- u_add_MessageText() allows you to directly store message text as opposed to using a message ID/message file
- u_set_Message_File() allows you to change the default message file and/or library being used
- u_send_File_Error() sends file messages based on a status code passed as a parameter. This subprocedure would be called based on an I/O error being received on a file operation and being trapped with an error extender
dcl-Proc u_add_MessageText export; dcl-Pi *n; msgText char(80) const; forFieldIn char(25) const options(*Omit:*noPass); severity int(10) const options(*noPass); end-Pi; dcl-S forField like(forFieldIn); msgCount += 1; messages(msgCount).msgText = msgText; if %parms()> 2; messages(msgCount).severity = severity; endIf; if %parms() > 1; if %Addr(forFieldIn) <> *null; forField = forFieldIn; endIf; endIf; messages(msgCount).forField = forField; end-Proc; dcl-Proc u_set_MessageFile export; dcl-Pi *n; newMsgf char(10) const; newMsgLib char(10) const options(*noPass); end-Pi; msgFile = newMsgF; if %parms()> 1; msgFileLib = newMsgLib; endIf; return; end-Proc; dcl-Proc u_send_FileError export; dcl-Pi *n ind; status int(5) const; end-Pi; // Duplicate if (status = STAT_DUPLICATE); u_add_Message(ERR_DUPLICATE); // Referential constraint elseIf (status = STAT_constRAINT_1 or status = STAT_constRAINT_2); send_constraintMsg(); // Trigger elseIf (status = STAT_TRIGGER_1 or status = STAT_TRIGGER_2); u_add_Message(ERR_TRIGGER); // Other else; u_add_Message(ERR_UNKNOWN); return *On; endIf; return *Off; end-Proc;
There You Have It
Hopefully these two articles have given you some food for thought. Your RPG code does not have to be confined to RPG only applications – all that great code can be opened up to wider use.
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.