📖

Spring BootでIBM iのRPGをコールする

2023/10/14に公開

はじめに

Spring Bootを使用して、IBM i上のRPGをコールする方法です。
JDBCを使用せずに直接コールします。
RPGコールの解説がメインで、Spring Bootの知識があることが前提です。

概要

webで名前・生年月日を入力したら、RPGをコールして年齢を計算してメッセージを返し、webに表示します。

フォーム入力

フォーム入力
名前と生年月日を入力します。
生年月日は日付型ではなく数値とします。
エラーチェックをしていないので、正しい日付を指定して下さい。

結果

結果
メッセージと年齢が表示されます。

環境

種類 バージョン
OS macOS Ventura 13.6
Eclipse Pleiades 2023-09.20231005
Java Java21(Pleiades内蔵)
Spring Boot 3.1.4
IBM Toolbox for Java JTOpen 20.0.3
IBM i 7.1

OSはWindows10でも動作確認済みです。

RPGプログラム

RPGコールにあたって、まず下記のプログラムをTEST2の名前で、RPGLEとして作成します。
ソースのライブラリは自由です。コンパイル結果はTSTPLIBとします。
別のライブラリにコンパイルする場合は、後のパラメータを読み替えてください。

TEST2
     D                 DS
     D DS#TMDT                 1     14  0 INZ(*ZERO)
     D   DS#TIME               1      6  0
     D   DS#DATE               7     14  0
     C*
     C     *ENTRY        PLIST
     C                   PARM                    @NAME            50             名前
     C                   PARM                    @BTDT             8 0           誕生日
     C                   PARM                    @MSG            100             メッセージ
     C                   PARM                    @AGE              3 0           年齢
     C*
     C*< START OF MAIN-PROGRAM >
     C*
     C                   TIME                    DS#TMDT
     C*
     C                   EVAL      @AGE = %DIFF(%DATE(DS#DATE:*ISO):
     C                                          %DATE(@BTDT:*ISO):*YEARS)
     C                   EVAL      @MSG = %TRIMR(@NAME) + ' さんの年齢は '
     C                                  + %CHAR(@AGE) + ' 歳です '
     C*
     C*< END OF MAIN-PROGRAM >
     C*
     C                   MOVE      *ON           *INLR
     C                   RETURN

RPGプログラムの説明

RPGをコールできればいいので、処理内容はシンプルにしてあります。
エラーチェック等は行っていません。

パラメータ

  • @NAME:名前(IN)
  • @BTDT:誕生日(IN) YYYYMMDD形式
  • @MSG:メッセージ(OUT)
  • @AGE:年齢(OUT)

処理概要

名前と誕生日を受け取り、年齢を計算してメッセージと年齢を返します。
文字と数値のパラメータをIN/OUTの両方に設けたかったので、こんな感じにしました。

Javaプログラム

EclipseでSpring Bootプロジェクト作成


タイプはGradle-Groovyを選択しました。この後の説明に影響するので、この設定にしてください。


依存関係は下記を選択

  • Spring Boot DevTools
  • Lombok
  • Thymeleaf
  • Spring Web

IBM Toolbox for Javaを依存関係に追加

IBM Toolbox for JavaのJTOpen 20.0.3を依存関係に追加します。
build.gradledependenciesに下記を追加します。

implementation 'net.sf.jt400:jt400:20.0.3'

最終的に下記のようになります。

build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.4'
	id 'io.spring.dependency-management' version '1.1.3'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	// https://mvnrepository.com/artifact/net.sf.jt400/jt400
	implementation 'net.sf.jt400:jt400:20.0.3'
}

tasks.named('test') {
	useJUnitPlatform()
}

