(CGI1)IBMiのAPIサーバをFFRPGのCGIで作成する(1)

# <u>■概略</u>
オープンソースエンジニア歴30年超の筆者が2023年からIBMiを学びだした学習記録です
IBMiをAPIサーバ(JSON)にするために、FFRPGでCGIを書いてみます

# <u>■IBMiでCGI</u>
### ◯CGIサンプルソース(RPGLE)
CGIはCommon Gateway Interfaceの略でWWWサーバが動的コンテンツを返すプログラムを
呼び出す規格

＊標準入力からパラメータを読込む
＊標準出力にHTML,JSON等出力しWWWサーバ経由で返す

プログラムが標準入出力を扱える必要があるが、IBMiでは標準入出力が扱えない  
そこで **qhttpsvr/qzhbcgi** というIBM提供のサービスプログラムを利用して
標準入出力を扱えるようにしたサンプルが下記  

参考URL : [https://www.ibm.com/support/pages/ile-rpg-cgi-programming-example](https://www.ibm.com/support/pages/ile-rpg-cgi-programming-example)
※ソースコードは上記リンクのsamplerpg.txt

### ◯FFRPGでCGIサンプルソース(RPGLE)を書き換える
＊上記はRPGLEなのでFFRPGに書き換えた  
＊オープンソースの書き方にリファクタリングして、  
　getCGIVars()とresponseHTTP()の関数にまとめてサービスプログラム化した  
＊getCGIVars()はCGIから読み込んだGET/POSTのパラメータをキー・バリューで取得する
＊responseHTTP()はHTTPプロトコルで標準出力に出力する
＊getCGIVars()とresponseHTTP()のソースコードは後述する
＊呼出のソースは「(CGI1)IBMiのAPIサーバをFFRPGのCGIで作成する(1)」で記載する

### ◯HTTPプロトコル
responseHTTP()でCGIプログラムがコンテンツを応答する際、先頭に

Content-type: text/html\n\n(改行が2つ)  

をつける必要がある  
EBCDICでの改行は x'15' である  

### ◯GETとPOST
CGIの呼出し方法は2つある  

**GET**  
1行のURLで呼び出す  
http://{ホスト名}/cgi-bin/{PGM名}.PGM?param1=value1&param2=value2  
PGM名の後 ? でパラメータ開始  
param1=value1 の形式になり、起動や漢字はURLEncodeingする  
param2以後は & でつなげる  

**POST**  
パラメータはURLには現れない  
以下のHTMLで表示される、「送信」ボタンを押すとPGM名が呼び出される  
param1とparam2がparam=valueで渡される  
POSTはHTMLからのページ遷移が必要なので、API利用には原則GETを利用することになる  
ただし元のサンプルがPOST実装もしてあったのでPOSTの値も受け取れる  
 
呼出し元HTMLソース  
```
<form action=/cgi-bin/{PGM名}.PGM method=post>
　<input type=text name=param1 value="value1">
　<input type=text name=param2 value="value2">
　<input type=submit value="送信">
</form>
```

# <u>■FFRPGでCGIのサービスプログラム</u>
### ◯先頭部分
```
**free
// 2024/06/26 えむぼま高見禎成 作成
//
//【注意】
// POST & GETの読み込み最大長は1024バイト(prototype.ffrpgで指定)
// CGIの出力最大長は2048000バイト=2MB(prototype.ffrpgで指定)
// CGIで引き渡すパラメータは最大10まで(prototype.ffrpgで指定)
// Qtmh*呼出し時はvarchar不可。charでないとCGI実行エラーになる!
//
**free
ctl-opt nomain;
ctl-opt text('CGI共通ライブラリ');

/include ./prototype.ffrpg
```
### ◯getCGIVars()
```
// POST or GETから値を取得するサブプロシージャー
dcl-proc getCGIVars export;
    // 引数
    dcl-pi *n;
        CGIkeys   char(bufinlen) dim(cgivarsnum);
        CGIvalues char(bufinlen) dim(cgivarsnum);
    end-pi;

    // 変数
    dcl-s  datalen    bindec(9:0) inz(0);
    dcl-s  maxdatalen bindec(4:0) inz(1024);
    dcl-s  BufIn      char(bufinlen);
    dcl-s  i          packed(4:0);
    dcl-s  keyvalue   varchar(bufinlen);
    dcl-s  key        varchar(bufinlen);
    dcl-s  value      varchar(bufinlen);
    dcl-s  and_pos    packed(4:0) inz(0);
    dcl-s  eq_pos     packed(4:0) inz(0);
    dcl-s  count      packed(4:0) inz(1);
    // QtmhRdStIn(POST)
    dcl-s  StdIn      char(bufinlen);
    dcl-s  StdInLen   bindec(9:0);
    // QtmhGetEnv(GET)
    dcl-s  EnvIn      char(bufinlen);
    dcl-s  EnvLen     bindec(9:0);
    dcl-s  EnvName    char(25)    inz('CONTENT_LENGTH');
    dcl-s  APIErr     char(8000);
    dcl-s  tmp        char(bufinlen);

    // 環境変数REQUEST_METHODを読込む
    EnvName = 'REQUEST_METHOD';
    QtmhGetEnv(EnvIn : %len(EnvIn) : EnvLen : EnvName : %len(%trim(EnvName)) : APIErr);
    // POSTならCONTETN_LENGTH分標準入力から読込む
    if %subst(EnvIn:1:4) = 'POST';
        // 環境変数CONTENT_LENGTHを読込み最大長より大きければ最大長にする
        EnvName = 'CONTENT_LENGTH';
        QtmhGetEnv(EnvIn : %size(EnvIn) : EnvLen : EnvName : %len(%trim(EnvName)) : APIErr);
        //debug('envin='+EnvIn : 'TAKAMI');
        //debug('envlen='+%char(EnvLen) : 'TAKAMI');
        datalen = %dec(%subst(EnvIn:1:EnvLen):9:0);
        if maxdatalen < datalen;
            datalen = maxdatalen;
        endif;
        // 標準入力を読込む
        QtmhRdStin(StdIn : datalen : StdInLen : APIErr);
        BufIn = StdIn;
        datalen = StdInLen;
    // POST以外=:GETならQUERY_STRINGから読込む。最大長より大きければエラーメッセージにする
    else;
        EnvName = 'QUERY_STRING';
        QtmhGetEnv(EnvIn : %size(EnvIn) : EnvLen : EnvName : %len(%trim(EnvName)) : APIErr);
        if EnvLen > maxdatalen;
            BufIn = 'Data buffer not big enough for available input data';
            datalen = %len(BufIn);
        else;
            BufIn = EnvIn;
            datalen = EnvLen;
        endif;
    endif;

    // BufInの分解
    for i = 1 to %len(BufIn);
        and_pos = %scan('&' : BufIn : i);
        // &までをキー値ペアで取り出す
        if and_pos > 0;
            keyvalue = %subst(BufIn : i : and_pos - i );
            //debug('keyvalue:'+keyvalue+' i='+%char(i)+' and_pos='+%char(and_pos) : 'TAKAMI');
            // = でキーと値を分ける
            eq_pos   = %scan('=' : keyvalue : 1);
            if eq_pos > 0;
                key   = %subst(keyvalue : 1 : eq_pos - 1 );
                value = %subst(keyvalue : eq_pos + 1 : %len(keyvalue) - eq_pos );
                CGIkeys(count)   = key;
                CGIvalues(count) = value;
                //debug('key:'+key+' eq_pos='+%char(eq_pos) : 'TAKAMI');
                //debug('value:'+value+' eq_pos='+%char(eq_pos) : 'TAKAMI');
            endif;
            i = and_pos;
            count += 1;
        // &がなければ末尾までをキー値ペアで取り出す
        else;
            keyvalue = %subst(BufIn : i : %len(BufIn) - i);
            //debug('keyvalue:'+keyvalue+' i='+%char(i)+' and_pos='+%char(and_pos) : 'TAKAMI');
            // = でキーと値を分ける
            eq_pos   = %scan('=' : keyvalue : 1);
            if eq_pos > 0;
                key   = %subst(keyvalue : 1 : eq_pos - 1 );
                value = %subst(keyvalue : eq_pos + 1 : %len(keyvalue) - eq_pos );
                // 末尾の場合x'00'が入っていることがあるので除去する
                value = %trim(%replace('':value:%scan(x'00':value) : 1));
                CGIkeys(count)   = key;
                CGIvalues(count) = value;
                //debug('key:'+key+' eq_pos='+%char(eq_pos) : 'TAKAMI');
                //debug('value:'+value+' eq_pos='+%char(eq_pos) : 'TAKAMI');
            endif;
            leave;
        endif;
    endfor;
    return;
end-proc;
```

### ◯responseHTTP()
```
// 標準出力に出力するサブプロシージャー
dcl-proc responseHTTP export;
    // 引数
    dcl-pi *n;
        BufOut  char(bufoutlen);
    end-pi;

    // 定数
    dcl-c  constNL    x'15'; // EBCDICのNewlineを送ると改行になる。LFやCRではない
    dcl-s  httpHEADER varchar(80) inz('Content-type: text/html');
    // 変数
    dcl-s  APIErr     char(8000);

    // 先頭にHTTPヘッダーを追加する
    httpHEADER = httpHEADER + constNL + constNL;
    BufOut = httpHEADER + BufOut;
    QtmhWrStout(%trim(BufOut) : %size(%trim(BufOut)) : APIErr);
    return;
end-proc;
```

### ◯debug()
```
// デバッグプリント
// 表示メッセージは40桁まで
dcl-proc debug export;
    dcl-pi *N;
        message varchar(40) const; // デバッグ表示する文字列
        msgq    varchar(10) const; // 出力先メッセージキュー
    end-pi;

    dcl-s dt date(*JIS);
    dcl-s tm time(*JIS);
    dcl-s timestamp varchar(19);
    dcl-s debug varchar(50);

    dt  = %DATE();
    tm  = %TIME();
    //timestamp = %CHAR(dt) + ' ' + %CHAR(tm);
    timestamp = %CHAR(tm);
    debug = '[' + timestamp + ']' + message;
    dsply debug msgq;
end-proc;
```

### ◯prototype.ffrpg
```
**free
// 2024/05/14 えむぼま高見禎成 作成
//
// CGI(qhttpsvr/qzhbcgi)
//
dcl-c  END_OF_FILE  const('02000');
dcl-c  SYNTAX_ERROR const('42601');
dcl-c  BufInLen   1024;
dcl-c  BufOutLen  2048000;
dcl-c  CGIVarsNum 10;

dcl-pr QtmhRdStin ExtProc('QtmhRdStin');
    *n char(BufInLen);
    *n bindec(9:0) const;
    *n bindec(9:0);
    *n char(8000);
end-pr;
dcl-pr QtmhWrStout ExtProc('QtmhWrStout');
    *n char(BufOutLen)  const;
    *n bindec(9:0) const;
    *n char(8000);
end-pr;
dcl-pr QtmhGetEnv ExtProc('QtmhGetEnv');
    *n char(BufInLen);
    *n bindec(9:0) const;
    *n bindec(9:0);
    *n char(25)    const;
    *n bindec(9:0) const;
    *n char(8000);
end-pr;

// cgilib
dcl-pr getCGIVars;
    *n char(BufInLen) dim(CGIVarsNum);
    *n char(BufInLen) dim(CGIVarsNum);
end-pr;
dcl-pr responseHTTP;
    *n char(BufOutLen);
end-pr;

// デバッグプリント
dcl-pr debug;
    message varchar(40) const; // デバッグ表示する文字列
    msgq    varchar(10) const; // 出力先メッセージキュー
end-pr;
```

# ■サービスプログラムのコンパイルbash
### ◯個人ライブラリへ出力
```
#!/bin/bash
USER=`whoami`
#---
TARGET_LIB=${USER}"LIB"
MOD1="CGILIB"
SRVPGM="qhttpsvr/qzhbcgi"
DIR="/home/${USER}/CGIs/QFFRPGSRC"
#---
# CGIモジュール
file=$DIR"/"$MOD1".SRVFFRPG"
target=$TARGET_LIB"/"$MOD1
system "DLTMOD MODULE($target)"
system "CRTRPGMOD MODULE($target) SRCSTMF('$file') TGTCCSID(5035)" 2>&1

# サービスプログラム
system "DLTSRVPGM SRVPGM($target)"
system "CRTSRVPGM SRVPGM($target) MODULE($target) BNDSRVPGM($SRVPGM) EXPORT(*ALL)" 2>&1
```

### ◯developブランチをCGILIBへ出力
```
#!/bin/bash
USER=`whoami`
#---
TARGET_LIB='CGILIB'
MOD1="CGILIB"
SRVPGM="qhttpsvr/qzhbcgi"
DIR="/home/${USER}/CGIs/QFFRPGSRC"
#---
# gitでdevelopに変更し最新に更新する
git checkout develop && git pull origin --ff-only

# CGIモジュール
file=$DIR"/"$MOD1".SRVFFRPG"
target=$TARGET_LIB"/"$MOD1
system "DLTMOD MODULE($target)"
system "CRTRPGMOD MODULE($target) SRCSTMF('$file') TGTCCSID(5035)" 2>&1
# サービスプログラム
system "CRTSRVPGM SRVPGM($target) MODULE($target) BNDSRVPGM($SRVPGM) EXPORT(*ALL)" 2>&1

# 元のbranchに戻る
git checkout -
```