In den ersten Teilen des Artikels haben wir uns mit dem Schreiben von Dateien im IFS befasst, wie man es bei der Generierung von XML- oder CSV-Dateien direkt aus RPG-Anwendungen benötigt. Das Lesen von Streamfiles ermöglicht nicht nur die Verarbeitung von CSV-Dateien oder ähnlicher Schnittstellen in RPG-Programmen, ohne den Umweg über CPYFRMSTMF, CPYFRMIMPF oder gar CPYFRMPCDOC zu gehen, sondern auch die Verarbeitung von Property-Dateien, wie sie unter Unix oder in Java-Anwendungen oft für das Abspeichern von Konfigurationen genutzt werden.

Die Prototypen der C-APIs

Vor die Verwendung der C-APIs haben die ILE-Götter die Prototypen gesetzt, die man durch die Übertragung der Prototypen der entsprechenden C-Funktionen erhält. Zum Lesen von Streamfiles werden die Funktionen open, read und close benötigt. Die RPG-Namen dieser Funktionen müssen gegenüber den originalen C-Namen verändert werden, um den Free-Format RPG-Compiler vor Verwirrung zu bewahren.

 
      /IF NOT DEFINED (IFSAPIS_QRPGLEH)
      /DEFINE IFSAPIS_QRPGLEH
      *-----------------------------------------------------
     D open_           PR            10I 0 EXTPROC('open')
      * return: filedescriptor
      *         -1 = error
     D path                            *   VALUE OPTIONS(*STRING)
     D openFlags                     10I 0 VALUE
     D accessRights                  10U 0 VALUE OPTIONS(*NOPASS)
     D codePage                      10U 0 VALUE OPTIONS(*NOPASS)
      *-----------------------------------------------------
     D read_           PR            10I 0 EXTPROC('read')
      * return: length
      *         -1 = error
     D fileDescriptor                10I 0 VALUE
     D dataPointer                     *   VALUE
     D lengthOfData                  10U 0 VALUE
      *-----------------------------------------------------
     Dclose_           PR            10I 0 EXTPROC('close')
      * return:  0 = OK
      *         -1 = error
     D fileDescriptor                10I 0 VALUE
      /ENDIF

Die open- und close-Funktionen haben wir bereits beim Schreiben in Streamfiles benutzt, der Prototyp zur Verwendung von read muss noch in der Copy-Strecke ergänzt werden. Der Datei-Deskriptor, den open zurückliefert, wird ebenso als Parameter übergeben wie ein Pointer auf den Lesepuffer, in den dann die Daten gestellt werden sollen. Als letzter Parameter wird die Länge des Lesepuffers übergeben. Die Funktion read stellt die Daten in den Lesepuffer ein und gibt die Anzahl der übertragenen Bytes zurück; im Fehlerfall wird der Wert -1 zurückgegeben; bei jedem Lesen wird der Dateizeiger entsprechend weiter geschoben.

Für das Öffnen der Datei werden noch die entsprechenden Flags benötigt. Bei Textdateien kommt man dabei mit zwei Stück aus, die meist kombiniert verwendet werden. Für die vollständige Aufstellung wird auf die Referenz für die IFS-APIs verwiesen.

      * Daten nur lesen
     DO_RDONLY         C                   CONST(1)
      * Datei enthält Textdaten, die konvertiert werden sollen
     DO_TEXTDATA       C                   CONST(16777216)

Zur Kombination mehrerer Flags werden diese einfach addiert.
Das Lesemodul INSTREAM orientiert sich von der Struktur am Java-Vorbild FileInputStream, ohne dessen Einfachheit und Eleganz zu erreichen. Was man in RPG doch eher mühsam selber schreiben muss, bekommt man in Java alles fix und fertig als Komponente. Das ist einer der Gründe, weshalb die Programmierer-Produktivität in Java meist größer sein kann als in RPG.

Ein RPG FileInputStream

