RPGの新命令DATA-INTO: XML-INTOとOpen Accessの出会い
2018年2月 | ジョン・パリス&スーザン・ガントナー この記事では、RPG言語への最新の追加機能であるDATA-INTOについて紹介します。表題で示唆したように、DATA-INTOはOpen AccessとXML-INTOの組み合わせと捉えるのがおそらく最善の考え方です。XML-INTOはデータを受け取り、それを対応するRPGのデータ構造に展開します。Open Accessは、あなたが書いた、またはサードパーティーから提供されたカスタムハンドラを利用して、任意のソースからのデータをファイルデータのように扱います。DATA-INTOは、XML-INTOのようにデータ構造(または配列)にデータを配置しますが、Open Accessが行うようにカスタムパーサーを使用して、どのデータをどこに格納するか見つけ出します。 IBMはなぜこの最新機能を導入したのでしょうか?一つには、少なくともXML-INTOに相当するJSONのサポートを数多く要望されてきているからです。もちろん、CSVからキーワードとテキストのペアに至るまで、他にもネイティブ言語のサポートによって恩恵を受ける可能性のある多くの文字データ形式があります。誰が次の“ホットな”データ交換形式を知り得るでしょう。何だかんだ言っても、10年前にどれほどの人がJSONのことを耳にしたことがあったでしょうか?けれども、JSONはデータ交換の世界を支配するようになりました。これはXMLの役割と考えられていましたが、このせわしないビジネス環境では物事は急速に変化します。 ですから、このことを念頭に置いて、IBMはOpen Accessで設定した道を進み、この新しい機能を言語拡張点として設計しました。これにより、企業は独自のパーサーを書くことができるだけでなく、サードパーティーやオープンソース・グループがさまざまなデータ交換フォーマット用のパーサーを提供できるようになります。率先して事を始めるために、IBMは多くのサンプル・パーサーを提供しています。多くは主に教材用として提供されていますが、完全なJSONパーサーも提供しており、これはJSON-INTO機能を求めている人たちを喜ばせるはずです。 言語に機能を追加するこのアプローチは、今後もRPGが歩み続けると期待されるものです。つまり、単に限られた機能を持つ新しい機能を追加するのではなく、IBMのリソースを使用して言語の拡張を容易にするというアプローチです。 導入部はこれで十分でしょう。実装を見てみましょう。
基本的な構文
DATA-INTOの基本的な構文は、XML-INTOで使用されている構文に大変よく似ています。 しかし、すぐに気付く違いの1つは、XML-INTOでは%XMLを使用するのに対し、%DATAを使用する点です。名前の違いとは別に、(使用するデータとオプションのソースを特定するための)パラメーターの基本的な目的は、XML-INTOとほとんど同じです。 本質的な違いは、第3のパラメーター%PARSERにあります。これは、構文解析操作を実行するプログラム(またはサブプロシージャー)を識別するために使用されます。これがOpen Accessとの第一の類似点です。基本構文
DATA-INTO {(EH)} 受け取り変数- %DATA(ドキュメント{:オプション})
- %PARSER(パーサー{:パーサーオプション}};
DATA-INTO accounts %DATA('Sample1.csv': 'doc=file case=any') %PARSER('*LIBL/PARSECSV1');上の例では、accountsというデータ構造(DS)にsample1.csvファイルから抽出されたデータがロードされています。そのファイル内のフィールドとそれに関連する値を識別する作業は、PARSECSV1というパーサーによって実行されます。構文ダイアグラムで分かるように、呼び出し元は、パーサーオプション・パラメーターを使用して、追加のパラメーター情報をパーサーに渡すこともできます。 ここで示した基本的な構文に加えて、XML-INTOと同様にDATA-INTOにも、処理データの量がRPGの容量制限を超えるインスタンス、または利用可能な限り単純にデータの各部分を処理したいインスタンスに対して%HANDLERの変異形が準備されています。XML-INTOと共に%HANDLERを使う方法に馴染みがない場合は、この記事でそれに関するすべてを見つけることができます。DATA-INTOと共に%HANDLERを使用する例については今後別の記事で述べるつもりです。
パーサーはどのように動作するか?
XML-INTOがどのように動作するかを少し考えてみましょう。コンピューター内部では、IBM提供のXMLパーサーが文書から名前と値を抽出し、一致する名前と階層を持つRPG変数にその値を格納します。DATA-INTOとの唯一の本質的な違いは、DATA-INTOではIBMのランタイム・ロジックが名前と値の解析を行うのではなく、解析作業を行うためのロジックをユーザーが提供する必要があることです。関連する値を特定したら、ユーザー作成のパーサーはRPGランタイムに通知し、RPGランタイムがその値を格納します。 DATA-INTOを呼び出すと、まずRPGランタイム・ロジックは指定した変数またはファイルからデータを抽出し、バッファーに格納します。次に、そのバッファーのアドレスとその長さおよび他の識別情報をユーザー作成のパーサーに渡します。 制御を受け取ると、パーサーはバッファーを処理し、見つけたものをRPGに通知します。これを行うために、パーサーはデータ構造、名前、およびそれらの値についてRPGに通知するための多くのプロシージャーを呼び出すことができます。後で簡単な例を見るときに、これらのプロシージャーについてもう少し詳しく説明します。しかし、差し当たってすべてのパーサーが使用する主要なものを、一般的に使用される順序で見てみましょう。 パーサーは、処理が開始されたことをRPGに通知するためにQrnDiStart()を呼び出して開始しなければなりません。 次のステップでは通常、RPGに処理中の項目の名前を通知するためにQrnDiReportName()を呼び出します。XML-INTOの場合と同様、名前は重要であり、この名前はターゲット構造の名前と一致している必要があります。 通知された名前がデータ構造(DS)名の場合、次に呼び出すのはQrnDiStartStruct()です。一方、名前が配列名の場合は、代りにQrnDiStartArray()を呼び出します。 当面、仮にDSを開始しているとすると、通常次にQrnDiReportName()を呼び出してDS内のフィールドを識別し、続いてすぐにQrnDiReportValue()を呼び出してそのフィールドに格納するようRPGに値を通知します。この一対の呼び出しは、DS内のすべてのフィールドに対して繰り返されます。 すべてのフィールドが処理されると、パーサーはQrnDiEndStruct()を呼び出して構造体(DS配列の場合は配列要素)の処理が完了したことをRPGに通知します。 最後に、パーサーはQrnDiFinish()を呼び出して作業が完了したことをRPGに通知します。これは、パーサーが終了したら元のプログラムに制御を戻すようRPGに指示します。 この時点で、メインプログラムのDATA-INTO操作の次の命令に制御が戻され、かつデータはすべて関連する変数にきちんと格納されており、ユーザーがそれを処理するのを待っているはずです。 ユーザー作成のパーサーが、存在しないフィールド名を識別すると、データがターゲットと一致しないことを示すエラーが通知されます。XML-INTOの場合と同様に、これはDATA-INTOでallowextra = yesオプションを指定することで回避できます。同様に、allowmissing = yesを使用して、フィールド値の欠落のようなエラーを回避することもできます。さまざまなオプションについての詳細は、今後の記事で紹介する予定です。パーサーの例
IBMの例の1つを使用するのではなく、単純なCSVファイルを解析するプログラムを作成することにしました。呼び出し元プログラムは次のようになります。dcl-ds accounts Qualified Dim(10) inz; account char(4); name char(20); end-ds; dcl-ds pgmStat psds; numElements int(20) pos(372); end-ds; dcl-s b char(1); dcl-s i int(10); data-into accounts %data('Sample1.csv': 'doc=file case=any ccsid=job') %parser('*LIBL/PARSECSV1'); for i = 1 to numElements; dsply ( 'Account: ' + accounts(i).account + ' Name: ' + accounts(i).name); endfor;上に示したように、このプログラムはファイル ‘Sample1.csv’のデータをaccountsというDS配列にロードします。この配列には、accountとnameという2つのサブフィールドがあります。CSVファイルsample1.csvには、次のようなデータが含まれています。
- 1234,Jones
- 2345,Smith
- 3456,Gantner
- 4567,Paris
パーサーPARSECSV1を研究する
パーサーで使用される基本的なデータ定義は次のとおりです。(A)// DSサブフィールドの名前 dcl-s subfieldName_Account varchar(15) inz('account'); dcl-s subfieldName_Name varchar(15) inz('name'); (B)//サブフィールドの値 dcl-s account varchar(10); dcl-s name varchar(30); (C)//レコードの場所と内容の変数 dcl-s record varchar(256); dcl-s separator int(5); dcl-s pcurrentPosn pointer;(A)では、データを格納するDSサブフィールドの名前を定義します。前述したように、データにはフィールド名が存在しないので、パーサーがフィールド名を提供する必要があることを思い出してください。将来お見せする例では、この問題を回避するために列名を使用する方法を紹介する予定ですが、当面は簡単な例を示します。フィールドは可変長(varchar)として定義されていることに注意してください。後で名前とその長さの両方が必要になるのですが、%lenを使ってこれらを簡単に取得するため、このように定義しました。このやり方によって、名前の末尾の空白を常に削除し忘れないようにします。そうしないと、RPGランタイム内でフィールド名の不一致が発生します。 accountフィールドとnameフィールドの値の格納場所は(B)で定義されています。これらは可変長フィールドでもあります。これには、値の長さの取得が容易になることに加え、もし呼び出し元プログラムのDS配列内のターゲットフィールドの1つの長さが変化しても正しく処理され、フィールドの後方に多くの不要なブランクを含まないという利点があります。 (C)の変数は、現在のレコード、コンマ区切りの位置、最初または次のレコードが始まるバッファー内の位置を格納するのに使用されます。 基本的な変数定義を見てきたので、今度はプログラムロジックを見てみましょう。見てのとおり、前節で概説したパターンに非常に密接に沿ってそれは進みます。 まず、最初のステップ(D)ではプログラムコードがすべてのQrnDixxxプロシージャーにアクセスできるように環境ポインターをコピーすることです。これについて心配する必要はありません。単にすべてのパーサーの初めにそれが必要なだけです。同様に、(E)では、RPGが提供したデータバッファーへのポインターを現在のレコード位置ポインターにコピーします。 実際の作業は、(F)のQrnDiStartプロシージャーの呼び出しで始まります。ここで渡されるパラメーターは、RPGがこの特定のDATA-INTO操作を一意に識別するために与えた“ハンドル”です。この後で示すように、これ以降QrnDixxxプロシージャーのいずれかを呼び出すたびに、この“ハンドル”値を含める必要があります。 (G)では、QrnDiStartArrayプロシージャーを呼び出して、RPGにデータが配列であることを通知します。この例ではファイル中には配列名がないので、QrnDiReportNameプロシージャーを使って配列名を通知するステップを省略し、名前のない配列であることを単に通知しています。RPGは、DATA-INTO操作からターゲットとして使用される名前を得ます。 ループ(I)内でコンマ区切りの位置を検索し、そのレコードを複合フィールドに分割します。データを分離したら、RPGに“発見した”ものを伝えます。
//コールバック関数へのアクセスを有効にする (D) pQrnDiEnv = parm.env; //現在のレコード位置をバッファーの先頭に設定する (E) pCurrentPosn = parm.data; //解析を開始する (F) QrnDiStart (parm.handle); //データが配列を表すことをRPGに通知する (G) QrnDiStartArray(parm.handle); //構文解析する最初のデータセットを取得する (H) record = getRecord(pcurrentPosn); DoW record <> ''; //レコード内のカンマの位置を見つけて値を抽出する (I) separator = %scan(',': record); account = %subst(record: 1: separator - 1); name = %subst(record: separator + 1);配列要素ごとに、配列要素の先頭をRPGに通知するために(J)のようにQrnDiStartStructプロシージャーを呼び出す必要があります。 次に、(K)でQrnDiReportNameとQrnDiReportValueを呼び出して、抽出したデータの名前と値をRPGに通知します。名前と値に可変長フィールドを使用しているので、RPGに渡す必要のあるアドレスはフィールドのデータ部分の先頭にあります。ですから、%addr関数で修飾子*dataを使用することに注意してください。 値が設定されたら、DSのこの要素の終わりに達したことをRPGに通知するために(L)でQrnDiEndStructを呼び出します。それから次のレコードに移り、処理を繰り返します。
(J)//構造体の反復の開始をRPGに通知する QrnDiStartStruct (parm.handle); //最初のサブフィールド 'account'の名前を報告する (K) QrnDiReportName (parm.handle : %addr(subfieldName_Account: *data) : %len(subfieldName_account) ); //そしてRPGに関連する値を与える QrnDiReportValue(parm.handle : %addr(account: *data) : %len(account) ); //サブフィールド 'name'でプロセスを繰り返す。 QrnDiReportName (parm.handle : %addr(subfieldName_Name: *data) : %len(subfieldName_Name) ); QrnDiReportValue(parm.handle : %addr(name: *data) : %len(name) ); //構造体のこの繰り返しを終了する (L) QrnDiEndStruct (parm.handle); //構文解析する別のデータセットを取得する record = getRecord(pcurrentPosn); EndDo;最後に、すべてのレコードが処理され、Doループが終了したら、(M)でQrnDiEndArrayを呼び出して配列を完成させます。 最後のステップ(N)でQrnDiFinishを呼び出し、RPGに解析が完了したことを通知します。そして単純にパーサーから戻り、RPGは「整理」を行い、DATA-INTO操作の後に続く命令文に制御を戻します。
//配列の終わりをRPGに通知する (M) QrnDiEndArray(parm.handle); //構文解析を終了することをRPGに通知する(これ以上データは無い) (N) QrnDiFinish(parm.handle);これですべてです。今、データはすべてaccountsというDS配列に安全に格納されており、制御が元のプログラムに戻ったらプログラムの先に進んで好きなやり方で要素を処理できます。ここで示したサンプルプログラムの場合、単にループしてすべての要素の値を表示するだけです。