コメントのURL(https://mvnrepository.com/artifact/net.sf.jt400/jt400)はJTOpenのリポジトリで、各バージョンとMavenやGradleへの参照設定があります。
執筆時点の最新バージョンは20.0.3でした。

webのデザインを作成

src/main/resource/templatesrpgcall.htmlを作成します。

rpgcall.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>RPG CALL</title>
</head>
<body>
<form name="Form" action="#" th:action="@{/rpgcall}" th:object="${form}" method="post">
  <table>
    <tr>
      <td>名前</td>
      <td><input type="text" name="name" th:field="*{name}"></td>
    </tr>
    <tr>
      <td>生年月日</td>
      <td><input type="number" name="birth" th:field="*{birth}"></td>
    </tr>
  </table>
  <input type="submit">
</form>
<div>メッセージ:<span th:text="${msg}"/></div>
<div>年齢:<span th:text="${age}"/></div>
</body>
</html>

thymeleafを使用しています。

IBM i接続パラメータの設定

IBM iに接続するためのパラメータをapplication.propertiesに設定します。

application.properties
as400.hostname=AS400
as400.username=testuser
as400.password=testpw
as400.libraries=TSTDLIB,TSTPLIB
as400.programlib=TSTPLIB

プロパティの名前のas400.hostname等は、勝手に付けた名前です。特に決まりはないので自由に付けて貰って良いのですが、この後のプログラムで使用するプロパティ名前と合わせる必要があります。

プロパティの説明

  • as400.hostname:IBM iのホスト名です。IPアドレスでもOKです。
  • as400.username:IBM iに接続するユーザー名
  • as400.password:IBM iに接続するパスワード
  • as400.libraries:RPGを実行するのに必要なライブラリをカンマ区切りで指定
    今回のRPGプログラムはDBにアクセスしたりサブルーチンを呼び出したりしていないのですが、通常は別ライブラリにあるDBにアクセスするのでライブラリを指定します。
    内部的にはADDLIBLEでライブラリリストに追加しています。
  • as400.programlib:実行するRPGプログラムのライブラリ
    ライブラリリストにTSTPLIBを追加しているので別途指定する必要があるのかと思うのですが、RPGコール時にライブラリ指定が必須のようなので、プロパティとして別に設けてあります。

Webフォームからパラメータを受け取るクラスを作成

src/main/java/com.example.demoRpgCallForm.javaを作成

RpgCallForm.java
package com.example.demo;

public class RpgCallForm {
	private String name;
	private int birth;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getBirth() {
		return birth;
	}
	public void setBirth(int birth) {
		this.birth = birth;
	}
}

RPGをコールするメインクラスを作成

RpgCall.java
package com.example.demo;

import java.math.BigDecimal;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Message;
import com.ibm.as400.access.AS400PackedDecimal;
import com.ibm.as400.access.AS400Text;
import com.ibm.as400.access.CommandCall;
import com.ibm.as400.access.ProgramCall;
import com.ibm.as400.access.ProgramParameter;
import com.ibm.as400.access.QSYSObjectPathName;

import lombok.extern.slf4j.Slf4j;

@Controller
@Slf4j
@RequestMapping("rpgcall")
public class RpgCall {

	@Value("${as400.hostname}")
	String hostName;
	@Value("${as400.username}")
	String userName;
	@Value("${as400.password}")
	String password;
	@Value("${as400.libraries}")
	String libraries;
	@Value("${as400.programlib}")
	String programLib;

	// 日本語(5035のサブセット)
	public static final int CCSID = 939; 

	@GetMapping
	public String initialView(Model model) {
		model.addAttribute("form", new RpgCallForm());
		model.addAttribute("msg", "");
		model.addAttribute("age", null);
		return "rpgcall";
	}

	@PostMapping
	public String mainProc(@ModelAttribute RpgCallForm form, Model model) throws Exception {
		String msg;
		BigDecimal age;
		
		char[] charPass = password.toCharArray();
		AS400 as400 = new AS400(hostName, userName, charPass);
		// ADDLIBLE
		if (!addLib(as400, libraries)) {
			log.error("ADDLIBLE Error:" + libraries);
			throw new Exception("ADDLIBエラー");
		}
		// CCSID
		if (!setCCSID(as400, CCSID)) {
			log.error("CCSID Error:" + CCSID);
			throw new Exception("CCSIDエラー");
		}
		
		// パラメータ設定
		ProgramParameter[] params = new ProgramParameter[4];
		AS400Text paramName = new AS400Text(50);
		AS400PackedDecimal paramBirth = new AS400PackedDecimal(8, 0);
		AS400Text paramMsg = new AS400Text(100);
		AS400PackedDecimal paramAge = new AS400PackedDecimal(3, 0);
		params[0] = new ProgramParameter(paramName.toBytes(form.getName()), 50);
		params[1] = new ProgramParameter(paramBirth.toBytes(form.getBirth()), 8);
		params[2] = new ProgramParameter(paramMsg.toBytes(""), 100);
		params[3] = new ProgramParameter(paramAge.toBytes(0), 3);
		
		// プログラム設定
		ProgramCall pgmCall = new ProgramCall(as400);
		pgmCall.setProgram(QSYSObjectPathName.toPath(programLib, "TEST2", "PGM"), params);
		
		// プログラム実行
		if (!pgmCall.run()) {
			AS400Message[] callMsgs = pgmCall.getMessageList();
			for (AS400Message callMsg: callMsgs) {
				log.debug(String.format("AS400 Error:%s", callMsg));
			}
		}
		
		// 戻り値設定
		msg = paramMsg.toObject(params[2].getOutputData()).toString();
		age = (BigDecimal)paramAge.toObject(params[3].getOutputData());
		
		as400.disconnectAllServices();
		
		model.addAttribute("form", form);
		model.addAttribute("msg", msg);
		model.addAttribute("age", age);
		return "rpgcall";
		
	}

	public boolean addLib(AS400 as400, String libs) throws Exception {
		if (libs.length() == 0) return true;
		
		for (String lib: libs.split(",")) {
			if (!runCommand(as400, String.format("ADDLIBLE LIB(%s)", lib))) {
				return false;
			}
		}
		return true;
	}

	public boolean runCommand(AS400 as400, String cmdValue) throws Exception {
		CommandCall cmd = new CommandCall(as400);
		return cmd.run(cmdValue);
	}

	public boolean setCCSID(AS400 as400, int ccsid) throws Exception {
		return runCommand(as400, String.format("CHGJOB CCSID(%d)", ccsid));
	}

}

プログラムの説明

Spring Boot関連の説明は省略します。

IBM i 接続設定
char[] charPass = password.toCharArray();
AS400 as400 = new AS400(hostName, userName, charPass);

ここでIBM iに接続するための設定を行っています。
AS400のパスワードはchar[]型なので、事前に変換しています。

ライブラリリスト設定
// ADDLIBLE
if (!addLib(as400, libraries)) {
	log.error("ADDLIBLE Error:" + libraries);
	throw new Exception("ADDLIBエラー");
}

throwは適当です。
ちゃんと例外処理をした方が良いです。

addLib
	public boolean addLib(AS400 as400, String libs) throws Exception {
		if (libs.length() == 0) return true;
		
		for (String lib: libs.split(",")) {
			if (!runCommand(as400, String.format("ADDLIBLE LIB(%s)", lib))) {
				return false;
			}
		}
		return true;
	}

application.propertiesas400.librariesはライブラリ名をカンマ区切りで持っているので、カンマで分割して個別にADDLIBLEコマンドを発行しています。

runCommand
	public boolean runCommand(AS400 as400, String cmdValue) throws Exception {
		CommandCall cmd = new CommandCall(as400);
		return cmd.run(cmdValue);
	}

共通のコマンド実行メソッドです。

CCSIDの設定
// 日本語(5035のサブセット)
public static final int CCSID = 939; 

CHGJOBに使用するので939ではなく5035で良いような気がしますが、試していません。

// CCSID
if (!setCCSID(as400, CCSID)) {
	log.error("CCSID Error:" + CCSID);
	throw new Exception("CCSIDエラー");
}

こちらもthrowは適当です。
ちゃんと例外処理をした方が良いです。

setCCSID
public boolean setCCSID(AS400 as400, int ccsid) throws Exception {
	return runCommand(as400, String.format("CHGJOB CCSID(%d)", ccsid));
}

CHGJOBコマンドを発行しています。

パラメータ設定
// パラメータ設定
ProgramParameter[] params = new ProgramParameter[4];
AS400Text paramName = new AS400Text(50);
AS400PackedDecimal paramBirth = new AS400PackedDecimal(8, 0);
AS400Text paramMsg = new AS400Text(100);
AS400PackedDecimal paramAge = new AS400PackedDecimal(3, 0);
params[0] = new ProgramParameter(paramName.toBytes(form.getName()), 50);
params[1] = new ProgramParameter(paramBirth.toBytes(form.getBirth()), 8);
params[2] = new ProgramParameter(paramMsg.toBytes(""), 100);
params[3] = new ProgramParameter(paramAge.toBytes(0), 3);

RPGのTEST2はパラメータは4つなので、new ProgramParameter[4]で4つ用意しています。
RPGのパラメータに合わせて、paramNameは文字50桁、paramBirthは数値8桁、paramMsgは文字100桁、paramAgeは数値3桁で定義します。
定義したパラメータをpramsの0〜3にセットします。
paramNameparamBirthはwebフォームの名前と生年月日をセットします。
paramMsgparamAgeは受け取り用なので空白と0をセットします。

試行錯誤してこの方法で上手くいったので、toBytesなど処理が複雑な気がしますが、こういうものだと諦めています。

プログラム設定
// プログラム設定
ProgramCall pgmCall = new ProgramCall(as400);
pgmCall.setProgram(QSYSObjectPathName.toPath(programLib, "TEST2", "PGM"), params);

実行するプログラムを設定します。
QSYSObjectPathName.toPath(programLib, "TEST2", "PGM")programLibでプログラムのライブラリを指定、"TEST2"でプログラム名を指定、"PGM"はタイプを指定します。
QSYSObjectPathNameを使用しない場合は、"QSYS.LIB/" + programLib + ".LIB/TEST2.PGM" と指定します。

プログラム実行
// プログラム実行
if (!pgmCall.run()) {
	AS400Message[] callMsgs = pgmCall.getMessageList();
	for (AS400Message callMsg: callMsgs) {
		log.debug(String.format("AS400 Error:%s", callMsg));
	}
}

実行部はpgmCal.run()のみですが、エラー処理としてメッセージを取得してログに出力しています。

実行してみる

プロジェクトを右クリックして…

デバッグを選択して…

3 Spring Boot アプリケーションを選択します。


コンソールにこのように表示されて、特にエラーらしきものが無ければ成功です。
ブラウザで http://localhost:8080/rpgcall を開きます。


この様に表示されたら、名前・生年月日を入力して、送信ボタンを押してメッセージと年齢が表示されたら成功です。

Discussion