Die Prototypen der exportierten Prozeduren des RPG-Moduls INSTREAM werden in einer Teildatei gleichen Namens in der Quelldatei QRPGLEH gespeichert und per /COPY-Befehl in die eigentliche Quelle des Moduls ebenso wie in die verwendenden Programme eingebunden. Die Copy-Strecken werden mittels einer Compiletime-Variable gegen doppeltes Kopieren geschützt.

      /IF NOT DEFINED (INSTREAM_QRPGLEH)
      /DEFINE INSTREAM_QRPGLEH
     /*===================================================================*/
     /*  INSTREAM                                                         */
     /*     lesen StreamFile                                              */
     /*     analog zu FileInputStream                                     */
     /*-------------------------------------------------------------------*/
     D setInStream     pr              n   EXTPROC('INSTREAM_setInStream')
     /* geöffnete Datei wird geschlossen                                  */
     /* prüft auf vorhanden sein returns TRUE if exists, else FALSE       */
     /* öffnete Datei                                                     */
     D path                         128a   value
     d                                     varying
     /*-------------------------------------------------------------------*/
     D readStream      pr          1024a   EXTPROC('INSTREAM_readStream')
     d                                     varying
     /* liest bis zu 1024 Byte                                            */
     /* am Dateiende entsprechend weniger und Datei wird geschlossen      */
     /*-------------------------------------------------------------------*/
     D readLnStream    pr          1024a   EXTPROC('INSTREAM_readLnStream')
     d                                     varying
     /* liest eine Zeile (max1024 byte)                                   */
     /* am Ende wird Blank zurückgegeben und automatisch geschlossen     */
     /*-------------------------------------------------------------------*/
     D getLength       pr            10i 0 EXTPROC('INSTREAM_getLength')
     /* gibt die zuletzt gelesene Länge zurück                            */
     /* wird benötigt, um folgende Blanks zu erkennen                     */
     /*-------------------------------------------------------------------*/
     D getEof          pr              n   EXTPROC('INSTREAM_getEof')       
     /* Test End of file                                                  */
     /*-------------------------------------------------------------------*/
      /ENDIF

Die Prozedur setInStream bekommt als Parameter den qualifizierten Dateinamen übergeben, wobei die maximale Länge des Pfads mit 128 Byte vorgesehen ist. Zunächst wird geprüft, ob diese Datei vorhanden ist; gegebenenfalls wird eine vorher geöffnete Datei geschlossen. Bei erfolgreicher Prüfung wird TRUE zurückgegeben, anderenfalls FALSE. Zum Schluss wird die Datei automatisch geöffnet und das Modul ist bereit zum Anfordern von Daten.

Zum Lesen sind zwei Varianten vorgesehen. Die Prozedur readStream liest einen Block von maximal 1024 Byte und liefert diesen als String variabler Länge zurück: Am Dateiende wird entsprechend weniger gelesen und die Datei wird automatisch geschlossen. Ist die Länge der zurückgegebenen Variablen kleiner als 1024, kann mit der Prozedur getLength ermittelt werden, ob Blanks am rechten Rand abgeschnitten wurden oder ob ein Dateiende vorliegt.

Die alternative Variante readLnStream liest eine Zeile, wobei die Zeilenwechsel-Zeichen (CR und LF) automatisch entfernt werden. Diese Lesemethode liefert am Dateiende Blanks zurück und schließt Streamfile ebenfalls automatisch. Mit der Prozedur getEof kann abgefragt werden, ob das Dateiende erreicht ist.

