Three ways I called an RPGLE program from Java

A recent project had me comparing different ways to call an RPGLE program using Java. I am a newbie / novice Java programmer, so this gave a lot of insight into the Java language and some of the JT400 / JTOpen tools.

To start, I had this RPGLE program. It retrieves, increments, sets, and returns a general ledger (GL) journal entry sequence number.

//////// remove this line
      // Program: incgljseq
      // Retrieve last used journal entry sequence; increment and store;
      // return new sequence.
     Fdbaxrel0  UF   E           K DISK

     d incgljseq       pr                  extpgm('INCGLJSEQ')
     d inlocation                     5a   const
     d outuseseq                     10i 0

     d incgljseq       pi
     d inlocation                     5a   const
     d outuseseq                     10i 0

     d sequence        s             10i 0

      /free

         sequence = 0;
         chain inlocation @axrecd;
         if %found;
            axlgnb += 1;
            sequence = axlgnb;
            update @axrecd;
         endif;
         outuseseq = sequence;
         *inlr = *on;
         return;

      /end-free

Originally, I intended to call the program using a stored procedure. In the course of the project I decided to pursue calling the RPGLE program directly. In the end, I called the program using PCML, but would consider JDBC due to its brevity.

Here is the stored procedure definition.

create procedure rtvgljseq
(in location char(5),
 out newseq integer)
external name incgljseq
language rpgle
parameter style general
;

The integration tool I was using, Extol Business Integrator, has special parameter requirements when using Java programs a particular way. Because of this, I used the main() method as a testbed, enabling me to call the program from the command line, and later in Eclipse when developing the other calls.

I investigated three different ways to call the RPGLE program: using ProgramCall (call the program directly), ProgramCallDocument (call the program using PCML, which is like a prototyped call), and JDBC. The main method is the same for all three classes:

	public static void main(String[] args) {
		// Exerciser program.
		Object[] parms = new Object[5];
		// parms[0] receives the output, nextSequence
		parms[1] = "192.168.0.1"; // IP address of IBM i system
		parms[2] = "USERID"; // user id to sign on
		parms[3] = "PASSWORD"; // password for user id
		parms[4] = "LOC"; // location from which to retrieve the next sequence
		try {
			System.out.println("Input : '" + parms[4] + "'");
			execute(parms, null, null);
			System.out.println("Output: '" + parms[0] + "'");
		}
		catch (Exception e) {
			System.out.println(e.toString());
			e.printStackTrace();
		}
	}