In der Quelle des Moduls sind zu Beginn in einem Kommentar-Eintrag die Befehle eingetragen, die zur Erstellung des Service-Programms führen. Im ersten Schritt wird ein Modul erstellt, das dann im zweiten Schritt zu einem Service-Programm gebunden wird. Ich habe mich beim Binden für den einfachsten Weg entschieden und auf den Einsatz von so genannter Binder Language und manueller Verwaltung von Signaturen verzichtet. Die Angabe von *ALL beim Parameter EXPORT macht bei Änderung von Exporten ein erneutes Binden der Teile der Anwendung erforderlich, die dieses Service-Programm gebunden haben. Wenn ich hier mehr Flexibilität haben will, dann wird zur Laufzeit dynamisch gebunden, was ohnehin eleganter ist und keine Geschwindigkeitsverluste nach sich zieht.
Alle benötigten Prototypen werden per Copy-Anweisung eingebunden. Neben den eigenen Prozedur-Exporten sind das noch die Deklarationen der C-APIs für die Streamfile-Verarbeitung und die C-Funktion access, die zum Prüfen des Vorhandenseins der Datei benutzt wird und im ersten Teil des Artikels ausführlich beschrieben wurde.

     H nomain
     D*B   CRTRPGMOD INSTREAM
     D*B+       DBGVIEW(*SOURCE)
     D*B   CRTSRVPGM INSTREAM
     D*B+       EXPORT(*ALL)
     D*B+       ACTGRP(*CALLER)
     D*B+       BNDDIR(QC2LE)
     /*-------------------------------------------------------------------*/
     /* Prototypen Exporte
      /COPY QRPGLEH,INSTREAM
     /*-------------------------------------------------------------------*/
     /* Prototypen Import
      /COPY QRPGLEH,ACCESS
      /COPY QRPGLEH,IFSAPIS
     /*-------------------------------------------------------------------*/
     /* lokale Prototypen
     d openFile        pr              n
     d closeFile       pr              n
     d fillBuf         pr
     /*-------------------------------------------------------------------*/
      * Konstanten
     D TRUE            C                   *ON
     D FALSE           C                   *OFF
     D CR              C                   x'0D'
     D LF              C                   x'25'
     D CRLF            C                   x'0D25'
     /*-------------------------------------------------------------------*/
     /* Zustands Variablen
     /*-------------------------------------------------------------------*/
     d path            s            128a
     d fileD           s             10i 0 inz(-1)
     d buffer          s           2048a
     d bufLeng         s             10i 0
     d eof             s               n   INZ(TRUE)
     d lengthProv      s             10i 0
     /*-------------------------------------------------------------------*/

Im globalen Deklarationsteils des Moduls werden ebenfalls die Prototypen der lokal verwendeten Prozeduren openFile, closeFile und fillBuff beschrieben, deren Funktionalität wir uns bei der Implementierung im Detail ansehen werden. In den Konstanten-Deklarationen sind TRUE und FALSE definiert, die ich der Verwendung der Begriffe *ON und *OFF vorziehe, da diese mich zu sehr an Bezugszahlen erinnern. Die letzte Konstante CRLF enthält Carriage Return und Line feed und findet als Zeilentrenner Verwendung.

Als globale Variablen sind wie beim Outstream die Zustandsgrößen path für den Pfad und fileD zur Aufnahme des File Descriptors deklariert, letzterer wird mit -1 vorbelegt. Die Variable buffer wird als interner Lesepuffer verwendet und die Variable buflen enthält die gefüllte Länge, während lengthProv die zuletzt gelieferte Datenmenge speichert; eof schließlich ist der interne Merker dafür, dass das Dateiende erreicht wurde.

Die Prozedur setInStream ist wesentlich einfacher als das Gegenstück des OUTSTREAM im ersten Teil des Artikels, da hier nur geprüft wird, ob die Datei vorhanden ist und anschließend im Textmodus sofort geöffnet werden kann.

     P setInStream     b                   export
     D setInStream     pi              n
     D inPath                       128a   varying
     d                                     value
     D result          s               n
      /free
                   path = inPath;
                   if access(%trim(path):R_OK) <> 0;
                      result = FALSE;
                   else;
                      result = openFile();
                   endif;
                   return result;
      /end-free
     P setInStream     E

Zum Öffnen der Datei dient eine interne Prozedur openFile, die aus Gründen der einfacheren Lesbarkeit ausgegliedert worden ist.

     P openFile        b
     D openFile        pi              n
     D result          s               n   inz(TRUE)
      /free
                   fileD = open_( %trim(path)
                                : O_RDONLY + O_TEXTDATA
                                );
                   if filed = -1;
                      result = FALSE;
                   endif;
                   eof = FALSE;
                   return result;
      /end-free
     P openFile        E

Der File-Deskriptor, der von der C Funktion open zurückgegeben wird, wird in der globalen Variable fileD zur späteren Verwendung bei den Leseaufrufen gespeichert.
Kern des gesamten Moduls ist die interne Leseroutine fillBuf, die in einen internen Lesepuffer einliest. Die externen Leseroutinen bedienen sich dann aus diesem Puffer. Sinn und Zweck dieses Verfahrens ist es, zu ermöglichen, dass komfortable Leseroutinen bereitgestellt werden können und dass trotzdem in festen Blöcken gelesen wird.

Die gesamte Leseroutine ist in eine Sicherheitsabfrage eingepackt, die sicherstellt, dass nur gelesen wird, wenn die Datei geöffnet ist.

 
     P fillBuf         b
     D fillBuf         pi
     d buf             s           1024a
     d bufP            s               *   inz(%addr(buf))
     d length          s             10i 0
      /free
                   if fileD  <> -1;
                      dou (bufLeng > 1024) or eof;
                         length = read_(fileD:bufP:1024);
                            if length < 1024;
                               eof = TRUE;
                               closeFile();
                            endif;
                         %subst(buffer : bufLeng + 1 : length)
                           =  %subst(buf:1:length);
                         bufLeng = bufLeng + length;
                      enddo;
                   endif;
                   return;
      /end-free
     P fillBuf         E

Die Prozedur liest in Blöcken von 1024 Byte und stellt sicher, dass der Buffer mindestens 1024 Byte enthält, solange das Dateiende noch nicht erreicht ist. Wenn das Ende erreicht wird, enthält der Buffer den Rest der gelesenen Daten und die Datei wird automatisch geschlossen. Die gelesenen Blöcke werden im Lesepuffer hinten angefügt. Die gefüllte Länge wird in Variable bufLeng verwaltet. Dieses auf den ersten Blick umständliche Verfahren ist erforderlich, damit beim Auftreten von Blanks am Ende des Puffers nichts verloren geht.
Die Prozedur fillBuf wird einfach von allen Leseanforderungen zu Beginn aufgerufen und sorgt dafür, dass immer Daten bereitstehen.
Die von außen aufrufbare Prozedur readStream stellt einen Block Daten von maximal 1024 Byte Länge bereit.

     P readStream      b                   export
     D readStream      pi          1024a   varying
     D result          s           1024a   varying
      /free
                   fillBuf();
                   if bufLeng > 1024;
                      result = %subst(buffer:1:1024);
                      lengthProv = 1024;
                      bufLeng = bufLeng - 1024;
                      buffer = %subst(buffer:1025:bufLeng);
                   else;
                      result = %subst(buffer:1:bufLeng);
                      lengthProv = bufLeng;
                      buffer = *BLANK;
                      bufLeng = 0;
                   endif;
                   return result;
      /end-free
     P readStream      E

Solange der Puffer noch mehr als 1024 Byte enthält wird die maximale Länge geliefert. Enthält dieser weniger Daten, ist das Dateiende erreicht und der Rest wird geliefert. In der Variable lengthProv wird die tatsächliche Länge der Daten gespeichert, die bei Leerzeichen am Ende der Lieferung von der abfragbaren Länge der verwendeten VARYING-Variable abweichen kann, da diese automatisch am rechten Ende getrimmt wird, also Blanks entfernt werden. Die gelieferten Daten werden aus dem Puffer entfernt und der jeweilige Rest wird wieder linksbündig gestellt.
Die exportierte Prozedur readLnStream ist durch die Verwendung des Puffers nur wenig komplizierter als die soeben besprochene read-Routine.

     P readLnStream    b                   export
     D readLnStream    pi          1024a   varying
     D result          s           1024a   varying
     d crlfPos         s             10i 0
      /free
                   fillBuf();
                   crlfPos = %scan(CRLF:buffer);
                   if (crlfPos = 0) or crlfPos > 1024;
                      result = readStream();
                   else;
                      result = %subst(buffer:1:crlfPos - 1);
                      lengthProv = crlfPos - 1;
                      buffer = %subst( buffer
                                     : crlfPos + 2
                                     : bufLeng - crlfPos - 1
                                     );
                      bufLeng = bufLeng - crlfPos;
                   endif;
                   return result;
      /end-free
     P readLnStream    E