For all three classes, the execute() method calls the RPGLE program and returns the next sequence. First, here is the call using ProgramCall:

	public static synchronized void execute(Object[] parms, StateTableInstructionState stis, MethodMessages mm) {
		int nextSequence = 0;
		String sysname = (String) parms[1];
		String userid = (String) parms[2];
		String password = (String) parms[3];
		String vanguardLocation = (String) parms[4];
		AS400 sys = new AS400(sysname, userid, password);
		try {
			String msgId, msgText;

			// Set up library list for program call.
			CommandCall command = new CommandCall(sys);
			if (command.run("CHGLIBL LIBL(PROD_MOD VNGDBDTA)") != true) {
				AS400Message[] messageList = command.getMessageList();
				for (int i = 0; i < messageList.length; i++) {
					msgId = messageList[i].getID();
					msgText = messageList[i].getText();
					System.err.println(msgId + " - " + msgText);
				}
			}

			// Create field types for parameters.
			AS400Text txt5 = new AS400Text(5);

			// Create parameter array and populate.
			ProgramParameter[] parmList = new ProgramParameter[2];
			parmList[0] = new ProgramParameter(txt5.toBytes(vanguardLocation));
			parmList[1] = new ProgramParameter(4);

			// Set up program call and run.
			ProgramCall pgm = new ProgramCall(sys, "/QSYS.LIB/PROD_MOD.LIB/INCGLJSEQ.PGM", parmList);
			if (pgm.run() != true) {
				AS400Message[] messageList = pgm.getMessageList();
				for (int i = 0; i < messageList.length; i++) {
					msgId = messageList[i].getID();
					msgText = messageList[i].getText();
					System.err.println(msgId + " - " + msgText);
				}
			} else {
				AS400Bin4 bin4Converter = new AS400Bin4();
				byte[] data = parmList[1].getOutputData();
				nextSequence = bin4Converter.toInt(data);
			}
		} catch (Exception e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
		sys.disconnectAllServices();
		parms[0] = Integer.toString(nextSequence);
	}

Don’t worry about the execute() method declaration; that is special for Extol Business Integrator (EBI). Also disregard the conversion of nextSequence from integer to string; again, this is to work around an issue I had with EBI.

The ProgramCall code has lots of parts: conversions from Java to IBM i (AS/400) data types and back, setting up the library list, and handling fro retrieving IBM i error messages if necessary.

A little shorter way is to call using PCML. PCML is a tagged document similar to XML which is a “prototype” for calling an RPGLE program from Java. For more information about compiling a program to generate the PCML file see the post http://code.blackrobes.net/calling-rpgle-programs-with-java-and-pcml/.

The PCML file defines the interface to the called program, including the program’s location, and number and type of parameters. The PCML’s file name is incgljseq.pcml.

<pcml version="4.0">
   <!-- RPG program: INCGLJSEQ  -->
   <!-- created: 2009-10-30-13.26.10 -->
   <!-- source: LOYD/SOURCE(INCGLJSEQ) -->
   <!-- 18 -->
   <program name="INCGLJSEQ" path="/QSYS.LIB/LOYD.LIB/INCGLJSEQ.PGM">
      <data name="INLOCATION" type="char" length="5" usage="input" />
      <data name="OUTUSESEQ" type="int" length="4" precision="31" usage="inputoutput" />
   </program>
</pcml>

And here is the Java code to call using PCML. Note on line 20, we do not have to give the PCML’s extension when passing the file name into ProgramCallDocument. Under the covers, the method looks for a serialized version with extension .pcml.ser, or the regular file with extension .pcml.

	public static synchronized void execute(Object[] parms, StateTableInstructionState stis, MethodMessages mm) {
		int nextSequence = 0;
		String sysname = (String) parms[1];
		String userid = (String) parms[2];
		String password = (String) parms[3];
		String vanguardLocation = (String) parms[4];
		AS400 sys = new AS400(sysname, userid, password);
		try {
			String msgId, msgText;
			// Set up library list for program call.
			CommandCall command = new CommandCall(sys);
			if (command.run("CHGLIBL LIBL(PROD_MOD VNGDBDTA)") != true) {
				AS400Message[] messageList = command.getMessageList();
				for (int i = 0; i < messageList.length; i++) {
					msgId = messageList[i].getID();
					msgText = messageList[i].getText();
					System.err.println(msgId + " - " + msgText);
				}
			}
			ProgramCallDocument pcml = new ProgramCallDocument(sys, "incgljseq");
			pcml.setValue("INCGLJSEQ.INLOCATION", vanguardLocation);
			pcml.setValue("INCGLJSEQ.OUTUSESEQ", nextSequence);
			if (pcml.callProgram("INCGLJSEQ") != true) {
				AS400Message[] messageList = pcml.getMessageList("INCGLJSEQ");
				for (int i = 0; i < messageList.length; i++) {
					msgId = messageList[i].getID();
					msgText = messageList[i].getText();
					System.err.println(msgId + " - " + msgText);
				}
			} else {
				nextSequence = (Integer) pcml.getValue("INCGLJSEQ.OUTUSESEQ");
			}
		}
		catch (Exception e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
		sys.disconnectAllServices();
		parms[0] = Integer.toString(nextSequence);
	}

Finally, here is the call using JDBC, using the stored procedure definition listed above. It has by far the fewest lines of code, and eliminates the need for multiple conversions from Java types to IBM i types. It also elminiates the need of a separate call for setting the library list. (This can be done by calling a CL program wrapper, but this exercise is calling the RPGLE program.) One disadvantage of JDBC is being limited to using “simple” parameter types. Contrast with the PCML call, which can use complex parameters, such as RPG data structures.

	public static synchronized void execute(Object[] parms, StateTableInstructionState stis, MethodMessages mm) {
		int nextSequence = 0;
		String sysname = (String) parms[1];
		String userid = (String) parms[2];
		String password = (String) parms[3];
		String vanguardLocation = (String) parms[4];
		try {
			DriverManager.registerDriver(new com.ibm.as400.access.AS400JDBCDriver());
			Connection conn = DriverManager.getConnection("jdbc:as400://" + sysname + ";naming=system; libraries=,prod_dta,prod_mod,vngdbdta", userid, password);
			CallableStatement cs = conn.prepareCall("call rtvgljseq(?,?)");
			cs.registerOutParameter(2, java.sql.Types.INTEGER);
			cs.setString(1, vanguardLocation);
			cs.execute();
			nextSequence = cs.getInt(2);
			cs.close();
		}
		catch (Exception e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
		parms[0] = Integer.toString(nextSequence);
	}

My preference is to use either PCML or JDBC. The JDBC code is very brief and hides or elminiates variable casting. However, it is really suitable for simple parameter types, and requires an extra IBM i object (the stored procedure definition). PCML is nice because like JDBC, it describes the number and kind of parameters. However, there is a separate system connection and call to set the library list if needed. Like JDBC an extra object is needed (the PCML file) but rather than server-side, it is bundled with the Java code.

This post pulled together and briefly compared various ways to call an ILE RPG program from Java.

Links:

Calling RPGLE programs with Java and PCML

Demonstrating how to call an RPGLE program from Java using PCML.

Here is the RPGLE program interface.

//////// remove this line
     d incgljseq       pr                  extpgm('INCGLJSEQ')
     d inlocation                     5a   const
     d outuseseq                     10i 0

Program compilation.

CRTBNDRPG PGM(LOYD/INCGLJSEQ) SRCFILE(LOYD/SOURCE) SRCMBR(INCGLJSEQ) REPLACE(*YES) PGMINFO(*PCML) INFOSTMF('/home/loyd/incgljseq.pcml')

The important options are PGMINFO(*PCML) and INFOSTMF(’/home/loyd/incgljseq.pcml’). When compiled, a PCML interface file looks like this.

<pcml version="4.0">
   <!-- RPG program: INCGLJSEQ  -->
   <!-- created: 2009-10-30-13.26.10 -->
   <!-- source: LOYD/SOURCE(INCGLJSEQ) -->
   <!-- 18 -->
   <program name="INCGLJSEQ" path="/QSYS.LIB/LOYD.LIB/INCGLJSEQ.PGM">
      <data name="INLOCATION" type="char" length="5" usage="input" />
      <data name="OUTUSESEQ" type="int" length="4" precision="31" usage="inputoutput" />
   </program>
</pcml>

The PCML document serves for Java and ProgramCallDocument() the same function as the RPGLE prototype for other program calls: it is the “contract” between the caller and callee, specifying the number, kind, and use of parameters. This is also the same jump from using RPG’s *ENTRY PLIST going to prototyped calls, allowing for better enforcement of parameters in the call.

Used with the ProgramCallDocument method in jt400.jar or jtopen.jar, this allows Java programs to call RPGLE programs with little fuss. In the next code examples I focus on the program call process and not the setup, such as connecting to the IBM i, setting the library list, etc.

The Java code samples pass INLOCATION using string variable vanguardLocation, and receive OUTUSESEQ into integer variable nextSequence.

Here is the original program to call RPGLE, using ProgramCall.

// Create field types for parameters.
AS400Text txt5 = new AS400Text(5);

// Create parameter array and populate.
ProgramParameter[] parmList = new ProgramParameter[2];
parmList[0] = new ProgramParameter(txt5.toBytes(vanguardLocation));
parmList[1] = new ProgramParameter(4);

// Set up program call and run.
ProgramCall pgm = new ProgramCall(sys, "/QSYS.LIB/LOYD.LIB/INCGLJSEQ.PGM", parmList);
if (pgm.run() != true) {
	...
} else {
	AS400Bin4 bin4Converter = new AS400Bin4();
	byte[] data = parmList[1].getOutputData();
	nextSequence = bin4Converter.toInt(data);
}

Notice we must create a parameter list, convert incoming parameters to IBM i types, and convert outgoing parameters back to Java types. We are also exposed to the full path to the called program. The PCML document already specified the data types required by the program and hides the full path to the called program. The ProgramCallDocument class handles type conversions for us. All of this results in less code:

// Next line references the PCML file. By default, in the same place as the .class file.
ProgramCallDocument pcml = new ProgramCallDocument(sys, "incgljseq");
pcml.setValue("INCGLJSEQ.INLOCATION", vanguardLocation);
pcml.setValue("INCGLJSEQ.OUTUSESEQ", nextSequence);

if (pcml.callProgram("INCGLJSEQ") != true) {
	...
} else {
	nextSequence = (Integer) pcml.getValue("INCGLJSEQ.OUTUSESEQ");
}

In line 2 above we reference the PCML file, incgljseq.pcml, which is in the same place as the class file. Notice we do not specify the extension .pcml. The reason for this is we can also serialize the PCML file. The PCML loader first looks for the serializaed version (for example, incgljseq.pcml.ser), then for the normal PCML file (incgljseq.pcml).

Coding Java programs for EBI external API actions

In Extol Business Integrator (EBI), coding requirements for External API Actions (implemented via Java programs) are different from a Business Process task Run Java Program. The Run Java Program task can execute any method from a class or JAR file with any parameter combination. In contrast, External API Actions have specific parameter requirements.

The following Java structure shows External API Actions coding.

import com.extol.engines.StateTableInstructionState;
import com.extol.engines.MethodMessages;
public class MyClassApi implements Serializable {
	public static synchronized void execute(Object[] parms, StateTableInstructionState stis, MethodMessages mm) {
		// parameters passed in Object[] parms - these map to EBI
		String parm1 = (String) parms[1];
		String parm2 = (String) parms[2];
		...
		// method body
		...
		parms[0] = someReturnValue; // return value
	}

	// empty main method...
	public static void main(String[] args) {
	}

	// or use as testing, call from command line...
	public static void main(String[] args) {
		// Exerciser program.
		Object[] parms = new Object[3];
		// parms[0] receives the output, nextSequence
		parms[1] = "my parm 1";
		parms[2] = "my parm 2";
		try {
			execute(parms, null, null);
			System.out.println("Output: '" + parms[0] + "'");
		}
		catch (Exception e) {
			System.out.println(e.toString());
			e.printStackTrace();
		}
	}
}

The two import com.extol.… lines are required for communications between EBI and the Java program when used as an External API Action.

The class definition should implement Serializable.

Extol discourages the use of the main() method. Other coding examples I’ve seen use execute() as the method name. Obviously, if you have multiple methods in your class/JAR file, they will have different names.

When defining the External API Action in EBI, your mapped parameters correspond to the Object[] parms parameter. By Extol convention, if there is a (single) return value, it is passed in parms[0].

I use the main() method to call from Eclipse or the command line for testing, and set up the parameters the same way as they are expected to be used in EBI.

It is recommended (at least for EBI 2.4, as of this writing) that return values be type String. When used in an EBI transformation ruleset, output type of Integer cannot be mapped to a corresponding middle tree variable, resulting in a type mismatch error message.

Update 5 Nov 2009: If an External API Action definition is changed, any rule sets referencing it must be updated. Open the ruleset and select Tools, Refresh Action Lists. Then validate, compile, and save the rule set. The reason for this is because the ruleset is compiled into a Java class, and any external API actions are integrated at compile-time.

This article was originally published on 2 Nov 2009.

UIM brush file for SyntaxHighliter

User Interface Manager (or UIM) is an IBM tag language primarily used to define help text, menus, and display panels. It can be thought of as a markup language similar to HTML.

Here is a brush file for use with SyntaxHighligher, and by extension, the WordPress plugin SyntaxHighlighter Evolved.

/**
 *
 * IBM i5/OS UIM (user interface manager) brush file.
 *
 * Written by Loyd Goodbar <loyd@blackrobes.net>, 2009.
 * getFunctions() code from helen at alexgorbatchev.com forums.
 *
 */
SyntaxHighlighter.brushes.Uim = function()
{
	// Tags with corresponding end tag.
	var etags = ':e?(cit|class|data(grp|slt)?|dl|fig|help|hp[0-9]|info|keyl|'+
		'lines|link|list(grp)?|mbarc?|menu(grp)?|note|nt|panel|parml|'+
		'pdfld|pk|pnlgrp|prthead|prtpnl|pv|ol|rt|sl|tl|tt|ul|xmp)[.]?';

	// Standalone tags.
	var tags = ':(appfmt|botinst|check|cmdline|cond|copyr|datac(ol)?|dataix?|'+
		'datasltc|(dd|dt)(hd)?|figcap|h[1-4]|imhelp|import|isch(syn)?|'+
		'keyi|li|list(act|col|def|view)|lp|menui|mi|optline|pc|'+
		'pd(accel|fldc)?|prttrail|pt|p|text|ti|topinst|ttrow|var(rcd)?|'+
		'xh[1-4])[.]?';

	var symbols = '&(amp;)?(amp|colon|cont|msg|period|slr)[.]';

	var comments = '^[.*].*$';

	this.regexList = [
		{ regex: new RegExp(comments, 'gmi'), css: 'comments' },
		{ regex: new RegExp(tags, 'gmi'), css: 'keyword' },
		{ regex: new RegExp(etags, 'gmi'), css: 'keyword' },
		{ regex: new RegExp(symbols, 'gmi'), css: 'color3' }
	];
};

SyntaxHighlighter.brushes.Uim.prototype = new SyntaxHighlighter.Highlighter();
SyntaxHighlighter.brushes.Uim.aliases = ['uim'];

One note about the brush file. UIM defines figurative symbol names such as &colon. Because WordPress escapes the ampersand, what is presented to SyntaxHighter is &amp;colon. The symbols regex takes this into account.

As an example of User Interface Manager, here is a help definition for the Display Record Format command. This is a third-party tool, search Google for DSPRCDFMT for more details.

.* Help for the Display Record Format command.

:pnlgrp.

:help name=dsprcdfmt.Display Record Format (DSPRCDFMT)
:p.DSPRCDFMT displays the record format (list of field names and attributes) of a
logical or physical file.
:p.Display Record Format shows the following information about a file&colon. file
name and library, file text, record format name, record length and number of
fields.
:p.The following file field information is shown&colon. field name, field type,
field size, key number if a key field, beginning and ending positions of the
field, and field text.
:ehelp.

:help name='dsprcdfmt/file'.File (FILE) parameter
:xh3.File (FILE) parameter
:p.Specifies the name and library of the file to be used by the Display Record
Format command.
:p.This is a required parameter.
:p.The possible values are&colon.
:p.:parml.:pt.file-name
:pd.Specify the name of the file to be processed.
:eparml.
:p.The possible library values are&colon.
:p.:parml.:pt.:pk def.*libl:epk.
:pd.All libraries in the job's library list are searched.
:pt.library-name
:pd.Specify the name of the library to be searched.
:eparml.
:ehelp.

:help name='dsprcdfmt/rcdfmt'.Record format (RCDFMT) parameter
:xh3.Record format (RCDFMT) parameter
:p.Specify the name of the record format to be used.
:p.The possible values are&colon.
:p.:parml.:pt.:pk def.*FIRST:epk.
:pd.The first record format found will be processed.
:pt.format-name
:pd.Specify the name of a record format to be processed.
:eparml.
:ehelp.

:help name='dsprcdfmt/output'.Output (OUTPUT) parameter
:xh3.Output (OUTPUT) parameter
:p.Specifies whether the output from the command is displayed at the requesting
work station or printed with the job's spooled output.
:p.The possible parameters are&colon.
:p.:parml.:pt.:pk def.*:epk.
:pd.The output is displayed (if requested by an interactive job) or printed with
the job's spooled output (if requested by a batch job).
:pt.*PRINT
:pd.The output is printed with the job's spooled output.
:eparml.
:ehelp.

:epnlgrp.

Minor code changes are required to SyntaxHighlighter Evolved’s syntaxhighlighter.php file to process UIM code:

		// Register brush scripts
...
		wp_register_script( 'syntaxhighlighter-brush-uim',        plugins_url('syntaxhighlighter/syntaxhighlighter/scripts/shBrushUim.js'),        array('syntaxhighlighter-core'), $this->agshver );
...
		// Create list of brush aliases and map them to their real brushes
		$this->brushes = apply_filters( 'syntaxhighlighter_brushes', array(
...
			'uim'           => 'uim',
...
		) );

Check the SyntaxHighlighter category for other brushes!

ILE RPG brush file for SyntaxHighlighter

Below is a reasonably complete custom brush file for ILE RPG, for use with SyntaxHighlighter. It is current through i5/OS ILE RPG version V6R1 (aka 6.1). This site uses the WordPress plugin SyntaxHighlighter Evolved, so minor changes are needed to the plugin file syntaxhighlighter.php.

Normally SyntaxHighlighter only highlights words, and not any special symbols around them. ILE RPG prefixes built-in functions with a percent sign (such as %abs), and some key words are prefixed with an asterisk (such as *inlr or **CTDATA). The function getFunctions allows the special symbols to be highlighted with the associated key word. Many thanks to user helen at the alexgorbatchev.com forums for providing it.

The brush language is rpgle (same as the source member type) or rpg4 (the informal RPG language version).

/**
 *
 * IBM ILE RPG brush file.
 * Current for version V6R1
 *
 * Written by Loyd Goodbar <loyd@blackrobes.net>, 2009.
 * getFunctions() code from helen at alexgorbatchev.com forums.
 *
 */
SyntaxHighlighter.brushes.Rpgle = function()
{
	var bifs = '%abs %addr %alloc %bit(and|not|or|xor) %char %check(r)? %date %days '+
		'%dec(h|pos)? %diff %div %edit(c|flt|w) %elem %eof %equal %error %fields %float '+
		'%found %graph %handler %hours %int %kds %len %lookup(lt|ge|gt|le)? %minutes '+
		'%months %mseconds %nullind %occur %open %paddr %parms %realloc %rem %replace '+
		'%scan %seconds %shtdn %size %sqrt %status %str %subarr %subdt %subst %this '+
		'%time(stamp)? %tlookup(lt|ge|gt|le)? %trim(l|r)? %ucs2 %uns %unsh %xfoot '+
		'%xlate %xml %years';

	var opcodes = 'acq add(dur)? alloc and(gt|lt|eq|ne|ge|le)? begsr bit(off|on) '+
		'cab(gt|lt|eq|ne|ge|le) call(b|p)? cas(gt|lt|eq|ne|ge|le) cat chain check(r)? '+
		'clear close commit comp dealloc define delete div do dou(gt|lt|eq|ne|ge|le)? '+
		'dow(gt|lt|eq|ne|ge|le)? dsply dump else(if)? end(cs|do|for|if|mon|sl|sr)? '+
		'eval(r|-corr)? except exfmt exsr extrct feod for force free goto '+
		'if(gt|lt|eq|ne|ge|le)? in iter kfld klist leave(sr)? lookup m(h|l){2}zo monitor '+
		'move(a|l)? mult mvr next occur on-error open or(gt|lt|eq|ne|ge|le)? other out '+
		'parm plist post read(c|e|p|pe)? realloc rel reset return rolbk scan select '+
		'set(gt|ll|off|on) shtdn sorta sqrt sub(dur|st)? tag test(b|n|z)? time unlock '+
		'update when(gt|lt|eq|ne|ge|le)? write xfoot xlate xml-(into|sax) z-(add|sub)';

	var ckeywords = 'actgrp altseq alwnull aut bnddir ccsid copy(nest|right) cursym '+
		'cvtopt dat(edit|fmt) debug dec(edit|prec) dft(actgrp|name) enbprfcol expropts '+
		'extbinint fixnbr fltdiv formsalign ftrans genlvl indent intprec langid (no)?main '+
		'openopt optimize option pgminfo prfdta srtseq text thread timfmt truncnbr usrprf';

	var fkeywords = 'block commit datfmt devid ext(desc|file|ind|mbr) form(len|ofl) ignore '+
		'include indds infsr keyloc likefile maxdev oflind pass pgmname plist prefix '+
		'prtctl qualified rafdata recno rename saveds saveind sfile sln static template '+
		'timfmt usropn';

	var dkeywords = 'align alt(seq)? ascend based ccsid class const ctdata datfmt '+
		'descend dim dtaara export ext(fld|fmt|name|pgm|proc) fromfile import inz '+
		'like(ds|file|rec)? noopt occurs opdesc options overlay packeven perrcd prefix '+
		'procptr qualified static template timfmt tofile value varying';

	var pkeywords = 'export serialize';

	var figuratives = '[*]{2}ctdata [*]blanks? [*]zeros? [*](hi|lo)val [*]null [*]on [*]off '+
		'[*]all(x|g)? [*]start [*]end';

	var directives = '[/](end-)?(free|exec) [/](copy|eject|else|eof|include|space|title) '+
		'[/](un)?define [/](else|end)?if';

	var indicators = '[*]?in([01-99]|lr|(h|l)[1-9])';

	// Comments starting at column 7 for fixed format.
	var fixedcomments = '^.{6}[*].*$';

	this.getFunctions = function(list)
	{
		return "(?:" + list.replace(/\s+/g, "|") + ")\\b";
	};

	this.regexList = [
		{ regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' },
		{ regex: new RegExp(fixedcomments, 'gm'), css: 'comments' },
		{ regex: new RegExp(this.getFunctions(directives), 'gmi'), css: 'color1' },
		{ regex: new RegExp(this.getFunctions(bifs), 'gmi'), css: 'functions' },
		{ regex: new RegExp(this.getKeywords(opcodes), 'gmi'), css: 'keyword' },
		{ regex: new RegExp(this.getFunctions(indicators), 'gmi'), css: 'color3' },
		{ regex: new RegExp(this.getFunctions(figuratives), 'gmi'), css: 'color3' },
		{ regex: new RegExp(this.getKeywords(ckeywords), 'gmi'), css: 'color3' },
		{ regex: new RegExp(this.getKeywords(fkeywords), 'gmi'), css: 'color3' },
		{ regex: new RegExp(this.getKeywords(dkeywords), 'gmi'), css: 'color3' },
		{ regex: new RegExp(this.getKeywords(pkeywords), 'gmi'), css: 'color3' }
	];
};

SyntaxHighlighter.brushes.Rpgle.prototype = new SyntaxHighlighter.Highlighter();
SyntaxHighlighter.brushes.Rpgle.aliases = ['rpgle','rpg4'];

In order to use this brush file with the WordPress plugin SyntaxHighlighter Evolved, changes are needed in the syntaxhighlighter.php plugin file to recognize the new brush.

		// Register brush scripts
...
		wp_register_script( 'syntaxhighlighter-brush-rpgle',      plugins_url('syntaxhighlighter/syntaxhighlighter/scripts/shBrushRpgle.js'),      array('syntaxhighlighter-core'), $this->agshver );
...
		// Create list of brush aliases and map them to their real brushes
		$this->brushes = apply_filters( 'syntaxhighlighter_brushes', array(
			'as3'           => 'as3',
...
			'python'        => 'python',
			'rpgle'         => 'rpgle',
			'rpg4'          => 'rpgle',
			'rails'         => 'ruby',
...
		) );

Watch this space for updates and refinements!

Update 13 May 2009: Added more figurative constants and fixed-format comments.

This article was originally published on 9 May 2009.

ILE RPG epoch functions

When working with the Integrated File System (IFS), it is often useful to convert file dates into real timestamp values. IFS file date/time attributes are stored in *NIX epoch format. The *NIX epoch is the number of seconds since midnight, 1 January 1970. I had some projects working with Scott Klement’s FTPAPI, where I need to do date comparisons based on a file’s modified date. Here are the Wikipedia entries for epoch and UNIX time.

Here are the prototypes, from the file SOURCE, member PROTOTYPES:

//////// remove this line
      // Convert Unix timestamp to OS/400 timestamp.
     d CvtEpochTS      pr              z
     d  inEpochSecs                  10i 0 const
     d  inAdjustUTC                    n   const options(*nopass)

      // Convert OS/400 timestamp to Unix timestamp.
     d CvtTSEpoch      pr            10i 0
     d  inTS                           z   const
     d  inAdjustUTC                    n   const options(*nopass)

…and the procedures, from file SOURCE, member PROCEDURES:

//////// remove this line
      // -----------------------------------------------------------------------
      // CvtEpochTS
      // ----------
      // Helper routine to convert UNIX-type epoch values (typically the number
      // of seconds from midnight, Jan 1, 1970) to an OS/400 timestamp value.
      // The UNIX-type epoch value is considered the "timestamp" value to the
      // UNIX world.
      //
      // INPUTS
      // inEpochSecs int
      //      The UNIX timestamp, in UNIX epoch format (number of seconds since
      //      midnight, Jan 1, 1970.
      // [nAdjustUTC] bool
      //      Optional parameter.
      //      Determine whether the time should be adjusted by the UTC offset.
      //      *true - adjust for UTC offset (default value)
      //      false - do not adjust for UTC offset
      //
      // OUTPUTS
      // OS400Timestamp char[26]
      //      Equivalent OS/400 timestamp value.

     p CvtEpochTS      b

     d CvtEpochTS      pi              z
     d  inEpochSecs                  10i 0 const
     d  inAdjustUTC                    n   const options(*nopass)

      // Constants.
     d EPOCHSTART      s               z   inz(z'1970-01-01-00.00.00.000000')

      // Variables.
     d returnts        s               z   inz(z'0001-01-01-00.00.00.000000')
     d utcoffhours     s             10i 0 inz
     d utcoffmins      s             10i 0 inz
     d utcoffsecs      s              8f   inz
     d utcoffset       s             10i 0 inz
     d workAdjustUTC   s                   inz like(inAdjustUTC)

     d GetUTCOffset    pr                  extproc('CEEUTCO')
     d  offsethours                  10i 0
     d  offsetminutes                10i 0
     d  offsetseconds                 8f
     d  feedback                     12a   options(*nopass)

      /free

         if %parms() < 2;
              workAdjustUTC = *on;
         else;
              workAdjustUTC = inAdjustUTC;
         endif;
         returnts = EPOCHSTART + %seconds(inEpochSecs);
         if workAdjustUTC;
              callp(e) GetUTCOffset(utcoffhours:utcoffmins:utcoffsecs);
              utcoffset = utcoffsecs;
              returnts += %seconds(utcoffset);
         endif;
         return returnts;

      /end-free

     p CvtEpochTS      e

      // -----------------------------------------------------------------------
      // CvtTSEpoch
      // ----------
      // Helper routine to convert OS/400 timestamp to UNIX-type epoch.
      // KNOWN LIMITATIONS:
      // * Unknown results if used before 1/1/1970.
      //
      // INPUTS
      // inOS400Timestamp char[26]
      //      OS/400 timestamp value.
      // [inAdjustUTC] bool
      //      Optional parameter.
      //      Determine whether the time should be adjusted by the UTC offset.
      //      *true - adjust for UTC offset (default value)
      //      false - do not adjust for UTC offset
      //
      // OUTPUTS
      // outEpochSecs int
      //      The UNIX timestamp, in UNIX epoch format (number of seconds since
      //      midnight, Jan 1, 1970.

     p CvtTSEpoch      b

     d CvtTSEpoch      pi            10i 0
     d  inOS400Timestamp...
     d                                 z   const
     d  inAdjustUTC                    n   const options(*nopass)

      // Constants.
     d EPOCHSTART      s               z   inz(z'1970-01-01-00.00.00.000000')

      // Variables.
     d returnepoch     s             10i 0 inz
     d utcoffhours     s             10i 0 inz
     d utcoffmins      s             10i 0 inz
     d utcoffsecs      s              8f   inz
     d utcoffset       s             10i 0 inz
     d workAdjustUTC   s                   inz like(inAdjustUTC)

     d GetUTCOffset    pr                  extproc('CEEUTCO')
     d  offsethours                  10i 0
     d  offsetminutes                10i 0
     d  offsetseconds                 8f
     d  feedback                     12a   options(*nopass)

      /free

         if %parms() < 2;
              workAdjustUTC = *on;
         else;
              workAdjustUTC = inAdjustUTC;
         endif;
         returnepoch = %diff( inOS400Timestamp : EPOCHSTART : *seconds );
         if workAdjustUTC;
              callp(e) GetUTCOffset(utcoffhours:utcoffmins:utcoffsecs);
              utcoffset = utcoffsecs;
              returnepoch -= utcoffset;
         endif;
         return returnepoch;

      /end-free

     p CvtTSEpoch      e

For completeness, here is an exerciser program:

//////// remove this line
     h dftactgrp(*no) actgrp('QILE')

      /include source,prototypes

     d myts            s               z   inz(z'1978-09-02-18.52.54.000000')
     d mytsepoch       s             10i 0 inz
     d myepoch         s             10i 0 inz(273628374)
     d myepochts       s               z   inz

      /free

         myepochts = CvtEpochTS( myepoch : *on );
         mytsepoch = CvtTSEpoch( myts : *on );

         *inlr = *on;
         return;

      /end-free

      /include source,procedures

ILE RPG UUID functions

A UUID is a unique identifier which has an extremely low collision rate. This article explains some real-world uses for UUIDs. Here is the Wikipedia entry.

The first procedure simply retrieves the UUID itself. A UUID is nothing more than 16 hex bytes. Many applications, however, expect to see the “human respresentation” of the bytes in a standard format. A wrapper procedure will convert the UUID from hex to readable format.

The only downside to the i5/OS UUID (as of V5R4, I do not have access to a 6.1 machine to test) is it is a DCE version 1 UUID. The lastest version as of this writing is version 5 with SHA-1 hashing.

Here is the getUUID prototype:

//////// remove this line
     d getUUID         pr            16a

And the corresponding procedure:

//////// remove this line
     p getUUID         b
     d getUUID         pi            16a
      // Implements DCE version 1 UUID
      // Source link:
      // http://publib.boulder.ibm.com/iseries/v5r1/ic2924/tstudio/tech_ref/mi/GENUUID.htm

      // Template structure required for _GENUUID.
     d UUID_template   ds
     d  UUID_bytes_provided...
     d                               10u 0 inz(%size(uuid_template))
     d  UUID_bytes_available...
     d                               10u 0
     d  UUID_reserved                 8a   inz(*allx'00')
     d  UUID_UUID                    16a

     d GenUUID         pr                  extproc('_GENUUID')
     d  UUID_Template                  *   value

      /free
         reset uuid_template;
         GenUUID(%addr(UUID_Template));
         return UUID_UUID;
      /end-free

     p getUUID         e

As mentioned above, many times it is more useful to get the UUID in a human readable format. With that in mind, here is a getUUIDString() procedure. It takes an optional parameter, a hex UUID (perhaps previous retrieved from getUUID). If a UUID is not passed, one will be retrieved automatically. The procedure also converts the text representation to lower case.

Here is the getUUIDString prototype:

//////// remove this line
     d getUUIDString   pr            36a
     d  inUUID                       16a   options(*nopass)

…and procedure:

//////// remove this line
     p getUUIDString   b
     d getUUIDString   pi            36a
     d  inUUID                       16a   options(*nopass)

     d workuuid        s                   inz like(inuuid)
     d uuid_string     s             36a   inz

      // Convert hex to character API.
     d cvthc           pr                  extproc('cvthc')
     d  Result                    65534a   options(*varsize)
     d  Source                    32767a   options(*varsize)
     d  ResultSize                   10i 0 value

      // NLS convert case.
     d convertcase     pr                  extproc('QlgConvertCase')
     d  ctrlBlock                          const like(FRCB)
     d  inString                  65535a   const options(*varsize)
     d  outString                 65535a   options(*varsize)
     d  inLength                     10i 0 const
     d  apiErrorDS                  300a   options(*varsize)
      // Formatted request control block required by QlgConvertCase.
     d FRCB            ds                  qualified
     d  ReqType                      10i 0 inz(1)
     d  CCSID                        10i 0 inz(0)
     d  CvtTo                        10i 0 inz(0)
     d  Reserved                     10a   inz(*allx'00')
      // Helper constants for FRCB.
      // Uses request 1 (CCSID format): assumes lower case, job CCSID.
      // For more information about the control block, see:
      // http://publib.boulder.ibm.com/infocenter/iseries/v5r3/topic/apis/QLGCNVCS.htm
     d CvtToUpper      c                   0
     d CvtToLower      c                   1

      // Error code structure.
     d errc0100        ds
     d  errc01bytpro                 10i 0 inz(%size(errc0100))
     d  errc01bytava                 10i 0 inz
     d  errc01excid                   7a   inz
     d  errc01resaaa                  1a   inz(x'00')
     d  errc01excdta                250a   inz

      /free
         if %parms() = 1;
            workuuid = inuuid;
         else;
            workuuid = getuuid();
         endif;
         cvthc( uuid_string : workuuid : %len(workuuid)*2 );
         frcb.cvtto = cvttolower;
         reset errc0100;
         convertcase( frcb : uuid_string : uuid_string :
                      %len(uuid_string) : errc0100 );
         uuid_string = %subst(uuid_string:1:8) + '-' +
                       %subst(uuid_string:9:4) + '-' +
                       %subst(uuid_string:13:4) + '-' +
                       %subst(uuid_string:17:4) + '-' +
                       %subst(uuid_string:21:12);
         return uuid_string;
      /end-free

     p getUUIDString   e

A few things going on in getUUIDString. It uses IBM QlgConvertCase API to convert the text to lower case (it can also do upper case), no %xlate() functions here. This is the “correct” way to convert case to respect CCSID. Also uses the cvthc API to convert the hex values to text representation. Personally I have not needed the function in normal programming, but it is also used to generate a human readable MD5 hash. Converting hex to character just doesn’t come up often in business programming. Lastly, it shows proper use of the error code parameter. All too often, developers leave the “bytes provided” parameter uninitialized. This translates to x’404040′ or a very large number. Just as often, it will be set to zero, which prevents the error structure from being populated.

You can download the entire text file with prototypes and procedures. The program demonstrates both using the getUUID and getUUIDString procedures. Run the program in debug mode, usually with command line option DBGVIEW(*SOURCE), to see that variables myuuid and myuuidstring are updated properly. Text file (source type RPGLE): ILE RPG UUID (245)

ILE RPG replaceXMLEntities()

Use this subprocedure when you need to escape common XML entities & (ampersand), < (less than), > (greater than), ‘ (single quote), and ” (double quote). Several web reports I generate using CGIDEV2 come in HTML or XML form, and those special XML characters must be escaped.

Put these variables and the prototype at the start of your main procedure:

//////// remove this line
      // Variables for XML entity replacement.
     d xmlentities     s              1a   dim(5) ctdata
     d xmlreplacement  s              4a   dim(5) alt(xmlentities)

     d replaceXMLEntities...
     d                 pr           400a   varying
     d  invalue                     200a   options(*varsize) const

Here is the procedure:

//////// remove this line
     p replaceXMLEntities...
     p                 b
     d replaceXMLEntities...
     d                 pi           400a   varying
     d  invalue                     200a   options(*varsize) const
     d lastpos         s             10i 0 inz
     d start           s             10i 0 inz
     d workvalue       s            400a   inz varying
     d z               s             10i 0 inz
      /free
         // See the XMLEntities CTDATA array for list.
         // Ampersand (&) should be first because that is what all the
         // other replacements use.
         workvalue = invalue;
         for z = 1 to %elem(xmlentities);
             lastpos = 0;
             start = 1;
             lastpos = %scan(xmlentities(z):invalue:start);
             dow lastpos > 0;
                 workvalue = %replace('&'+%trim(xmlreplacement(z))+';' :
                   workvalue:lastpos:1); // ":1" overwrites the existing "bad" char
                 start = lastpos+1;
                 lastpos = %scan(xmlentities(z):invalue:start);
             enddo;
         endfor;
         return workvalue;
      /end-free
     p replaceXMLEntities...
     p                 e

And finally, the compile-time array to map the entities with their replacement values:

//////// remove this line
**CTDATA xmlentities
&amp
<lt
>gt
'apos
"quot

Feel free to change the size of the invalue, workvalue, and return sizes. I use the procedure over single fields of data which are not very large.

ILE RPG ceiling()

This subprocedure implements a ceiling function: the smallest integer greater than or equal to value, given a scaling factor. It is implemented to mimic Excel’s ceiling, in that the returned value is farthest from zero.

Prototype:

//////// remove this line
     d ceiling         pr            10i 0
     d  value                        15s 5 const
     d  factor                       15s 5 const

Procedure:

//////// remove this line
     p ceiling         b
     d ceiling         pi            10i 0
     d  value                        15s 5 const
     d  factor                       15s 5 const
     d numtemp         s             10i 0 inz
     d sign            s              1s 0 inz
      /free
         if factor = 0;
            return 0;
         else;
            sign = 1;
            if value < 0;
               sign = -1;
            endif;
            numtemp = %int(value/factor);
            if numtemp = (value/factor);
               return %int(value);
            else;
               return (%abs(numtemp)+1)*factor*sign;
            endif;
         endif;
      /end-free
     p ceiling         e

ILE RPG floor()

This subprocedure implements a floor function: the largest integer value less than or equal to value, given a scaling factor.

Prototype:

//////// remove this line
     d floor           pr            10i 0
     d  value                        15s 5 const
     d  factor                       15s 5 const

Procedure:

//////// remove this line
     p floor           b
     d floor           pi            10i 0
     d  value                        15s 5 const
     d  factor                       15s 5 const
      /free
         if factor=0;
            return 0;
         else;
            return (%int(value/factor)*factor);
         endif;
      /end-free
     p floor           e