Nachdem mit dem Aufruf von fillBuf sichergestellt wird, dass der Puffer ausreichend Daten enthält, wird im Puffer das erste Auftreten eines Zeilenendes gesucht. Dieses ist an der Zeichenfolge Carriage Return Line Feed erkennbar, die als Konstante CRLF deklariert wurde. Als maximale Zeilenlänge wird von 1024 Byte ausgegangen, sollte diese Länge überschritten werden, werden 1024 Byte zurückgeliefert.
Wird ein Zeilenende gefunden, dann wird der Text bis zum Zeilentrenner zurückgeliefert und die Zeile mitsamt dem Zeilentrenner aus dem Puffer entnommen. Am Dateiende wird der Rest als letzte Zeile geliefert – unabhängig davon, ob noch ein Zeilentrenner folgt oder nicht. Bei der Entnahme aus dem Puffer werden die internen Variablen lengthProv und bufLength immer konsistent gestellt und die verbleibenden Daten des Puffers wieder linksbündig gestellt.
Der Vollständigkeit halber seien noch die beiden Prozeduren getLength und closeFile dargestellt.

     P getLength       b                   export
     D getLength       pi            10i 0
      /free
                   return lengthProv;
      /end-free
     P getLength       E

Die exportierte Prozedur getLength gibt einfach die Zustandsvariable lengthProv zurück, damit die aufrufende Anwendung feststellen kann, ob rechtsbündige Blanks entfernt wurden, was bei den verwendeten Varying-Variablen automatisch geschieht und nicht verhindert werden kann.

     P closeFile       b
     D closeFile       pi              n
     d result          s               n   inz(TRUE)
      /free
                   if close_(FileD) <> 0;
                      result = FALSE;
                   endif;
                   fileD = -1;
                   eof = TRUE;
                   return result;
      /end-free
     P closeFile       E

Die Prozedur closeFile wird nur intern verwendet und schließt Streamfile wieder. Sie wird aufgerufen, sobald das Dateiende festgestellt wird.
Die exportierte Prozedur getEof wird nur dann benötigt, wenn Leerzeilen in der Datei vorhanden sind und dient bei der Verarbeitung mit readLnStream dazu, festzustellen, ob das Dateiende erreicht ist.

     P getEof          b                   export
     D getEof          pi              n
      /free
                   if bufLeng = 0;
                      return eof;
                   else;
                      return FALSE;
                   endif;
      /end-free
     P getEof          e

Zu guter Letzt noch ein Beispiel für die Verwendung des INSTREAMS.

     D*B  CRTRPGMOD TESTINSTR
     D*B+    DBGVIEW(*COPY)
     D*B  CRTPGM TESTINSTR
     D*B+    ACTGRP(TESTINSTR)
     D*B+    BNDSRVPGM(INSTREAM
     D*B+             )
     /* Prototypen verwendete
      /COPY QRPGLEH,INSTREAM
     /*-------------------------------------------------------------------*/
      * Konstanten
     D TRUE            C                   *ON
     D FALSE           C                   *OFF
     /*-------------------------------------------------------------------*/
     d zeile           s             52a
      * Main
      /COPY QRPGLEC,INSTREAM
     /free
                   setInStream('/home/bender/BusCard.properties');
                      zeile = readLnStream();
                      dow  zeile <> *BLANK;
                         dsply zeile;
                         zeile = readLnStream();
                      enddo;
                      return;
      /end-free

In dem Programm wird Streamfile, das im Beispiel des ersten Teils erzeugt wurde, wieder gelesen und zeilenweise auf den Bildschirm ausgegeben. Wenn Leerzeilen vorkommen, muss das Dateiende noch mit getEof abgefragt werden, damit nicht bereits bei der ersten Leerzeile die Verarbeitungsschleife verlassen wird.

Den Autor Dieter Bender erreichen Sie unter dieter.bender@midrangemagazin.de