Project

General

Profile

Streams

Introduction

Streams in Progress 4GL

A stream is a sequence of bytes or characters that can be read or written from beginning to end.

Progress procedures have access to input streams (which are read) and output streams (which are written). It is also possible to have a stream that is simultaneously both an input and an output stream.

Streams can be named resources or they can be unnamed. Each Progress session has a single unnamed input stream and a single unnamed output stream. When reading and writing statements do not refer to an explicitly named stream, then they are implicitly referencing one or both of the unnamed streams.

The Progress CHUI has a unified design where the terminal is treated as a stream. That stream is so important that the default source for the unnamed input stream is the terminal and the default destination for the unnamed output stream is also the terminal.

In traditional computing terms, the terminal denotes both an input and an output device, used for an interactive user-interface (UI). This is the default setup of a Progress session. However, Progress procedures treat these two unnamed streams (for the most part) independently. A procedure may get input from the default unnamed stream (the terminal) while redirecting the unnamed output stream to a non-terminal destination (e.g. a file). Likewise, procedures may redirect the unnamed input from a non-terminal source while writing to the unnamed output stream which is not redirected (thus it is the terminal). It is also possible to redirect both unnamed streams simultaneously.

A single procedure can use multiple input sources and output destinations besides the two unnamed streams. These other input and output streams are named resources explicitly defined in 4GL code and accessed using those names. There are statements for defining named streams and for controlling where streams (named or unnamed) point, for example: files, printers or child processes.

Streams (named/unnamed) can be tied to some device, but can't be queried about the physical device they operate on at a given time.

Although many stream operations are the same no matter if the stream is backed by the terminal or not, certain operations are only allowed on a non-terminal stream (e.g. PUT, EXPORT, IMPORT, SEEK).

FWD Approach

In converted code, each stream corresponds to a local or member instance of an implementation of com.goldencode.p2j.util.Stream type. This class defines an API that provides the compatible Progress 4GL semantics for all forms of supported I/O processing. This is an abstract class and concrete subclasses actually provide the real implementation for each type of stream.

The design is such that the vast majority of code is implemented in Stream and the subclasses only have to provide the minimum function to implement the specific resource being used. This lets the same behavior be obtained from all types of streams and makes it easy to add more stream types with a small incremental effort.

Generally a stream is implemented as either an input OR an output but not as both. The sole exception to this is the case of using a named stream in the INPUT-OUTPUT THROUGH, where the same stream name is used for both the input and output pipes (the complexity of this is managed inside com.goldencode.p2j.util.ProcessStream and in the processing during conversion).

There are 2 unnamed streams ("unnamed input" and "unnamed output") which are implicitly used when a named stream is not explicitly present in the source file. These streams are stored as context local instances that are accessible throughout a user's session via the UnnamedStreams.safeInput() and UnnamedStreams.safeOutput() methods. Language statements that operate differently based on whether the unnamed input or output is redirected use the com.goldencode.p2j.util.UnnamedStreams class to detect these cases and for access to the stream(s).

UnnamedStreams keeps context-local state for both the input and output unnamed streams for a given session. If these are set to null, then the corresponding unnamed stream is not currently redirected. When the unnamed input stream is null and a read occurs, this is the same as reading from the terminal. When the unnamed output stream is null and a write occurs, this is the same as writing to the terminal. This is how the runtime is designed and as a consequence unnamed streams cannot and don't work with the terminal.

Instead, the case with the terminal as a default output (when a named stream is not involved and the unnamed stream is not redirected) is handled by the internal logic of the user-interface runtime code as a special case and in this situation communication with the terminal happens more directly, not through streams. When statements in this chapter refer to the unnamed stream as being the terminal, it is this what is meant.

Named streams can never reference the terminal either. Instead, in order to indicate that the intended target is the terminal, a null stream instance is used. More details about this situation will be given later in the chapter.

The FWD approach of not treating the terminal as a stream is a core design difference compared with Progress. However, from the converted code perspective, there is no logical difference in behavior. The design difference is hidden in the runtime.

Another important design difference is how the client functionality is executed. The Progress 4GL is implemented as a client-centric environment. Even the Progress database server is really just a (shared memory or network) connection to a process which takes a limited specification of index fields and the corresponding values or value ranges and returns records from that index. All query where clause and sorting is handled on the client side!

The FWD environment, in contrast, is designed with a remote client which is primarily a presentation engine and proxy for server access to client resources. This "thin client" does not run any converted code, instead it is a Java process which connects to the FWD server where the converted code runs. The client is run in the context of the user's operating system login or shell. In other words, the user will be logged in to some operating system (e.g. Linux) and will obtain a shell or desktop. If this is a terminal oriented system (CHUI) then that user will have logged in via a terminal session (via a serial line or a network protocol such as telnet or ssh). After the login, the user will have some shell or desktop process running on that system. From there the user either explicitly (with a command or icon) or implicitly (via a profile or startup script) will launch the FWD thin client process. When this document references the "thin client" or "client", this is what is meant.

File system access and child processes are all run on the FWD client side, which is remote from the server where the converted business logic runs. This remote client acts as a proxy for that local system access. For example, when the business logic needs to read or write to a stream, those read or write operations are transparently sent to the proxy for runtime execution. Although this split of the client and server is mostly hidden by the runtime environment, some hints of this split can be seen in the converted code. The most important of these visible clues is the use of the RemoteStream class. Instances of this class are used on the server to redirect primitive stream operations to the client.

In order for a stream to be used (a named stream that was previously defined or unnamed stream implicitly defined) it must be opened, that is, assigned (or redirected) to an instance of type RemoteStream, obtained with a static factory method in StreamFactory class, depending on stream type. The instance is named with a converted Java name (if it is a named stream) or is accessed "anonymously" via UnnamedStreams. Each RemoteStream will actually proxy a remote concrete Stream sub-class such as FileStream or ProcessStream which operates on the client side of the system.

To obtain a proxy for a named or unnamed stream requires that a RemoteStream instance be created and initialized to point to a specific client-side stream instance. FWD does this silently in the runtime, each time a StreamFactory.open[File|Process|Terminal]Stream() call is executed.

Each time a top level scope (external or internal procedure or trigger) is entered, the unnamed streams are saved away in order to be restored when the scope exits. When the called scope ends the calling scope will not see any difference with respect to unnamed stream redirection, regardless of what other possible redirections of the respective stream the called scope may have made while processing. A distinction exists between what stream instance the redirection is set to and the state of that stream instance (opened or closed). There is one case where the state of the stream that was used for redirecting the unnamed input stream in the calling scope is changed after the called scope exits: if the calling scope redirects the unnamed input stream to some stream and then the called scope redirects the unnamed input to null (meaning the terminal) at a time when the previous redirection (made in the calling scope) is still in use in the called scope, then the stream to which the unnamed input stream had been redirected in the calling scope IS closed. More details on this can be found in the section on stream closing.

A concrete Stream subclass is instantiated and assigned to the named or unnamed stream instances whenever an INPUT FROM, OUTPUT TO, INPUT THROUGH, OUTPUT THROUGH or INPUT-OUTPUT THROUGH language statement is encountered. The type of requested resource determines which class is instantiated and then the backing resource on the remote client is opened.

All of the formatted and unformatted reading and writing is provided by a common set of high level methods in Stream (see the table below). These methods handle the Progress semantics while actually reading/writing using a smaller set of very simple workers that are implemented in the concrete subclasses. The Stream class also implements the buffering of reads/writes, and all the line number, paging and column support.

Stream processing generates the EndConditionException on EOF (ENDKEY) and ErrorConditionException for ERROR condition generation. This processing honors the NO-ERROR construct using the silent error mode in the ErrorManager class.

Summary of conversion support for Progress 4GL language stream features:

Progress 4GL Feature Replacement Java Methods Supported
DEFINE STREAM new StreamWrapper() Yes
EXPORT Stream.writeField()
Stream.writeBlock()
Yes
IMPORT Stream.readField()
Stream.readBlock()
Stream.readLine()
Yes
INPUT FROM
OUTPUT TO
StreamFactory.open[File|Terminal]Stream() Yes
{INPUT | OUTPUT | INPUT-OUTPUT} THROUGH StreamFactory.openProcessStream()

+ process launching code (see the Process Launching chapter for details)
Yes
INPUT CLOSE Stream.closeIn()
UnnamedStreams.closeIn()
Yes
OUTPUT CLOSE Stream.closeOut()
UnnamedStreams.closeOut()
Yes
INPUT-OUTPUT CLOSE Stream.close()
UnnamedSTreams.closeBoth()
Yes
LINE-COUNTER Stream.getNextLineNum() Yes
PAGE Stream.advancePage() Yes
PAGE-NUM Stream.getPageNum() Yes
PAGE-SIZE Stream.getPageSize() Yes
PUT Stream.put()
Stream.putControl()
Stream.putUnformatted()
Yes
READKEY Stream.readChar() Yes
SEEK Statement Stream.setPosition() Yes
SEEK() Stream.getPosition() Yes

Summary of conversion support for access resources:

Progress 4GL FWD Class Input Output Supported Notes
Child Process ProcessStream Yes Yes Yes  
CLIPBOARD ClipboardStream No Yes No In the 4GL, this is only available on Windows (using the OUTPUT TO statement). This is not supported yet.

Full bidirectional clipboard support is available through the CLIPBOARD system handle. This alternative is likewise not yet supported.
Device
or
File
FileStream Yes Yes Yes UNIX and Linux systems allow devices to be addressed the same way as files.

Windows does not allow this, it requires I/O Control API calls. Direct device access would not be possible using streams in the 4GL, nor is it possible in FWD.
OS-DIR DirStream Yes No No  
PRINTER   No Yes No  
TERMINAL TerminalStream Yes Yes Yes In 4GL, only the unnamed streams can work with the terminal. In FWD, direct terminal access is not supported by streams directly. Instead, by setting either of the unnamed streams to terminal, this will force any runtime code that is invoked which can access the terminal (or a redirected unnamed stream) to directly access the terminal instead of trying to use the unnamed streams.

Working with Streams

Opening Streams

When working with a stream, in order to be able to send or receive data through it, it must be opened; in other words, linked to a resource or device. In 4GL, depending on how the stream needs to be used, this is done with some specialized statements, which are listed in the next table:

4GL Statement Details
INPUT [ STREAM <stream> ] FROM ... Opens the named stream <stream> for reading or the redirects the unnamed input stream, if a named stream is not specified.
OUTPUT [ STREAM <stream> ] TO ... Opens the named stream <stream> for writing or the redirects the unnamed output stream, if a named stream is not specified
INPUT [ STREAM <stream> ] THROUGH ... Assigns the named stream <stream> (or redirects the unnamed input stream, if a named stream is not specified) as the input source for the started process.
OUTPUT [ STREAM <stream> ] THROUGH ... Assigns the named stream <stream> (or redirects the unnamed output stream, if a named stream is not specified) as the output source for the started process.
INPUT-OUTPUT [ STREAM <stream> ] THROUGH ... Assigns the named stream <stream>(or redirects both the unnamed input and output streams, if a named stream is not specified) as the input and output source for the started process.

If no stream name is referenced in the statements above, the corresponding unnamed stream is considered - the unnamed input stream for statements affecting input, the unnamed output stream for statements affecting the output and both streams for INPUT-OUTPUT THROUGH. The resource can also be the terminal (TERMINAL), but note that only named streams can point to it, unnamed streams cannot work with the terminal. Instead, input and/or output default to the terminal (and is handled internally as a separate case by the implementation) when a named stream is not specified and the unnamed streams are not redirected.

Depending on how the statement is used - with a named, unnamed or process stream - generated code will look different. Details about each case will be treated in separate sections in this chapter: Named Streams, Unnamed Streams and Process Streams. This section shows only details about what is supported by FWD and explains how the FWD runtime handles the streams.

In FWD, opening a stream has two steps: opening the resource itself and assigning (linking) it to a designated stream - a named stream that has been explicitly defined before or the unnamed stream. Opening the resource itself is done the same way regardless of whether the stream is named or unnamed, depending on resource type (file or process); conversion details about each case will be explained in the stream-related sections of this chapter. Also, linking the resource to the stream that is being opened however is done differently, depending on the stream type - details are provided in the corresponding section, for each stream type.

There is a difference in how Progress and FWD handle unnamed streams initialization. As stated above, in 4GL a procedure's unnamed input and output streams are assigned to the terminal by default. In FWD however, unnamed streams need explicit initialization, to terminal or to other resource (file, process etc). In case this is not done, a no-operation stream implementation (p2j.util.NullStream) is used as a concrete implementation for the unnamed streams and while this is safe to use in any processing, it doesn't actually do anything. Hence, statements that work with a stream directly (such as IMPORT and EXPORT) would have no effect. On the other hand, statements that work with the UI and do not do anything special to set up the unnamed streams, will continue to work; the reason for this is that they work directly with the terminal and only go through an unnamed stream when the respective stream had been redirected. It is safe to do this since redirecting an unnamed stream implies its initialization.

This section will continue in explaining what is supported by FWD from the statements which open the unnamed or named stream and link them to a non-process resource. For details about streams working with processes, please visit the Process Streams section of this chapter.

INPUT FROM

Syntax:

INPUT [ STREAM stream ] FROM
   {    opsys-file
      | opsys-device
      | TERMINAL
      | VALUE ( expression )
      | OS-DIR ( directory ) [ NO-ATTR-LIST ]
   }
   [ BINARY ]
   [ ECHO | NO-ECHO ]
   [ KEEP-MESSAGES ]
   [ NO-MAP | MAP protermcap-entry ]
   [ UNBUFFERED ]
   [ NO-CONVERT
      |  { CONVERT
            [ TARGET target-codepage ]
            [ SOURCE source-codepage ]
         }
   ]

The option description table:

4GL Syntax FWD Equivalent Description Supported
STREAM stream The name of the FWD field which defines the stream. Specifies the name of the stream. In case of not specified name unnamed stream is used. Yes
opsys-file filename parameter for StreamFactory.openFileStream API. The name of the file from which it is required to direct input. Yes
opsys-device filename parameter for StreamFactory.openFileStream API. Represents the name of the operating system device.  
TERMINAL StreamFactory.openTerminalStream() Directs the input to the terminal which is the default input device. Yes
VALUE ( expression ) filename parameter for StreamFactory.openFileStream API. Represents an expression to be used as the destination of the output. Yes
OS-DIR ( directory ) [ NO-ATTR-LIST ] n/a   No
BINARY Stream.setBinary() Allows output to be written directly without any conversion or interpretation. Yes
ECHO Stream.setEcho(true) Sends all input data read from a file to the output destination. Data is echoed by default. Yes
NO-ECHO Stream.setEcho(false) Suppress the echoing of input data to the output destination. Yes
KEEP-MESSAGES n/a Causes the following messages not to echo to the default window: 4GL error and warning messages, and messages from the MESSAGE statement. This option causes sending messages only to the specified output stream. No
MAP protermcap-entry | NO-MAP n/a Using this option to send output to a device that requires different character mapping comparing to mapping applied to the current output stream. No
UNBUFFERED Stream.setUnbuffered() Disables usual OS buffering tool to speed up the i/o operations. Yes
CONVERT n/a Allows to modify the character conversion occurring the external file and memory. No
TARGET target-codepage n/a Specifies the target code page of the character conversion. The name specified must be the valid code page. No
SOURCE source-codepage n/a Specifies the source code page of the character conversion. The name specified must be the valid code page. No
NO-CONVERT n/a Disables the character conversion between external file and memory. No

The BINARY option in the INPUT FROM statement can be used for reading special chars which would normally impact reading itself. With BINAR Y, one can read control characters. Input sources often contain control characters that affect the format or quantity of data that is received. For example, a text file might contain NULL (\0) characters to terminate character strings. Without the BINARY option, Progress ignores all input on a line after the first NULL character. The BINARY option allows reading of all data in the file, including any NULL and other non-printable control characters without interpretation.

OUTPUT TO

Syntax:

OUTPUT [ STREAM stream ] TO
   { PRINTER [ printer-name ]
      | opsys-file
      | opsys-device
      | TERMINAL
      | VALUE ( expression )
      | "CLIPBOARD" 
   }
   [ NUM-COPIES { constant | VALUE ( expression ) } ]
   [ COLLATE ]
   [ LANDSCAPE | PORTRAIT ]
   [ APPEND ]
   [ BINARY ]
   [ ECHO | NO-ECHO ]
   [ KEEP-MESSAGES ]
   [ NO-MAP | MAP protermcap-entry ]
   [ PAGED ]
   [ PAGE-SIZE { constant | VALUE ( expression ) } ]
   [ UNBUFFERED ]
   [ NO-CONVERT
      |  { CONVERT
            [ TARGET target-codepage ]
            [ SOURCE source-codepage ]
         }
   ]

The option description table:

4GL Syntax FWD Equivalent Description Supported
STREAM stream The name of the FWD field which defines the stream. Specifies the name of the stream. In case of not specified name unnamed stream is used. Yes
PRINTER [ printer-name ] n/a By default, this option sends output to the printer defined in the default print context. The printer-name parameter can be used to send output to a specific printer. This option overrides but does not change the default print context. The output is considered to be paged unless
PAGE-SIZE 0 option has been specified.
No
opsys-file filename parameter for StreamFactory.openFileStream API. The name of the text file to which it is required to direct output from a procedure. Yes
opsys-device filename parameter for StreamFactory.openFileStream API. Represents the name of the operating system device  
TERMINAL StreamFactory.openTerminalStream() Directs the output to the terminal which is the default output device Yes
VALUE ( expression ) filename parameter for StreamFactory.openFileStream API. Represents an expression to be used as the destination of the output. Yes
“CLIPBOARD” n/a The system clipboard as destination. No
NUM-COPIES { constant | VALUE (expression) } n/a Specifies the number of copies to print. The constant or expression parameters must evaluate to a positive integer. No
COLLATE n/a Specifies whether multiple copies of output pages print in collated order. No
LANDSCAPE | PORTRAIT n/a Page orientation for Windows only for drivers that support such orientations. No
APPEND append parameter for StreamFactory.openFileStream API. Appends the output to the end of a file Yes
BINARY Stream.setBinary() Allows output to be written directly without any conversion or interpretation. Yes
ECHO Stream.setEcho(true) Sends all input data read from a file to the output destination. Data is echoed by default. Yes
NO-ECHO Stream.setEcho(false) Suppress the echoing of input data to the output destination. Yes
KEEP-MESSAGES n/a Causes the following messages not to echo to the default window: 4GL error and warning messages, and messages from the MESSAGE statement. This option causes sending messages only to the specified output stream. No
MAP protermcap-entry | NO-MAP n/a Using this option to send output to a device that requires different character mapping comparing to mapping applied to the current output stream. No
PAGED paged parameter for StreamFactory.openFileStream API. Formats the output into pages. Form feed chars are
CTRL-L. When output is paged, a page break occurs every 56 lines. PAGED is automatic for output to a printer.
Yes
PAGE-SIZE { constant | VALUE ( expression ) } pageSz parameter for StreamFactory.openFileStream API. Specifies the number of lines per page. The expression is a constant, field name, variable name, or expression with the integer value. Yes
UNBUFFERED Stream.setUnbuffered() Disables usual OS buffering tool to speed up the i/o operations. Yes
CONVERT n/a Allows to modify the character conversion occurring the external file and memory. No
TARGET target-codepage n/a Specifies the target code page of the character conversion. The name specified must be the valid code page. No
SOURCE source-codepage n/a Specifies the source code page of the character conversion. The name specified must be the valid code page. No
NO-CONVERT n/a Disables the character conversion between external file and memory. No
Streams and Resources

A named or unnamed stream can open a device, a file, the terminal or a resource determined after evaluating an expression. In 4GL, this is done using the opsys-file, opsys-device, TERMINAL and the VALUE(expression) options with the INPUT FROM and OUTPUT TO statements. From these, the TERMINAL clause has the special meaning of linking the stream with the terminal's input or output end, and is treated in a distinct manner by the conversion rules (see the INPUT FROM and OUTPUT TO related subsections from the Named Streams and the Unnamed Streams sections of this chapter).

The remaining options, although in 4GL terms each one is seen as a distinct case, during conversion the FWD engine treats them equally and converts them as string literals or expressions which evaluate to string values. Thus, they all get converted and passed as the filename parameter for the various stream open APIs in the Stream and UnnamedStreams classes.

When using a complex expression, its evaluated result must yield a valid path which can be opened for input or output, as needed.

Closing Streams

A stream can be explicitly closed by the programmer or implicitly closed, on certain conditions. To explicitly close a stream, the programmer calls one of the stream close statements, as shown in the next table:

4GL Statement FWD API Details
INPUT STREAM stream CLOSE Stream.closeIn() Closes the input for the specified named stream.
OUTPUT STREAM stream CLOSE Stream.closeOut() Closes the output for the specified named stream.
INPUT CLOSE UnnamedStreams.closeIn() Closes the input for the unnamed stream.
OUTPUT CLOSE UnnamedStreams.closeOut() Closes the output for the unnamed stream.
INPUT-OUTPUT STREAM stream CLOSE Stream.close() Closes the specified named stream, which was opened using the INPUT-OUTPUT STREAM stream THROUGH statement.
INPUT-OUTPUT CLOSE UnnamedStreams.closeBoth() Closes the unnamed input and output streams, which were redirected using the INPUT-OUTPUT THROUGH statement.

When explicitly closing a named stream, all further attempt at reading or writing using that stream will result in an I/O error. To use the stream again, it needs to be reopened. Calling a stream close statement for an already closed stream (named or unnamed), results in a no-op, as the runtime knows the stream is already closed and will do nothing.

For the unnamed stream case, this results in giving the control of the input or output (depending on how the unnamed stream was opened) back to the terminal.

The FWD runtime will implicitly close the named and unnamed streams, depending on certain conditions. Named streams are automatically closed on exit from the scope in which they are defined, except for global streams which are closed on end of the session. The TransactionManager's registerFinalizable() or registerTopLevelFinalizable() is used to obtain this support, as explained in the Defining Streams sub-section in the Named Streams section of this chapter. Global streams are registered with the global flag set to true. This registration code is emitted into the converted application, just after the reference is initialized. Shared streams that are not NEW are just imported. For this reason, they have already been registered for implicit close support in a previous scope.

When the scope in which the named stream was registered ends, Stream.finished() (its implementation being part of the stream's Finalizable flavor) is called on the stream wrapper instance RemoteStream, causing the concrete stream (on the client side) to be flushed and closed.

For the unnamed streams, the top-level block in which an open stream is first used for redirection of the unnamed stream, will cause that stream to be closed implicitly when that scope exits. When top-level scopes are entered, the current redirection of the unnamed streams is saved and it is restored when the scope ends. As already mentioned, however, what stream instance the redirection is set to must be distinguished from the state of that stream (if it is open or closed). The stream that is the default unnamed stream on entry should not have actually been closed (using Stream.close(), Stream.closeIn() or Stream.closeOut()) except in one case: if the default unnamed input stream is the current redirection at close time and the new stream is null (the terminal), then the default unnamed input stream IS closed. The unnamed output stream doesn't have this behavior. So there is this one case where the state of the stream is different on exit. Below follows more details on the process.

At each top level scope (trigger, external or internal procedure), the current unnamed input and output streams are saved as the defaults for that scope: UnnamedStreams is registered with the TransactionManager to receive notifications on entering and leaving scopes (methods scopeStart() and scopeFinished() in class UnnamedStreams). On entering a scope, the current input and output redirection (or the lack of redirection, which is represented by a null) is saved off; this is the "default" unnamed stream for the current (called) procedure. This only happens at the start of a "top-level" block, which is the start of an external procedure, internal procedure, user-defined function or a trigger.

If no further opening/closing of the default unnamed streams occurs within the called top-level scope, then when that scope returns, no change will have occurred in the unnamed streams' state. A stream can be used in the called scope, without causing any streams to be implicitly opened or closed - the default unnamed streams can be used without the calling scope seeing any difference.

Any close that occurs for the unnamed input or output streams which is not the result of assigning a new stream, causes the unnamed stream to be restored back to the default for that scope. Since the Progress programmer can change the unnamed streams but not query them, this feature was needed otherwise an unnamed stream redirected in a calling scope could be modified downstream and upon return that stream might have been already closed. This would lead to bizarre behavior.

When any of the unnamed stream open statements are called, the UnnamedStreams class changes the current redirected stream(s). These methods can be used for the purposes of registering a new redirection AND for the alternate purpose of restoring the default unnamed stream. The following describes the resulting changes (it is different depending on if the new redirection is for input or output):

Unnamed Input
  1. Stream Closing
    • If there is no current redirection, then no streams are closed.
    • If there is a current redirection, and that redirection is the same as the default unnamed stream (saved off at scopeStart()) AND the new stream is not the terminal, then no streams are closed.
    • Otherwise the current redirected stream is closed. In particular, this includes a case where the default unnamed input stream is the current redirection and the new stream is the terminal, then the default unnamed input stream IS closed. If the current redirection is a stream that was assigned in the current scope, this stream will also be closed.
  2. Current unnamed input stream is set to the new stream. This may be the terminal or even the same stream as the default unnamed input stream.
  3. If this is a registration (as opposed to a restore of the default unnamed input stream) AND the new stream is not the terminal, then it is ensured that a close callback (InputCloser) exists in order for the new stream instance to be closed when the top-level block exits.
Unnamed Output
  1. If the new stream is not null, the instance is marked as unnamed using Stream.setUnnamed(true).
  2. If the new output stream is the same as the old output stream, then we are done (no further processing below).
  3. Stream Closing
    • If there is no current redirection, then no streams are closed.
    • If there is a current redirection, and that redirection is the same as the default unnamed stream (saved off at scopeStart()), then no streams are closed.
    • Otherwise the current redirected stream is closed. This implies that the default unnamed output stream should never be closed by this logic.
  4. LogicalTerminal.redirectOutput(oldId, newId) is called to notify the client about the redirection change.
  5. Current unnamed output stream is set to the new stream. This may be the terminal or even the same stream as the default unnamed output stream.
  6. If this is a registration (as opposed to a restore of the default unnamed output stream) AND the new stream is not the terminal, then it is ensured that a close callback (OutputCloser) exists in order for the new stream instance to be closed when the top-level block exits.

The close callbacks (InputCloser and OutputCloser instances) only exist if there is a non-terminal redirection in the current top-level scope. They exist to force a call to UnnamedStreams.closeIn() or UnnamedStreams.closeOut() when that top-level scope exits. The closeIn() and closeOut() methods will restore the default unnamed input and default unnamed output streams respectively. During the restore, the assignInWorker() or assignOutWorker() is called. The logic above in "INPUT stream redirection" and "OUTPUT stream redirection" is executed, except the registration section is bypassed. This means that the exit from the top-level scope will cause the non-terminal redirection that is different from the default unnamed stream to be closed. No close of the current redirection occurs if the current redirection is the terminal OR if the current redirection is the default unnamed stream (and is not the terminal).

Named Streams

Defining Streams

Defining streams in 4GL

For conversion of shared streams, see Shared Streams section.

In order to work with a named stream in 4GL, first it must be defined and opened. The unnamed streams are implicitly defined and opened to refer the terminal. To define additional, named streams for a procedure, the DEFINE STREAM statement is used. This enables the procedure to get input from more than one source simultaneously and send output to more than one destination simultaneously. Streams you name can be operating system files, printers, the terminal, or other non-terminal devices. The syntax of this statement for non-shared streams is:

DEFINE STREAM <stream>

It defines a stream that can be used only by the procedure containing the DEFINE STREAM statement. The value used as stream name must be a string which satisfies the same rules as the normal variable names.

Defining Streams in FWD

For conversion of shared streams, see Shared Streams section.

FWD also makes a clear distinction between named and unnamed streams. On server side, the FWD developer sees all named streams as instances of class com.goldencode.p2j.util.StreamWrapper, while a special com.goldencode.p2j.util.UnnamedStreams class exists for unnamed streams (see the Unnamed Streams section of this chapter for details). API common to all streams reside in the common base class, com.goldencode.p2j.util.Stream.

A StreamWrapper is built simply by invoking its constructor and passing the stream name as a string. Internally, the StreamWrapper instance contains a reference to a RemoteStream, which communicates with the concrete Stream subclass (FileStream etc) in the client; however, these details are not relevant to the user of the API.

If the current procedure defines a new local stream (not a shared stream), it has to be registered for clean-up with TransactionManager.registerTopLevelFinalizable() in its Block.body() method:

TransactionManager.registerTopLevelFinalizable(Stream stream, boolean external);

where the external parameter must be set to true.

A simple example converted by FWD:

define stream s1.

Converted code:

Stream s1Stream = new StreamWrapper("s1");

public void execute()
{
   externalProcedure(SharedExample.this, new Block((Body) () -> 
   {
      TransactionManager.registerTopLevelFinalizable(s1Stream, true);
   }));
}

Details: as it can be seen, the new streams are registered in the TransactionManager.

Opening Streams

A named stream can be used to open a file, terminal or process stream. As process streams present unique properties not common with named streams, they are treated in their own section, Process Streams, which can be found later in this chapter.

A named stream is opened by calling one of the StreamFactory.openTerminalStream, StreamFactory.openFileStream or StreamFactory.openProcessStream APIs - only the first two will be treated here, the last one is process related. If a named stream is already opened, calling one of the open methods for this stream will result in first closing the stream and then re-opening it with the specified parameters.

Regardless of how a stream is opened - using either the INPUT STREAM stream FROM statement or the OUTPUT STREAM stream TO statement, the conversion code will emit similar APIs, which are split in two:

  • If the conversion rules can determine that the stream's target is the terminal, then the StreamFactory.openTerminalStream() API is emitted. Note that this API has no parameters, and its call closes the stream and sets this stream to reference the terminal - all statements using the stream will have the terminal as the target.
    Unlike unnamed streams, which when redirected back to terminal is equivalent to “the unnamed stream is not redirected and output and input goes directly to/from the terminal”, when a named stream targets the terminal, the concrete implementation on client side will be an instance of the TerminalStream - this is because, when sending or reading data using a named stream linked to the terminal, 4GL poses some exceptions when compared to accessing the terminal directly.
    TerminalStream class provides support for stream redirected to the interactive terminal. This stream behaves as something in the middle between stream and interactive terminal. It displays output as regular stream (for example like FileStream), but processes pause as interactive terminal.
    Terminal stream handles paging differently than regular stream. In particular, it has different default page size dictated by terminal height and each page triggers a pause which requires user action (pressing a key).
  • Else, the conversion rules will emit a StreamFactory.openFileStream() call, where the possible parameters are:
  • fileName: name of the file to be opened. Note that the FWD runtime will still decide that the stream's target is the terminal, in cases when the received file name is one of the terminal, term, TERMINAL or TERM string literals.
  • write: if true, this flag indicates that the file should be opened for writing.
  • append: when opening files in write mode, setting this flag to true tells to append written content to the end of the file; otherwise when the file is opened it is truncated to a size of 0.
  • paged: flag indicating that the stream is paged, according to Progress semantic.
  • pageSize: sets the page size for paged streams.

When a file is opened for output, any existing version of that file is removed and a 0 length file is created (unless APPEND is specified). That file is closed when the next stream is opened or by any explicit INPUT CLOSE, OUTPUT CLOSE or INPUT-OUTPUT CLOSE call, for the named stream. When a file is opened for input, the read pointer is located at the first byte of the file.

Full details about the syntax of the INPUT FROM and OUTPUT TO and what is supported by FWD can be found in the Working with Streams section, in their corresponding sub-sections. Here, will be exmplained how these statements are converted when named streams are used.

INPUT STREAM stream FROM

Example 1:

def stream s.
input stream s from test.txt.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openFileStream("test.txt", false, false));

Details:

Opening a named stream to get its input from a file results in an openFileStream call, with the first parameter set to the resource name and the next parameter set to false, as the resource needs to be opened for input access and the last parameter, the append mode, defaulting to false also.

Example 2:

def stream s.
input stream s from terminal.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openTerminalStream(false));

Details:

When the targeted resource is the terminal, the conversion rules will emit an openTerminalStream call.

Example 3:

def stream s.
def var file as char.
file = "terminal".
input stream s from value(file).

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
file.assign("terminal");
sStream.assign(StreamFactory.openFileStream((file).toStringMessage(), false, false));

Details:

Accessing a resource held by a variable or complex expression results in evaluating that expression and passing its result as the file name parameter. In this case, as the file name is the terminal literal, the FWD runtime will notice this and the stream will read all its input directly from the terminal.

Example 4:

def stream s.
input stream s from test.txt no-echo binary unbuffered.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openFileStream("test.txt", false, false));
sStream.setEcho(false);
sStream.setBinary();
sStream.setUnbuffered();

Details:

This example shows how the other stream options are set by the conversion rules; as they are not passed as parameters, the options are set using the setEcho, setBinary and setUnbuffered calls.

Example 5:

def var ch1 as character.
def var ch2 as character.
def var ch3 as character.
define stream mystr.

input stream mystr from "test.txt".
update stream mystr ch1 ch2 ch3.
display ch1 ch2 ch3.
input stream mystr close.

Converted code:

Stream mystrStream = new StreamWrapper("mystr");
...
TransactionManager.registerTopLevelFinalizable(mystrStream, true);
frame0.openScope();
mystrStream.assign(StreamFactory.openFileStream("test.txt", false, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1()),
   new Element(ch2, frame0.widgetCh2()),
   new Element(ch3, frame0.widgetCh3())
};
frame0.update(mystrStream, elementList0);

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1()),
   new Element(ch2, frame0.widgetCh2()),
   new Element(ch3, frame0.widgetCh3())
};
frame0.display(elementList1);

mystrStream.closeIn();

Details:

This example uses the defined stream to read the input from the file.

The output result will be the same as for the previous example. Note the update call has the parameter Stream mystrStream. This is the way we inform the system we need the input channel to be associated with the particular stream, which has been bound to the external file.

OUTPUT STREAM stream TO

Example 1:

def stream s.
output stream s to test.txt.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openFileStream("test.txt", true, false));

Details:

Opening a named stream for writing results in an openFileStream method call for that stream, with the first parameter being the resource name (a file in this case), the next set to true (to open the file for writing) and the last, the append mode, defaulting to false.

Example 2:

def stream s.
output stream s to terminal.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openTerminalStream(false));

Details:

When the targeted resource is the terminal, the openTerminalStream call is emitted for the named stream. This results in closing the previous resource (if not already closed) and redirecting all output sent through this stream directly to the terminal.

Example 3:

def stream s.
def var file as char.
file = "terminal".
output stream s to value(file).

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
file.assign("terminal");
sStream.assign(StreamFactory.openFileStream((file).toStringMessage(), true, false));

Details:

When the targeted resource is determined by a complex expression or variable, the result of the evaluation of this expression is passed as the file name parameter for the openFileStream call. In this case, as the targeted resource is the terminal literal, the FWD runtime will redirect all output sent through this named stream directly to the terminal.

Example 4:

def stream s.
output stream s to test.txt no-echo binary unbuffered.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openFileStream("test.txt", true, false));
sStream.setEcho(false);
sStream.setBinary();
sStream.setUnbuffered();

Details:

This example shows how the options which can't be set through the openFileStream parameters are emitted by the conversion rules. The setEcho, setBinary and setUnbuffered calls set the no-echo, binary and unbuffered modes for the named stream.

Example 5:

def stream s.
output stream s to test.txt append paged page-size 10.

Converted code:

Stream sStream = new StreamWrapper("s");
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openFileStream("test.txt", true, true, 10, true));

Details:

The output stream's append, paged and page-size options are set directly using the append, paged and pageSz parameters for the openFileStream call.

Example 6:

def var i as integer.
do i = 1 to 5:
   output to "test.txt" append.
   display "Hello World. #" i no-label.
   output close.
end.

Converted code:

for (i.assign(1); _isLessThanOrEqual(i, 5); i.increment())
{
   UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, true));

   FrameElement[] elementList0 = new FrameElement[]
   {
      new Element("Hello World. #", frame0.widgetExpr1()),
      new Element(i, frame0.widgeti())
   };
   frame0.display(elementList0);

   UnnamedStreams.closeOut();
}

Details:

After application execution the output file will be:

Hello World. #          1

Hello World. #          2

Hello World. #          3

Hello World. #          4

Hello World. #          5

When using the APPEND option, it allows the user to add the new content to the end of the already existed file. In other words, it moves the internal file pointer to the end of the file before writing new data. In the following example we do output several times in the same file.

Closing Streams

4GL provides the INPUT STREAM stream CLOSE and OUTPUT STREAM stream CLOSE statements to explicitly close a named stream. The conversion rules emit a stream.closeIn() when the input stream needs to be closed and a stream.closeOut() call when the output stream needs to be closed.

The rules to implicitly close a named stream are explained in the Closing Streams sub-section of the Working with Non-Process Streams section of this chapter.

Example:

define stream str.
input stream str from in.txt.
...
input stream str close.
output stream str to out.txt.
...
output stream str close.

Converted code:

TransactionManager.registerTopLevelFinalizable(strStream, true);
strStream.assign(StreamFactory.openFileStream("in.txt", false, false));
strStream.closeIn();
strStream.assign(StreamFactory.openFileStream("out.txt", true, false));
strStream.closeOut();

Details:

This example uses the same stream, str, for input and output operations. Once the input data is finished reading, closing the stream will make it available to be opened for other usage, output in this case.

Unnamed Streams

The unnamed streams are implicitly defined and opened to refer the terminal. Unnamed streams are implicitly defined for any procedure; no special action is needed to define them, they can be directly opened. In FWD the class providing unnamed stream support is p2j.util.UnnamedStreams.

Opening an unnamed stream results in redirected the terminal's input or output to the specified target. The statements which open unnamed streams are the INPUT FROM, OUTPUT TO, INPUT THROUGH, OUTPUT THROUGH and INPUT-OUTPUT THROUGH, where no explicit stream name is specified. For each statement, the corresponding unnamed stream is considered: the unnamed input stream for statements affecting input, the unnamed output stream for statements affecting the output and both streams for INPUT-OUTPUT THROUGH. The resource can also be the terminal (TERMINAL), but note that only named streams can explicitly point to it. Unnamed streams cannot work explicitly with the terminal; if this option is used, the FWD runtime will default the control back to the terminal, when the unnamed streams try to open a terminal resource.

There is a difference in how Progress and FWD handle unnamed streams initialization. As stated above, in 4GL a procedure's unnamed input and output streams are assigned to the terminal by default. In FWD however, unnamed streams need explicit initialization, to terminal or to other resource (file, process etc). In case this is not done, a no-operation stream implementation (p2j.util.NullStream) is used as a concrete implementation for the unnamed streams and while this is safe to use in any processing, it doesn't actually do anything. Hence, statements that work with a stream directly (such as IMPORT and EXPORT) would have no effect. On the other hand, statements that work with the UI and do not do anything special to set up the unnamed streams, will continue to work; the reason for this is that they work directly with the terminal and only go through an unnamed stream when the respective stream had been redirected. It is safe to do this since redirecting an unnamed stream implies its initialization.

Redirecting an unnamed stream that is already redirected can have the effect of closing the stream currently pointed by the unnamed streams. A more detailed discussion is presented in the section concerning stream closing.

Although in 4GL an unnamed stream can't be directly referenced once is opened (all redirection being done silently by the 4GL's runtime), in FWD in some cases we need to access the unnamed stream instance, to set some other stream options or to pass it as a parameter to other APIs. For this, FWD provides the UnnamedStreams.safeInput() and UnnamedStreams.safeOutput() APIs, to access the currently opened unnamed input stream and unnamed output stream, respectively. If there is no such opened stream, these methods will return a NullStream intstance, which EOF's all read attempts and drops all write attempts.

Opening the Unnamed Streams

Although in 4GL the unnamed streams are always opened and target the terminal, in FWD by default they are closed and need to be explicitly initialized, so that the terminal will be redirected. This is done using the input and output statements without specifying an explicit stream name. This section will show how the INPUT FROM and OUTPUT TO statements work when non-process resources are opened. For process resources, visit the Process Streams section of this chapter, which will explain how process streams work, when used with both named and unnamed streams.

INPUT FROM Statement

This statement is used to redirect input from the default terminal unnamed stream to the external device. The 4GL syntax of the statement is presented in the Working with Non-Process Streams section of this chapter.

The FWD conversion engine will emit one of the UnnamedStreams.assignIn(StreamFactory.open*()) APIs, depending on how the terminal was used:

  • If it targets the terminal, the UnnamedStreams.assignIn(StreamFactory.openTerminalStream()) is emitted.
  • If it targets another non-process resources, the UnnamedStreams.assignIn(StreamFactory.openFileStream(String filename)) method is emitted, where the fileName is the name of the file (or resource) to be opened. Note that the FWD runtime will still decide that the stream's target is the terminal, in cases when the received file name is one of the terminal, term, TERMINAL or TERM values.

The only mandatory option to be presented in every call of the INPUT FROM statement is the name of the external resource to be opened (device, file, terminal and so on).

Example 1:

input from test.txt.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));

Details:

Redirecting the unnamed input stream to a file will result in all input statements which don't target a named stream to get input from that file.

Example 2:

input from terminal.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openTerminalStream(false));

Details:

Redirecting the unnamed input stream to terminal will result in an openTerminalStream method return null. Therefore assignIn, instead of linking the unnamed input stream to the terminal, will deactivate it, and all input will come from the terminal.

Example 3:

def var file as char.
file = "terminal".
input from value(file).

Converted code:

file.assign("terminal");
UnnamedStreams.assignIn(StreamFactory.openFileStream((file).toStringMessage(), false, false));

Details:

As the file is set to be the terminal, the FWD runtime will notice this and the assignIn call will result in deactivating the unnamed input, with all subsequent input calls being directed to the terminal.

Example 4:

input from test.txt no-echo binary unbuffered.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));
UnnamedStreams.safeInput().setEcho(false);
UnnamedStreams.safeInput().setBinary();
UnnamedStreams.safeInput().setUnbuffered();

Details:

This example shows how various INPUT FROM options are emitted for the unnamed input stream. We need a reference to the unnamed stream, which is done using the UnnamedStream.safeInput() call. The setEcho, setBinary and setUnbuffered called on this instance will set the corresponding unnamed stream's options.

Example 5:

def var ch1 as character.
def var ch2 as character.
def var ch3 as character.

input from "test.txt".
update ch1 ch2 ch3.
display ch1 ch2 ch3.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1()),
   new Element(ch2, frame0.widgetCh2()),
   new Element(ch3, frame0.widgetCh3())
};

frame0.update(elementList0);

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1()),
   new Element(ch2, frame0.widgetCh2()),
   new Element(ch3, frame0.widgetCh3())
};

frame0.display(elementList1);

Details:

This example demonstrates the usage of the unnamed stream to redirect the input and to be read from the external file.

The initial file contains:

Hello Outside World!

The screen after application starts will be:

Note to read the whole line we need to have 3 character variables.

As the unnamed stream was used, the converted code for the UPDATE statement has no awareness of the redirected state of the unnamed stream; the FWD runtime is responsible of redirected the unnamed input stream to the file.

Example 6:

...
input from "test.txt".
update ch1.
display ch1.
message "This is the input from file, go back to terminal".
pause.
input from terminal.
update ch2.
pause.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1())
};

frame0.update(elementList0);

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1())
};

frame0.display(elementList1);

message("This is the input from file, go back to terminal");
pause();
UnnamedStreams.assignIn(StreamFactory.openTerminalStream(false));

FrameElement[] elementList2 = new FrameElement[]
{
   new Element(ch2, frame0.widgetCh2())
};

frame0.update(elementList2);
pause();

Details:

To restore the input back to the terminal, one can close the unnamed stream or call

INPUT FROM TERMINAL to redirect the input back to the terminal. The first screen after the application starts is:

Note the first

UPDATE statement takes the data from the external file. Press spacebar to continue. The screen become:

Here we see the variable ch2 is updated via terminal. This means we switched the input stream back to the terminal input.

Example 7:

def var ch1 as character.
def var ch2 as character.
def var ch3 as character.

def var chout as character format "x(40)" initial "test.txt".
message "The input will be redirected from "+chout.
pause.

input from value( chout ).
update ch1 ch2 ch3.
display ch1 ch2 ch3.

Converted code:

message(concat(new character("The input will be redirected from "), chout));
pause();
UnnamedStreams.assignIn(StreamFactory.openFileStream((chout).toStringMessage(), false, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1()),
   new Element(ch2, frame0.widgetCh2()),
   new Element(ch3, frame0.widgetCh3())
};

frame0.update(elementList0);

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(ch1, frame0.widgetCh1()),
   new Element(ch2, frame0.widgetCh2()),
   new Element(ch3, frame0.widgetCh3())
};

frame0.display(elementList1);

Details:

Sometimes it is useful not to hardcode the name of the input file, directly in the INPUT FROM call. For example, in a case when the input file name needs to be specified by the user. The option VALUE must be used for this purpose.

The initial screen will be:

Press spacebar to start the data reading from file:

So the input file name in this example will be defined by the value of the chout character variable.

OUTPUT TO Statement

This statement is used to redirect output from the default terminal unnamed stream to the external device. The 4GL syntax of the statement is presented in the Working with Non-Process Streams section of this chapter.

The FWD conversion engine will emit one of the UnnamedStreams.assignOut(StreamFactory.open*Stream()) APIs, depending on how the terminal was used:

  • If it targets the terminal, the StreamFactory.openTerminalStream is emitted.
  • If it targets another non-process resources, the StreamFactory.openFileStream is emitted, where the possible parameters are:
  • fileName: name of the file to be opened. Note that the FWD runtime will still decide that the stream's target is the terminal, in cases when the received file name is one of the terminal, term, TERMINAL or TERM values.
  • append: when opening files in write mode, setting this flag to true tells to append written content to the end of the file; otherwise when the file is opened it is truncated to a size of 0.
  • paged: flag indicating that the stream is paged, according to Progress semantic.
  • pageSize: sets the page size for paged streams.

Example 1:

output to test.txt.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));

Details:

Opening the unnamed output stream will result in assignOut + openFileStream calls.

Example 2:

output to terminal.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openTerminalStream(false));

Details:

When redirecting the unnamed output stream to the terminal, the conversion rules will emit assignOut + openTerminalStream() call. When executed, the FWD runtime will deactivate the unnamed output stream, leaving all output to be sent directly to the terminal.

Example 3:

def var file as char.
file = "terminal".
output to value(file).

Converted code:

file.assign("terminal");
UnnamedStreams.assignOut(StreamFactory.openFileStream((file).toStringMessage(), true, false));

Details:

Here, the resource name is taken from the value of a variable. In this case, as the file targets the terminal, the FWD runtime will deactivate the unnamed output stream, with all subsequent output being sent directly to the terminal.

Example 4:

output to test.txt no-echo binary unbuffered.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));
UnnamedStreams.safeOutput().setEcho(false);
UnnamedStreams.safeOutput().setBinary();
UnnamedStreams.safeOutput().setUnbuffered();

Details:

This example shows how some of the stream's options are set. As they are not passed as parameters for the openFileStream call, we need a reference to the unnamed stream, returned by the UnnamedStreams.safeOutput() call. The setEcho, setBinary and setUnbuffered calls on this stream instance will set the stream's options accordingly.

Example 5:

output to test.txt append paged page-size 10.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, true, 10, true));

Details:

The append, paged and page-size unnamed output stream options are emitted directly as parameters for the openFileStream call. More details about paging can be found in the Paging Support section of this chapter.

Example 6:

output to "test.txt".
display "Hello Outside World! Unnamed stream usage".

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element("Hello Outside World! Unnamed stream usage", frame0.widgetExpr1())
};

frame0.display(elementList0);

Details:

This example demonstrates the usage of the unnamed stream to redirect output to the external file. The contents of the output file is:

Hello Outside World! Unnamed stream usage

Example 7:

output to "test.txt".
display "Hello Outside World!".
output to terminal.
pause.
display "This should be on the screen!".
pause.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element("Hello Outside World!", frame0.widgetExpr1())
};
frame0.display(elementList0);

UnnamedStreams.assignOut(StreamFactory.openTerminalStream(false));
pause();

FrameElement[] elementList1 = new FrameElement[]
{
   new Element("This should be on the screen!", frame0.widgetExpr2())
};
frame0.display(elementList1);
pause();

Details:

Setting the output channel back to the terminal can be done by closing the unnamed output stream or by a OUTPUT TO TERMINAL call. This example writes data to the file but after the pause we will see the screen:

If the TERMINAL option is not used, the output will still go to the file.

Example 8:

def var chout as character format "x(40)" initial "test.txt".
message "The output will be redirected to "+chout.
pause.
output to value( chout ).
display "Hello Outside World!".

Converted code:

message(concat(new character("The output will be redirected to "), chout));
pause();
UnnamedStreams.assignOut(StreamFactory.openFileStream((chout).toStringMessage(), true, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element("Hello Outside World!", frame0.widgetExpr1())
};
frame0.display(elementList0);

Details:

Sometimes it is not suitable to hardcode the targeted resource directed in the OUTPUT TO call. To be able to have the opportunity to program the output channel name on the fly the option VALUE can be used. This example shows how the output will be redirected to the device whose name is specified in the expression variable, chout.

Example 9:

output to "test.txt" append.
display "Redirectwd output in appended mode".
output close.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, true));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element("Redirectwd output in appended mode", frame0.widgetExpr1())
};
frame0.display(elementList0);

UnnamedStreams.closeOut();

The option APPEND can be used to concatenate the new data to the already existed. This example demonstrates this option: the first run of the application creates file if not exist and writes one line of the text. The second run opens existed file and adds the next line and so on. The fact of initializing the APPEND mode can be seen from the true value of the boolean append parameter of the openFileStream() call.

Example 10:

output to "test.txt" paged.
repeat i = 1 to 100:
   down 0 with frame f1.
   display "Paged output, line" line-counter with frame f1.
end.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false, true));

ToClause toClause0 = new ToClause(i, 1, 100);
repeatTo("loopLabel0", toClause0, ..., 
(Body) () -> 
{
   f1Frame.down(0);

   FrameElement[] elementList0 = new FrameElement[]
   {
      new Element("Paged output, line", f1Frame.widgetExpr1()),
      new Element(UnnamedStreams.safeOutput().getNextLineNum(), f1Frame.widgetExpr2())
   };

   f1Frame.display(elementList0);
}));

Details:

The output files can be paged. Use the PAGED option of the OUTPUT TO statement. The output of this 4GL code will be paged every 56 lines by inserting the page separator. This is the default pagination value. If it is required to have another value for the size of the report page the option PAGE-SIZE should be used.

Example 11:

output to "test.txt" paged page-size 10.
repeat i = 1 to 30:
   down 0 with frame f1.
   display "Paged output, line" line-counter with frame f1.
end.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false, 10, true));

ToClause toClause0 = new ToClause(i, 1, 30);

repeatTo("loopLabel0", toClause0, ..., 
(Body) () -> 
{
   f1Frame.down(0);

   FrameElement[] elementList0 = new FrameElement[]
   {
      new Element("Paged output, line", f1Frame.widgetExpr1()),
      new Element(UnnamedStreams.safeOutput().getNextLineNum(), f1Frame.widgetExpr2())
   };

   f1Frame.display(elementList0);
}));

This code will insert the page separator every 10 lines. The only implementation difference is the page size is explicitly set as a parameter for the openFileStream call. The value 0 specified as page size disables the pagination completely. No page separator will be inserted in this case.

Closing the Unnamed Streams

The unnamed streams can be closed using the INPUT CLOSE and OUTPUT CLOSE statements. This section will treat only how these statements which explicitly close the unnamed stream get converted. For details about the implicit closing implementation in FWD, please visit the Closing Streams sub-section in the Working with Streams section of this chapter.

The conversion rules will emit an UnnamedStreams.closeIn() call for any INPUT CLOSE statement and an UnnamedStreams.closeOut() call for any OUTPUT CLOSE statement.

Example 1:

output to test.txt.
output close.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));
UnnamedStreams.closeOut();

Details:

Closing the unnamed output stream means deactivating the unnamed stream and all subsequent output will be sent directly to the terminal.

Example 2:

input from terminal.
input close.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openTerminalStream(false));
UnnamedStreams.closeIn();

Details:

In this case, as the unnamed input stream was already directed at the terminal, the UnnamedStreams.closeIn() call will be a no-op, leaving the default input to be read from the terminal.

Redirected Terminal Usage

In 4GL, the default unnamed output state is the one set by the OUTPUT TO TERMINAL statement, unless the procedure was called by another procedure while a different output destination was active. The output destination at the beginning of the procedure is the current output destination of the calling procedure. The OUTPUT TO TERMINAL PAGED statement clears the screen and displays output on scrolling pages with the length of the screen. The system pauses before each page header and this can be adjusted using the PAUSE statement. If the terminal report is wider than the screen, the report will be wrapped. It is planned to use the output file as the input one for another procedure consider to use the EXPORT statement instead. If the output is sending to target other than terminal the ROW option of the frame will be ignored. Also the frame boxes will not be displayed, the top line will be converted to the blank spaces. All messages will be sent to the current output destination. The memory-output character conversion is defined by the current language settings and packages installed in JRE which is used to run the converted Java application.

This section explains how the redirection of the unnamed output and input streams in the scope of the called procedures affect - or not - the existing redirection in the caller scope. The main focus is to explain how the program is executed.

Example 1:

This program has a master caller program (caller.p) which invokes two other programes (called_1.p and called_2.p). Each program reads data from a specific file, and the output demonstrates how the redirection works.

caller.p:

def var caller_var1 as char format "x(10)" init ?.
def new global shared stream slog.

output stream slog to log.txt.
input from infile_1.txt.

set caller_var1.
export stream slog "caller: first read from unnamed input stream: "+caller_var1.

export stream slog "caller: calling procedure 1".
run called_1.p.

set caller_var1.
export stream slog "caller: second read from unnamed input stream: "+caller_var1.

export stream slog "caller: calling procedure 2".
run called_2.p.

export stream slog "caller: reading from unnamed input stream".
set caller_var1.
export stream slog "caller: third read from unnamed input stream: "+caller_var1.

input close.
output stream slog close.

called_1.p:

def shared stream slog.
def var called1_var1 as char format "x(10)" init ?.
def var called1_var2 as char format "x(10)" init ?.

input close.
input from infile_2.txt.
set called1_var1.
export stream slog "called_1: first read from unnamed input stream: "+called1_var1.
input close.

set called1_var2.
export stream slog "called_1: second read from unnamed input stream: "+called1_var2.

input close.

called_2.p:

def shared stream slog.
def var called2_var1 as char format "x(10)" init ?.

input from terminal.

export stream slog "called_2: reading from terminal".
set called2_var1.
export stream slog "called_2: read from terminal: "+called2_var1.

Converted code:

caller.p:

...
TransactionManager.registerFinalizable(slogStream, true);
SharedVariableManager.addStream(ScopeLevel.GLOBAL, "slogStream", slogStream);
frame0.openScope();
slogStream.assign(StreamFactory.openFileStream("log.txt", true, false));
UnnamedStreams.assignIn(StreamFactory.openFileStream("infile_1.txt", false, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(callerVar1, frame0.widgetCallerVar1())
};
frame0.set(elementList0);

slogStream.export(new FieldEntry[]
{
   new ExportField(() -> concat("caller: first read from unnamed input stream: ", callerVar1))
});
slogStream.export(new FieldEntry[]
{
   new ExportField("caller: calling procedure 1")
});
ControlFlowOps.invoke("called_1.p");

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(callerVar1, frame0.widgetCallerVar1())
};
frame0.set(elementList1);

slogStream.export(new FieldEntry[]
{
   new ExportField(() -> concat("caller: second read from unnamed input stream: ", callerVar1))
});
slogStream.export(new FieldEntry[]
{
   new ExportField("caller: calling procedure 2")
});
ControlFlowOps.invoke("called_2.p");
slogStream.export(new FieldEntry[]
{
   new ExportField("caller: reading from unnamed input stream")
});

FrameElement[] elementList2 = new FrameElement[]
{
   new Element(callerVar1, frame0.widgetCallerVar1())
};
frame0.set(elementList2);

slogStream.export(new FieldEntry[]
{
   new ExportField(() -> concat("caller: third read from unnamed input stream: ", callerVar1))
});
UnnamedStreams.closeIn();
slogStream.closeOut();

called_1.p:

...
UnnamedStreams.closeIn();
UnnamedStreams.assignIn(StreamFactory.openFileStream("infile_2.txt", false, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(called1Var1, frame0.widgetCalled1Var1())
};
frame0.set(elementList0);

slogStream.export(new FieldEntry[]
{
   new ExportField(() -> concat("called_1: first read from unnamed input stream: ", called1Var1))
});
UnnamedStreams.closeIn();

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(called1Var2, frame0.widgetCalled1Var2())
};
frame0.set(elementList1);

slogStream.export(new FieldEntry[]
{
   new ExportField(() -> concat("called_1: second read from unnamed input stream: ", called1Var2))
});
UnnamedStreams.closeIn();

called_2.p:

...
UnnamedStreams.assignIn(StreamFactory.openTerminalStream(false));
slogStream.export(new FieldEntry[]
{
   new ExportField("called_2: reading from terminal")
});

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(called2Var1, frame0.widgetCalled2Var1())
};
frame0.set(elementList0);

slogStream.export(new FieldEntry[]
{
   new ExportField(() -> concat("called_2: read from terminal: ", called2Var1))
});

Details:

The input files have specific content in order to be able to distinguish the file based on a value read from it: variables are read from the unnamed input stream at times and printed to a log file (log.txt). Analyzing the log file allows us to tell the current redirection of the unnamed input stream at the time the log line was printed. Hence the three input files have the following content:

infile_1.txt:

aaa1
aaa2
aaa3

infile_2.txt:

bbb1
bbb2
bbb3

When running caller.p, at some point the user is prompted to enter values at the terminal; we enter the value “from_term”, in order to mark the terminal redirection in the log file.

When the program finishes, the content of the log file is the following:

"caller: first read from unnamed input stream: aaa1" 
"caller: calling procedure 1" 
"called_1: first read from unnamed input stream: bbb1" 
"called_1: second read from unnamed input stream: aaa2" 
"caller: second read from unnamed input stream: aaa3" 
"caller: calling procedure 2" 
"called_2: reading from terminal" 
"called_2: read from terminal: from_term" 
"caller: reading from unnamed input stream" 
"caller: third read from unnamed input stream: from_term" 

It can be noted how the third and last value that is read in caller.p, is input from the terminal. Directing the unnamed input to terminal (by setting null as the redirection) has the effect of closing the default redirection of the unnamed stream. As a consequence, the caller procedure has no open stream assigned to the unnamed input stream and, as such, the terminal is used.

As a side note, it's worth mentioning the effect of the explicit stream closing statements: the default redirection is restored and the called procedure reads the “aaa2” value from the file used as the unnamed input stream in the caller (infile_1.txt).

Example 2:

caller_out.p:

def var msg as char format "x(20)" init ?.

output to outfile.txt.

export "caller: calling procedure 1".
run called_out_1.p.

export "caller: calling procedure 2".
run called_out_2.p.

export "caller: calling procedure 3".
run called_out_3.p.

msg="caller: done".
display msg.

pause.

output close.

And here are the called procedures, with each 4GL snippet followed by its Java counterpart (when working with the terminal we use DISPLAY instead of EXPORT since EXPORT cannot work with the terminal).

called_out_1.p:

output close.
export "called_out_1: writing to unnamed output after closing existing redirection".

called_out_2.p:

output to outfile_2.txt.
export "called_out_2: writing to unnamed output after new redirection".

called_out_3.p:

def var msg as char format "x(40)" init ?.

output to terminal.

msg="called_out_3: writing to terminal".
display  msg.

Converted code:

caller_out.p:

UnnamedStreams.assignOut(StreamFactory.openFileStream("outfile.txt", true, false));
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField("caller: calling procedure 1")
});
ControlFlowOps.invoke("called_out_1.p");
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField("caller: calling procedure 2")
});
ControlFlowOps.invoke("called_out_2.p");
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField("caller: calling procedure 3")
});
ControlFlowOps.invoke("called_out_3.p");
msg.assign("caller: done");

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(msg, frame0.widgetMsg())
};

frame0.display(elementList0);

pause();
UnnamedStreams.closeOut();

called_out_1.p:

UnnamedStreams.closeOut();
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField("called_out_1: writing to unnamed output after closing existing redirection")
});

called_out_2.p:

UnnamedStreams.assignOut(StreamFactory.openFileStream("outfile_2.txt", true, false));
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField("called_out_2: writing to unnamed output after new redirection")
});

called_out_3.p:

UnnamedStreams.assignOut(StreamFactory.openTerminalStream(false));
msg.assign("called_out_3: writing to terminal");

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(msg, frame0.widgetMsg())
};
frame0.display(elementList0);

Details:

When executing caller_out.p, two files are created on disk (outfile.txt and outfile_2.txt):

outfile.txt:

"caller: calling procedure 1" 
"called_out_1: writing to unnamed output after closing existing redirection" 
"caller: calling procedure 2" 
"caller: calling procedure 3" 

outfile_2.txt:

"called_out_2: writing to unnamed output after new redirection" 

At the terminal, two values are displayed: the line output from the third procedure (“called_out_3: writing to terminal”) and the ”caller: done” message in the main program. The latter shows that unlike file redirections of the unnamed output stream, terminal redirection in called procedures overrides the redirection of the unnamed stream in the calling procedure, however the stream (file, process etc) that the unnamed output stream used to point to in the calling procedure initially is NOT closed and the user can continue to use it in the FWD program.

Hence there are important differences depending on whether it is the unnamed input or unnamed output that is redirected. The default unnamed output stream can't be closed by an assignment of a new redirection, while the default unnamed input stream will be closed if it is NOT the terminal and the new assignment is redirecting to the terminal.

The registration of the stream itself for a callback at the top-level block scope exit only occurs for a stream that is newly registered. That means that the top-level block in which an open stream is first used for redirection of the unnamed stream, will cause that stream to be closed implicitly when that scope exits.

Process Streams

4GL allows receiving or sending data to/from a process by connecting a stream's input or output end to the process's output or input, respectively. This is done using some specialized 4GL statements, as INPUT THROUGH, OUTPUT THROUGH or INPUT-OUTPUT THROUGH. Each statement can work with named or unnamed streams. When using unnamed streams, the terminal's input and output channels are connected to the process, and all the data is received from or sent to the process.

When the 4GL code opens a process stream using one of these statements, the conversion rules will emit these calls:

  • The process stream is opened using one of the specialized APIs in the Stream or UnnamedStreams classes, depending on the type of stream (named or unnamed).
  • The target process is launched with a ProcessOps.launch call. Full details about this function can be found in the Process Launching chapter of this book.
  • The parameters of the designated stream are set. The set of available parameters differs for INPUT, OUTPUT and INPUT-OUTPUT cases, these cases will be considered further in the next sections.

Once a stream is opened for process communication, the process is launched and its input and/or output channels are linked to this stream instance. Launching is done with one of the launch methods in the ProcessOps class. The command is launched as a child process of the current JVM process on the client side. When used in an INPUT/OUTPUT/INPUT-OUTPUT THROUGH statement, launch can take the following forms:

  • launch(String[] cmdlist, StreamWrapper sout, StreamWrapper sin): asynchronously executes a command (given in the cmdlist parameter) as a child process, connecting the given named streams sin and sout (if not null) to the process' STDIN and STDOUT/ STDERR , respectively. The two streams must be pointing to a@ ProcessStream instance on the client side.
  • launch(String[] cmdlist, RemoteStream sout, RemoteStream sin): similar to the one above, but takes a RemoteStream reference instead of a StreamWrapper. Used in the converted code when using the unnamed streams.

These methods allow FWD code to use different streams (possibly of different types) to read/write from/to a process, something that is not possible in 4GL (e.g. a named stream for input and the unnamed output stream for output).

On client side, when the child process is launched, its STDIN and/or STDOUT/STDERR are either connected to a pipe or to the parent process' terminal depending on the flow direction specified in the stream definition. The STDIO pipes from a child process are fully supported. The common process launching infrastructure is used to start the child process but a special ProcessStream class is used to implement the proper user-driven reading/writing support for these pipes.

All reads are done from a "combined" STDOUT and STDERR, where output will first be read from STDOUT and then from STDERR. Due to the "combining" semantic of Progress, a polling method is used to perform I/O without blocking on one of the pipes (thus ignoring input from the other pipe). In addition, an extra thread is used to wait for the termination of the child process. This allows these polling loops to exit when the pipes are empty and the child process has exited, which eliminates infinite read blocking. But, there are some limitations for the process streams:

  • SEEK statement and SEEK function do not work for process streams.
  • If an error has happened while writing data for a process stream (e.g. the process which receives data from a Progress procedure has been ended abnormally) then, because of some internal buffer, Progress displays the error message Pipe to subprocess has been broken and raises STOP condition only after some specific amount of data has been written to the process after the error has happened. FWD tries to reproduce this behavior, and the code which is reliable for this is in the com.goldencode.p2j.util.BufferSizeManager class.

INPUT THROUGH Statement

The INPUT THROUGH statement links the output from a process to the input of a 4GL program. The syntax of this statement is:

INPUT [ STREAM stream ] THROUGH
  { program-name | VALUE ( expression ) }
  [ argument | VALUE ( expression ) ] ...
  [ ECHO | NO-ECHO ]
  [ MAP protermcap-entry | NO-MAP ]
  [ UNBUFFERED ]
  [ NO-CONVERT
    | { CONVERT
        [ TARGET target-codepage ]
        [ SOURCE source-codepage ]
      }
  ]

From the list of possible options, FWD does not support the NO-CONVERT, CONVERT, MAP and NO-MAP options. All the other stream-related options, as ECHO, NO-ECHO and UNBUFFERED are supported by FWD and the conversion rules handle them as described in the INPUT FROM statement for the named and unnamed streams, in the Working with Streams section of this chapter.

As with the INPUT FROM statement, when the STREAM stream clause is used, the INPUT THROUGH statement is using a named stream.

The executed process is specified using the { program-name | VALUE (expression) } [ argument | VALUE (expression) ] ... options, and their usage is explained in the Process Launching chapter of this book. This section will explain how the stream's input channel is linked to the process's output channel, in the converted code. Also, details about how this parameter is converted can be found in the Specifying Program to Run paragraph of this section.

When the process is used with a named stream, the Stream.assign(StreamFactory.openProcessStream()) API is emitted to open and initialize this stream for process support. For unnamed streams, the UnnamedStreams.assignIn(StreamFactory.openProcessStream()) call is emitted, to initialize the unnamed input stream to read data from a process.

Once this is done, the conversion rules will emit code to launch the actual process, via a ProcessOps.launch call. Full information about this function can be found in the Process Launching chapter of this book.

The emitted code for named streams will look like:

<stream>.assign(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   <command>
}, (StreamWrapper) <stream>, (StreamWrapper) null);

while for the unnamed streams:

UnnamedStreams.assignIn(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
    <command>
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);

where <command> is a string expression representing the external program to be executed and <stream> is a StreamWrapper instance associated with a named stream. For named streams, the ProcessOps.launch receives as parameter the stream reference and for unnamed streams, the reference returned by the UnnamedStreams.safeInput() call. These references are passed as the second parameter for the ProcessOps.launch command, which is always the input channel where the process will send its data.

Example 1:

def stream s.
input stream s through value("echo test") unbuffered no-echo.

Converted code:

...
/* Create a process stream and assign the designated stream to it. */
sStream.assign(StreamFactory.openProcessStream());

/* Launch the target process. */
ProcessOps.launch(new String[]
{
   "echo test" 
}, (StreamWrapper) sStream, (StreamWrapper) null);

/* Set parameters of the designated stream. */
sStream.setUnbuffered();
sStream.setEcho(false);

Details:

This is a simple example which demonstrates how the named input stream is linked to the process's output channel. Note how the stream's options are set in the same way as for a stream which opens a non-process resource.

Example 2:

def var pid  as integer format ">>>>>>>z".
def var ppid as integer format ">>>>>>>z".
def stream s.

/* open input process stream */
input stream s through echo $$ $PPID.

/* read PID and parent PID from the stream */
import stream s pid ppid.

/* close input process stream */
input stream s close.

/* display PIDs */
display pid label "Process PID" 
        ppid label "FWD Client PID" with frame f1.

Converted code:

/* open input process stream */
sStream.assign(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo",
   "$$",
   "$PPID" 
}, (StreamWrapper) sStream, (StreamWrapper) null);

/* read PID and parent PID from the stream */
sStream.readField(pid);
sStream.readField(ppid);
sStream.resetCurrentLine();

/* close input process stream */
sStream.closeIn();

/* display PIDs */
FrameElement[] elementList0 = new FrameElement[]
{
   new Element(pid, f1Frame.widgetPid()),
   new Element(ppid, f1Frame.widgetPpid())
};
f1Frame.display(elementList0);

Details:

This example uses an INPUT THROUGH statement with a named stream to spawn a new process that executes a command for obtaining the PID (process identifier) of the JVM process that runs FWD client and of the process itself.

Example 3:

def var pid  as integer format ">>>>>>>z".
def var ppid as integer format ">>>>>>>z".

/* open unnamed input process stream */
input through echo $$ $PPID.

/* read PID and parent PID from the unnamed stream */
import pid ppid.

/* close unnamed input process stream */
input close.

/* display PIDs */
display pid label "Process PID" 
        ppid label "FWD Client PID" with frame f1.

Converted code:

/* open unnamed input process stream */
UnnamedStreams.assignIn(StreamFactory.openProcessStream())
ProcessOps.launch(new String[]
{
   "echo",
   "$$",
   "$PPID" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);

/* read PID and parent PID from the unnamed stream */
UnnamedStreams.safeInput().readField(pid);
UnnamedStreams.safeInput().readField(ppid);
UnnamedStreams.safeInput().resetCurrentLine();

/* close unnamed input process stream */
UnnamedStreams.closeIn();

/* display PIDs */
FrameElement[] elementList0 = new FrameElement[]
{
   new Element(pid, f1Frame.widgetPid()),
   new Element(ppid, f1Frame.widgetPpid())
};
f1Frame.display(elementList0);

Details:

This example uses an INPUT THROUGH statement with an unnamed stream to spawn a new process that executes a command for obtaining the PID (process identifier) of the JVM process that runs FWD client of and of the process itself.

OUTPUT THROUGH Statement

The OUTPUT THROUGH statement links the input from a process to the output of a 4GL program. The syntax of this statement is:

OUTPUT [ STREAM stream ] THROUGH
  { program-name | VALUE ( expression ) }
  [ argument | VALUE ( expression ) ] ...
  [ ECHO | NO-ECHO ]
  [ MAP protermcap-entry | NO-MAP ]
  [ PAGED ]
  [ PAGE-SIZE { constant | VALUE ( expression ) } ]
  [ UNBUFFERED ]
  [ NO-CONVERT
    | { CONVERT
        [ TARGET target-codepage ]
        [ SOURCE source-codepage ]
      }
  ]

From the list of possible options, FWD does not support the NO-CONVERT, CONVERT, MAP and NO-MAP options. All the other stream-related options, as PAGED, PAGE-SIZE, ECHO, NO-ECHO and UNBUFFERED are supported by FWD and the conversion rules handle them as described in the OUTPUT FROM statement for the named and unnamed streams.

As with the OUTPUT FROM statement, when the STREAM stream clause is used, the OUTPUT THROUGH statement is using a named stream.

The executed process is specified using the { program-name | VALUE (expression) } [ argument | VALUE (expression) ] ... options, and their usage is explained in the Process Launching chapter of this book. This section will explain how the stream's output channel is linked to the process's input channel. Also, details about how this parameter is converted can be found in the Specifying Program to Run paragraph of this section.

When the process is used with a named stream, the Stream.assign(StreamFactory.openProcessStream()) API is emitted to open and initialize this stream for process support. For unnamed streams, the UnnamedStreams.assignOut(StreamFactory.openProcessStream()) call is emitted, to initialize the unnamed input stream to send data to a process.

Once this is done, the conversion rules will emit code to launch the actual process, via a ProcessOps.launch call. Full information about this function can be found in the Process Launching chapter of this book.

The emitted code for named streams will look like:

<stream>.assign(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
    <command>
}, (StreamWrapper) null, (StreamWrapper) <stream>);

while for the unnamed streams:

UnnamedStreams.assignOut(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
    <command>
}, (RemoteStream) null, (RemoteStream) UnnamedStreams.safeOutput());

where <command> is a string expression representing the external program to be executed and <stream> is a StreamWrapper instance associated with a named stream. For named streams, the ProcessOps.launch receives as parameter the stream reference and for unnamed streams, the reference returned by the UnnamedStreams.safeInput() call. These references are passed as the third parameter for the ProcessOps.launch command, which is always the output channel from where the process will read its data.

Example 4:

output through "cat > file1.txt".

Converted code:

UnnamedStreams.assignOut(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "cat > file1.txt" 
}, (RemoteStream) null, (RemoteStream) UnnamedStreams.safeOutput());

Details:

This is a simple example which redirects all the terminal output to the file1.txt file and demonstrates how the unnamed output stream is linked with the process's input pipe.

Example 5:

def var ch as char.
def stream s.

/* open output process stream */
output stream s through "wc -w > wcdata.txt".

/* send the sentence to the process stream */
ch = "The quick brown fox jumps over the lazy dog".
export stream s ch.

/* close output process stream */
output stream s close.

Converted code:

/* open output process stream */
sStream.assign(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "wc -w > wcdata.txt" 
}, (StreamWrapper) null, (StreamWrapper) sStream);

/* send the sentence to the process stream */
ch.assign("The quick brown fox jumps over the lazy dog");
sStream.export(new FieldEntry[]
{
   new ExportField(ch)
});

/* close output process stream */
sStream.closeOut();

Details:

This example uses an OUTPUT THROUGH statement with a named stream to spawn a new process that executes a command for counting words in the input stream (using wc utility) and writes the value containing the number of words to the specified file.

After the procedure is executed, wcdata.txt has the following content:

9

Example 6:

def var ch as char.

/* open unnamed output process stream */
output through "wc -w > wcdata.txt".

/* send the sentence to the process stream */
ch = "The quick brown fox jumps over the lazy dog".
export ch.

/* close unnamed output process stream */
output close.

Converted code:

/* open unnamed output process stream */
UnnamedStreams.assignOut(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "wc -w > wcdata.txt" 
}, (RemoteStream) null, (RemoteStream) UnnamedStreams.safeOutput());

/* send the sentence to the unnamed process stream */
ch.assign("The quick brown fox jumps over the lazy dog");
UnnamedStream.safeOutput().export(new FieldEntry[]
{
   new ExportField(ch)
});

/* close output process stream */
UnnamedStream.closeOut();

Details:

This example uses an OUTPUT THROUGH statement with an unnamed stream to spawn a new process that executes a command for counting words in the input stream (using wc utility) and writes the value containing the number of words to the specified file. The output is the same as with the previous example. The goal is to demonstrate how the unnamed output stream is linked with the external process, via the third parameter for the ProcessOps.launch command.

INPUT-OUTPUT THROUGH Statement

The INPUT-OUTPUT THROUGH statement links both the output and input from a process to the input and output of a 4GL program. The syntax of this statement is:

INPUT-OUTPUT [ STREAM stream ] THROUGH
  { program-name | VALUE ( expression ) }
  [ argument | VALUE ( expression ) ] ...
  [ ECHO | NO-ECHO ]
  [ MAP protermcap-entry | NO-MAP ]
  [ UNBUFFERED ]
  [ NO-CONVERT
    | { CONVERT
        [ TARGET target-codepage ]
        [ SOURCE source-codepage ]
      }
  ]

From the list of possible options, FWD does not support the NO-CONVERT, CONVERT, MAP and NO-MAP options. All the other stream-related options, as ECHO, NO-ECHO and UNBUFFERED are supported by FWD and the conversion rules handle them as described in the Working with Streams section of this chapter.

As with the INPUT FROM and OUTPUT TO statements, when the STREAM stream clause is used, the INPUT-OUTPUT THROUGH statement is using a named stream.

The executed process is specified using the { program-name | VALUE (expression) } [ argument | VALUE (expression) ] ... options, and their usage is explained in the Process Launching chapter of this book. This section will explain how the stream's input channel is linked to the process's output channel. Also, details about how this parameter is converted can be found in the Specifying Program to Run parapraph of this section.

When the process is used with a named stream, the Stream.assign(StreamFactory.openProcessStream()) API is emitted to open and initialize this stream for process support. For unnamed streams, the UnnamedStreams.assignBoth(StreamFactory.openProcessStream()) call is emitted, to initialize both the unnamed input and output streams to work with the process.

Once this is done, the conversion rules will emit code to launch the actual process, via a ProcessOps.launch call. Full information about this function can be found in the Process Launching chapter of this book.

The emitted code for named streams will look like:

<stream>.assign(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
    <command>
}, (StreamWrapper) <stream>, (StreamWrapper) <stream>);

while for the unnamed streams:

UnnamedStreams.assignBoth(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
    <command>
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) UnnamedStreams.safeOutput());

where <command> is a string expression representing the external program to be executed and <stream> is a StreamWrapper instance associated with a named stream. For named streams, the ProcessOps.launch receives as parameters the stream reference and for unnamed streams, the reference returned by the UnnamedStreams.safeInput() and UnnamedStreams.safeOutput() calls. These references are passed as the second and third parameters for the ProcessOps.launch command, to link the process output channel with the input stream and the process input channel with the output stream.

Example 7:

def var greet as char format "x(20)".
def stream s.

/* open input-output process stream */
input-output stream s through "read name; echo Hello, $name!".

/* send the name to the process stream */
put stream s unformatted "John" skip.

/* get the greeting message from the process stream */
import stream s delimiter "\n" greet.

/* close input-output process stream */
input-output stream s close.

/* display the greeting message */
display greet label "Greeting message" with frame f1.

Converted code:

/* open input-output process stream */
sStream.assign(StreamFactory.openProcessStream());
sStream.setEcho(false);
ProcessOps.launch(new String[]
{
   "read name; echo Hello, $name!" 
}, (StreamWrapper) sStream, (StreamWrapper) sStream);

/* send the name to the process stream */
sStream.putUnformatted(new FieldEntry[]
{
   new PutField("John"),
   new SkipField()
});

/* get the greeting message from the process stream */
sStream.setDelimiter("\n");
sStream.readField(greet);
sStream.resetCurrentLine();

/* close input-output process stream */
sStream.close();

/* display the greeting message */
FrameElement[] elementList0 = new FrameElement[]
{
   new Element(greet, f1Frame.widgetGreet())
};
f1Frame.display(elementList0);

Details:

Here an INPUT-OUTPUT THROUGH statement is used with a named stream to spawn a new process that executes a command which reads a name and returns the greeting message “Hello, $name!”. The output of the procedure is:

Greeting message
--------------------
Hello, John!

See how the sStream reference is passed as the input and output parameters for the ProcessOps.launch command.

Example 8:

def var greet as char format "x(20)".

/* open unnamed input-output process stream */
input-output through "read name; echo Hello, $name!".

/* send the name to the process stream */
put unformatted "John" skip.

/* get the greeting message from the process stream */
import delimiter "\n" greet.

/* close unnamed input-output process stream */
input-output close.

/* display the greeting message */
display greet label "Greeting message" with frame f1.

Converted code:

/* open input-output process stream */
UnnamedStreams.assignBoth(StreamFactory.openProcessStream());
UnnamedStreams.safeInput().setEcho(false);
ProcessOps.launch(new String[]
{
   "read name; echo Hello, $name!" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) UnnamedStreams.safeOutput());

/* send the name to the process stream */
UnnamedStreams.safeOutput().putUnformatted(new FieldEntry[]
{
   new PutField("John"),
   new SkipField()
});

/* get the greeting message from the process stream */
UnnamedStreams.safeInput().setDelimiter("\n");
UnnamedStreams.safeInput().readField(greet);
UnnamedStreams.safeInput().resetCurrentLine();

/* close unnamed input-output process stream */
UnnamedStreams.closeBoth();

/* display the greeting message */
FrameElement[] elementList0 = new FrameElement[]
{
   new Element(greet, f1Frame.widgetGreet())
};
f1Frame.display(elementList0);

Details:

Here an INPUT-OUTPUT THROUGH statement is used with the unnamed input and output streams to spawn a new process that executes a command which reads a name and returns the greeting message “Hello, $name!”. The content of the file is the same as in the previous example, the goal being to demonstrate how the unnamed input and output streams are used by the conversion rules to handle this statement.

Closing Process Streams

Process streams can be explicitly closed using the INPUT CLOSE, OUTPUT CLOSE or INPUT-OUTPUT CLOSE statements. The first two statements are the same as the ones used to close streams which access non-process resources, and convert in the same way: Stream.closeIn() and Stream.closeOut() for named streams, and UnnamedStreams.closeIn() and UnnamedStreams().closeOut() for unnamed streams.

The INPUT-OUTPUT CLOSE statement is used to close the named stream and the unnamed input and output streams opened with an INPUT-OUTPUT THROUGH statement. It gets converted to a Stream.close() call for the named streams and to an UnnamedStreams.closeBoth() for the unnamed streams.

The details about implicitly closing the named and unnamed streams specified in the _Working with Stream_s section apply to the process streams, too.

Example 9:

def stream s1.
def stream s2.

input stream s1 through value("echo test").
output stream s2 through value("echo").

input-output through value("echo test").

input stream s1 close.
output stream s2 close.

input-output close.

Converted code:

...
s1Stream.assign(StreamFactory.openProcessStream());
...
s2Stream.assign(StreamFactory.openProcessStream());
...
UnnamedStreams.assignBoth(StreamFactory.openProcessStream());
...
s1Stream.closeIn();
s2Stream.closeOut();
UnnamedStreams.closeBoth();

Details:

This example shows how the stream closing statements gets converted, for named and unnamed streams.

Opening Streams in UNBUFFERED Mode

Setting a stream in UNBUFFERED mode will make it read one character at a time from an input process stream or write one character at a time to an output process stream. This option applies to INPUT FROM, OUTPUT TO, INPUT THROUGH, OUTPUT THROUGH and INPUT-OUTPUT THROUGH statements. The conversion rules will emit a setUnbuffered() call for the named or unnamed stream reference, whenever the UNBUFFERED clause is used with the above statements.

In FWD this option does not affect the buffering process. The only thing that it affects is the buffer size emulation in case of a stream error.

This option is supported on the compilation stage but not yet fully implemented in runtime. So it is not recommended to intermix the 4GL output with the output generated by another OS process.

Example 1:

def var chvar as character initial "Hello unbuffered world" format "x(40)".
output to "test.txt" unbuffered.
os-command no-wait no-console "uname -a > output.txt".
display chvar no-label.

Converted code:

character chvar = new character("Hello unbuffered world");
...
UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));
UnnamedStreams.safeOutput().setUnbuffered();
ProcessOps.launch(new String[]
{
   "uname -a > output.txt" 
}, false, false);

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(chvar, frame0.widgetChvar())
};
frame0.display(elementList0);

Details:

In this example the output from the

MESSAGE statement is intermixed with the external OS command routed to the same output file. After this example is executed, the output file will have something like this:

Hello unbuffered world
-0.9-desktop #1 SMP PREEMPT 2011-10-19 22:33:27 +0200 x86_64 x86_64 x86_64 GNU/Linux

Note the result of the OS statement is partially overwritten with the following MESSAGE statement. In real non-buffered output, the order of the output statements must be preserved.

Example 2:

def var intCount as integer initial 2.
message "Before recalculation:" intCount.
input-output through "cost" unbuffered.
export intCount.
set intCount.
input-output close.
message "After recalculation:" intCount.

Converted code:

message(new Object[]
{
   "Before recalculation:",
   intCount
});
UnnamedStreams.assignBoth(StreamFactory.openProcessStream());
UnnamedStreams.safeInput().setEcho(false);
ProcessOps.launch(new String[]
{
   "cost" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) UnnamedStreams.safeOutput());
UnnamedStreams.safeInput().setUnbuffered();
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField(intCount)
});

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(intCount, frame0.widgetIntCount())
};
frame0.set(elementList0);

UnnamedStreams.closeBoth();
message(new Object[]
{
   "After recalculation:",
   intCount
});

The cost.c source code:

#include <stdio.h>
main( )
{
   int count;
   setbuf(stdout, (char *) NULL);
   while (scanf("%u", &count) == 1) {
      /* We take the value, modify and return back */
      count = count + 5;
      printf("%8u\n", count);
   }
}

Details:

This is an example where this option is really useful, when a 4GL program and an external one written in C language cooperate. After application starts, it executes the external executable and exchange data with it:

Opening Streams in BINARY Mode

This option allows the input to be read or output to be written without any conversion or interpretation. It applies to the INPUT FROM and OUTPUT TO statements, with both the named and unnamed streams.

To set the binary mode for the stream, the conversion rules will emit a setBinary() call for the named or unnamed stream reference.

Example:

input from "test.txt" binary.
readkey.
message "Binary mode for 0xOD:"+lastkey.
readkey.
message "Binary mode for 0xOA:"+lastkey.
...
input from "test.txt".
readkey.
message "Usual mode for 0x0D:"+lastkey.
readkey.
message "Usual mode for 0x0A:"+lastkey.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));
UnnamedStreams.safeInput().setBinary();
KeyReader.readKey();
message(concat(new character("Binary mode for 0xOD:"), lastKey()));
KeyReader.readKey();
message(concat(new character("Binary mode for 0xOA:"), lastKey()));
...
UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));
KeyReader.readKey();
message(concat(new character("Usual mode for 0x0D:"), lastKey()));
KeyReader.readKey();
message(concat(new character("Usual mode for 0x0A:"), lastKey()));

Details:

The file contains only two bytes: 0x0D and 0x0A. Note that the last byte is the EOF character. After application starts the screen is:

showing the binary mode for control character reading. Press any key to see the usual file reading results:

Note the 0x0A is no longer read, as end of file is reached.

Opening Streams in ECHO and NO-ECHO modes

The ECHO and NO-ECHO modes displays or not all input data on the current output device. Data is echoed by default. Applies to INPUT FROM, OUTPUT TO, INPUT THROUGH, OUTPUT THROUGH and INPUT-OUTPUT THROUGH statements, for both the named and unnamed streams.

As 4GL documentation describes, this option:

  • For input streams: displays all input data on the current output destination. Data is echoed by default.
  • For output streams: sends all data from an input stream to the output stream. Data is echoed by default.
  • For input-output stream: displays all input data to the unnamed stream. Data is not echoed by default.

To set the NO-ECHO mode for input or output streams, a setEcho(false) call will be emitted for the named or unnamed stream reference, in the converted code. As data for input-output streams is not echoed by default, when no explicit ECHO or NO-ECHO clause is specified, a setEcho(false) call is emitted for the given input-output stream, named or unnamed.

In FWD this option works in the following way: echoing of an input stream happens if the input stream is marked for echoing AND no output redirection is active (i.e. the unnamed stream outputs to the terminal) or output redirection is active (i.e. the unnamed stream outputs through a process or to a file) and that stream is marked for echoing.

Example 1:

def var ch as char.
def stream s.

input stream s through "echo test" echo.
output through "cat > file.txt" echo.

set stream s ch.

Converted code:

sStream.assign(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo test" 
}, (StreamWrapper) sStream, (StreamWrapper) null);
sStream.setEcho(true);
UnnamedStreams.assignOut(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "cat > file.txt" 
}, (RemoteStream) null, (RemoteStream) UnnamedStreams.safeOutput());
UnnamedStreams.safeOutput().setEcho(true);

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(ch, frame0.widgetCh())
};
frame0.set(sStream, elementList0);

Details:

Here the value “test” is read from the stream and the frame containing this value is sent to file.txt. After execution, the output of the file will be:

ch
--------
test

Note that echoing happens when data is read using UPDATE/SET/PROMPT-FOR and does not happen when data is read using READKEY or IMPORT.

Example 2:

input from "test.txt" no-echo.
set chVar.
input close.
message chVar.
pause.
input from "test.txt" echo.
set chVar.
input close.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));
UnnamedStreams.safeInput().setEcho(false);

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(chVar, frame0.widgetChVar())
};
frame0.set(elementList0);

UnnamedStreams.closeIn();
message(chVar);
pause();
UnnamedStreams.assignIn(StreamFactory.openFileStream("test.txt", false, false));
UnnamedStreams.safeInput().setEcho(true);

FrameElement[] elementList1 = new FrameElement[]
{
   new Element(chVar, frame0.widgetChVar())
};
frame0.set(elementList1);

UnnamedStreams.closeIn();

Details:

NO-ECHO is the opposite for the ECHO option, as it disables echoing of the input data to the output target (see the ECHO option above for more information). In this example, the echo mode will be turned off.

After starting the screen is (this is the message statement - the only way to see the variable):

As you note the value of the variable read from file is not duplicated to the output device. Press a key to restore the default mode:

The input file test.txt contains the following string:

This_is_the_echo_mode_demo

Stream Only I/O Statements

For cases when data needs to be written to a file and then read back by (some other part of) the application, 4GL provides statements which work only with streams and allow the writing and reading of data, using a certain format. This allows the user to write code which can write and then read back the same data, without any other complexities. For this, 4GL provides the EXPORT, IMPORT and PUT statements, which can work only with named streams or with unnamed streams, when the terminal is redirected to some other resource. EXPORT statement converts data to a standard character format and displays it to the current output destination or to a named output stream.
IMPORT statement reads the line from an input file that might be created by the preceding EXPORT statement or other external tool according to the EXPORT statement style. PUT statement prepares the data in a fixed format possibly more acceptable to use by another system. In the following sections we consider these statements in detail and with particular examples applying to the subject of this chapter.

EXPORT Statement

The statement is intended to write the data to the file using a format compatible with the IMPORT statement. The result is a set of lines consisted of the formatted data, separated by the delimiter char. By default the delimiter is one space character.

If this statement is executed with the unnamed stream and the unnamed stream targets the terminal, then this will be a no-op.

The 4GL syntax of the statement is:

EXPORT [ STREAM stream ] [ DELIMITER character ]
   {   expression ...
     | record [ EXCEPT field ... ]
   }

and the available options are:

4GL syntax Description Supported
STREAM stream Specifies the name of the stream.
This parameter is optional and can be omitted. In this case the output should be redirected to the file using the OUTPUT TO statement before using the EXPORT statement.
Yes
DELIMITER character The character to use as a delimiter between field values. The character must be a quoted single character. The default value is a space character.
Is set using the setDelimiter() API in the Stream class and can be set before the export call.
Yes
expression One or more expression to convert into standard character format to an output destination.  
record The name of the record buffer for which its fields should be written to the file. During conversion, the FWD rules will automatically emit export-style field definitions for all buffer's field, in the converted export statement. Yes
[ EXCEPT field ] The name of the(s) field that must be excluded from being written to the output file when using the record option mentioned above. This option is handled at conversion time, by not omitting field definition for the omitted fields, in the converted export statement. Yes

The FWD counterpart for this statement is:

Stream.export( FieldEntry data[] )

where data is the array list of the fields to be exported and each element in the array is an instance of com.goldencode.p2j.util.ExportField class.

When using simple variables or constants, the ExportField has c'tors which receive as parameter a BaseDataType value (for 4GL compatible variables/expressions) or Java-compatible value. When record fields are used, each field will be accessed using a FieldReference instance, so the field's value will be resolve when it is actually exported. Same for complex expression: a Resolvable instance will be passed as a parameter to the ExportField c'tor, which will evaluate it when it needs to write it.

When writing the data to the stream, export uses a special format, which can not be overwritten by the user. This format is explained in the following table, for each 4GL type:

4GL Type Export Format
character The entire string is written to the file, enclosed in double quotes; any double quotes inside the string are doubled.
date This format is unusual in that it does not have a fixed width for the year but in fact it ensures that all years are formatted to at least 3 digits (with leading zeros) up to a maximum of 6 digits (for the year -32768 which is the widest possible Progress year) as necessary. The months and days are always formatted as '99'. Another 'quirk' of this format is that if the year is within the Y2K window, it is formatted as a 2 digit year. The separators are always '/' characters.
The implementation creates the given string based on the order of date components (e.g. MDY, YMD...) defined for this context.
decimal This is a signed “dynamic” format in radix 10, which uses a maximum of 50 digits for the integer part and maximum 50 digits for the fractional part, using the decimal and group separators defined in the current context.
integer The signed representation returned by Integer.toString(), in radix 10, without grouping.
logical yes for true and no for false.
handle It uses the >>>>>>>>>9 format to write the data.
memptr The string representation of its content.
raw The 0x02 hex bytes, followed by 4 bytes in hex with the data's length, ending with the base64 encoding of the data.
rowid The hex representation of the ID, with a 0x prefix.
unknown The ? character.

Example 1:

def var chVar1 as char initial "Hello".
def var chVar2 as char initial "Export".
def var intVar as integer initial 20.
def var decVar as decimal initial 3.14.

output to "redirected_training25.txt".
export chVar1 chvar2 intVar decVar.

Converted code:

character chVar1 = UndoableFactory.character("Hello");
character chVar2 = UndoableFactory.character("Export");
integer intVar = UndoableFactory.integer((long) 20);
decimal decVar = UndoableFactory.decimal(decimal.fromLiteral("3.14"));
...
UnnamedStreams.assignOut(StreamFactory.openFileStream("redirected_training25.txt", true, false));
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField(chVar1),
   new ExportField(chVar2),
   new ExportField(intVar),
   new ExportField(decVar)
});

Details:

After the program is executed, the output file is:

"Hello" "Export" 20 3.14

Note the data formatting in the output line. Data is converted to the character format with adding ” chars to the character variables. The unknown values will be exported as unquoted question mark (?). The logical fields will be exported as YES or NO. The delimiter is the default space character for this example.

Example 2:

def var chVar1 as char initial "Hello".
def var chVar2 as char initial "Export".
def var intVar as integer initial 20.
def var decVar as decimal initial 3.14.

define stream mystr.
output stream mystr to "redirected_training26.txt".
export stream mystr chVar1 chvar2 intVar decVar.

Converted code:

Stream mystrStream = new StreamWrapper("mystr");
...
TransactionManager.registerTopLevelFinalizable(mystrStream, true);
mystrStream.assign(StreamFactory.openFileStream("redirected_training26.txt", true, false));
mystrStream.export(new FieldEntry[]
{
   new ExportField(chVar1),
   new ExportField(chVar2),
   new ExportField(intVar),
   new ExportField(decVar)
});

Details:

This example is using the defined stream as transport for the EXPORT statement. After the program is finished, the output file will be the same as in previous example. Note now the export call is associated with the defined mystrStream field (the placeholder of the converted named stream), instead of the static class UnnamedStreams usage.

Example 3:

def var chVar1 as char initial "Hello".
def var chVar2 as char initial "Export".
def var intVar as integer initial 20.
def var decVar as decimal initial 3.14.

export delimiter ";" chVar1 chvar2 intVar decVar.

Converted code:

...
UnnamedStreams.safeOutput().setDelimiter(";");
UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField(chVar1),
   new ExportField(chVar2),
   new ExportField(intVar),
   new ExportField(decVar)
});

Details:

After changing the delimiter, when the program completes the output file is:

"Hello";"Export";20;3.14

Note the default delimiter has been changed. The delimiter used in EXPORT statement must be used in corresponding IMPORT statement to read this line. Note that in this program it is assumed the unnamed output stream is redirected to some file.

Example 4:

export book.

Converted code:

UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField(new FieldReference(book, "bookId")),
   new ExportField(new FieldReference(book, "bookTitle")),
   new ExportField(new FieldReference(book, "publisher")),
   new ExportField(new FieldReference(book, "isbn")),
   new ExportField(new FieldReference(book, "onHandQty")),
   new ExportField(new FieldReference(book, "cost")),
   new ExportField(new FieldReference(book, "pubDate")),
   new ExportField(new FieldReference(book, "authorId")),
   new ExportField(new FieldReference(book, "soldQty")),
   new ExportField(new FieldReference(book, "price"))
});

Details:

Here, the EXPORT statement is used with the record option. It allows the output of the all the record's fields a short command. For each field, a FieldReference instance is passed as a parameter to the ExportField instance, allowing the export() call to get and output the current values for each field. The converted code always explicitly emits the fields used by the EXPORT statement, even if 4GL computes them internally.

Example 5:

export book except book.price book.isbn.

Converted code:

UnnamedStreams.safeOutput().export(new FieldEntry[]
{
   new ExportField(new FieldReference(book, "bookId")),
   new ExportField(new FieldReference(book, "bookTitle")),
   new ExportField(new FieldReference(book, "publisher")),
   new ExportField(new FieldReference(book, "onHandQty")),
   new ExportField(new FieldReference(book, "cost")),
   new ExportField(new FieldReference(book, "pubDate")),
   new ExportField(new FieldReference(book, "authorId")),
   new ExportField(new FieldReference(book, "soldQty"))
});

Details:

If we do not need all record to be exported, we have to provide the excluded fields explicitly using the EXCEPT option. This example excludes two fields: price and isbn. In the converted code, those fields are excluded from the FieldEntry array.

Example 6:

def var i as int init 1.
def var j as int init 2.
def var k as int init  3.
def var a as char format "x(10)" init "abc".
def var b as char format "x(10)" init "def".

define stream os.
output stream os to out.txt.

export stream os delimiter ":" i a j b k.

Converted code:

osStream.assign(StreamFactory.openFileStream("out.txt", true, false));
osStream.setDelimiter(":");
osStream.export(new FieldEntry[]
{
   new ExportField(i),
   new ExportField(a),
   new ExportField(j),
   new ExportField(b),
   new ExportField(k)
});

Details:

This is another example where the delimiter is set to a colon character and it outputs various variables to a named stream.

IMPORT Statement

The statement is intended to read the line of data created previously by the EXPORT statement or any other tool which produces compatible data. Each line in the file consists of formatted data separated by the delimiter char. By default the delimiter is one space character. In 4GL the IMPORT statement doesn't work with the terminal, it works only with streams - named or unnamed. In FWD, the unnamed stream instance should be obtained with UnnamedStreams.safeInput() API; this instance can then be used when data is read with IMPORT, as if it were a named stream.

The 4GL syntax of the statement is:

IMPORT [ STREAM stream ]
   {   [ DELIMITER character ] { field | ^ } ...
     | [ DELIMITER character ] record [ EXCEPT field ... ]
     | UNFORMATTED field
   }
   [ NO-ERROR ]

where each option represents:

4GL syntax Description Supported
STREAM stream Specifies the name of the stream. In case of not specified name unnamed stream is used. Yes
DELIMITER character The character to use as a delimiter between field values. The character must be a quoted single character. The default value is a space character. When explicitly set, the Stream.setDelimiter(String) call is emitted to set the delimiter. In case the given delimiter is null, it will default to space. Yes
field One or more expression to convert from the standard character format and to place the data into. Yes
^ Special symbol indicating skip over the next line. Converted to Stream.skipField() calls. Yes
record The name of the record buffer to import data from the file. Yes
[ EXCEPT field ] The name of the field to be excluded from importing process using the external data file when using the record option mentioned above. Yes
UNFORMATTED Turn on the read whole line mode instead of the single field mode that is used without this option. The line of data has been read with single call moving the file pointer to the next line. Yes
NO-ERROR Suppressing the system error handling allowing to check the error status and decide what to do next.
If this functionality is used, then the conversion rules will suppress all errors by bracketing the converted IMPORT statement with ErrorManager.silentErrorEnable() and ErrorManager.silentErrorDisable() calls. Once the line has been read, the developer can check for errors with ErrorManager.isError().
Yes

This statement is converted to either:

Stream.readField( BaseDataType var )

or

Stream.readLine( BaseDataType var )

where the difference is the amount of the data to be read. The readLine() is used to get the whole line in one call (when the UNFORMATTED option is used) while the readField(BaseDataType) method is reading the next single data entry between two delimiter characters. In the case of the single field reading if we do not reach the end of the line and want to move to the next line the Stream.resetCurrentLine() should be called explicitly to move to the next line. This is call is emitted by the conversion rules automatically, after the last field is read in the current IMPORT call is read.

As for the previously considered EXPORT statement the Stream stream parameter is optional and can be omitted. In this case we have to redirect input before using the IMPORT FROM statement.

If the stream has been opened in BINARY mode, the binary block will be read using the Stream.readBlock(memptr) calls emitted by the conversion rules.

Example 1:

def var chVar1 as char.
def var chVar2 as char.
def var intVar as integer.
def var decVar as decimal.

input from "redirected_training29.txt".
import chVar1 chvar2 intVar decVar.
input close.
display chVar1 chvar2 intVar decVar.

Converted code:

character chVar1 = UndoableFactory.character();
character chVar2 = UndoableFactory.character();
integer intVar = UndoableFactory.integer();
decimal decVar = UndoableFactory.decimal();
...
UnnamedStreams.assignIn(StreamFactory.openFileStream("redirected_training29.txt", false, false));
UnnamedStreams.safeInput().readField(chVar1);
UnnamedStreams.safeInput().readField(chVar2);
UnnamedStreams.safeInput().readField(intVar);
UnnamedStreams.safeInput().readField(decVar);
UnnamedStreams.safeInput().resetCurrentLine();
UnnamedStreams.closeIn();

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(chVar1, frame0.widgetChVar1()),
   new Element(chVar2, frame0.widgetChVar2()),
   new Element(intVar, frame0.widgetIntVar()),
   new Element(decVar, frame0.widgetDecVar())
};
frame0.display(elementList0);

Details:

The initial input file contains the following:

"Hello" "Import" 44 6.28

After application starts the screen is:

which demonstrates the data was read from the file correctly. Note how the the IMPORT statement was converted in this example. First the static call of the UnnamedStreams class is used here. And second - we are sequentially getting the next field of the line from first to last via readField() method. And third - we move the line counter explicitly to the next line by resetCurrentLine().

Example 2:

def var chVar1 as char format "x(40)".

define stream mystr.
input stream mystr from "redirected_training30.txt".
import stream mystr chVar1.
input stream mystr close.
display chVar1.

Converting to Java as:

mystrStream.assign(StreamFactory.openFileStream("redirected_training30.txt", false, false));
mystrStream.readField(chVar1);
mystrStream.resetCurrentLine();
mystrStream.closeIn();

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(chVar1, frame0.widgetChVar1())
};
frame0.display(elementList0);

Details:

If the input file contains the following data:

"Hello Import long string variable" 44 6.28

After application starts the screen is:

As the named stream is used to read data, the converted code of the IMPORT statement is a bit different here. An instance of the Stream class is used to access the file, and the readField(chVar1) is emitted for the read variable.

Example 3:

def var intVar1 as integer.
def var intVar2 as integer.
import delimiter ";" intVar1 intVar2.

Converted code:

UnnamedStreams.safeInput().setDelimiter(";");
UnnamedStreams.safeInput().readField(intVar1);
UnnamedStreams.safeInput().readField(intVar2);
UnnamedStreams.safeInput().resetCurrentLine();

Details:

the initial file to read is:

1234,5678

As the default delimiter style for input file is changed to a “;” character, the file is read and is assumed to contain only one field. After the program starts the screen is:

with the entire data being read in the first variable, as the delimiter for the EXPORT call was not the same as the delimiter for the IMPORT call.

Example 4:

def var intVar1 as integer.
def var intVar2 as integer.
import delimiter "," intVar1 intVar2.

Converted code:

UnnamedStreams.safeInput().setDelimiter(",");
UnnamedStreams.safeInput().readField(intVar1);
UnnamedStreams.safeInput().readField(intVar2);
UnnamedStreams.safeInput().resetCurrentLine();

Details:

If the delimiter is changed to a comma character (,), the resulted screen is:

with both variables being read correctly.

Example 5:

import book.

Converted code:

UnnamedStreams.safeInput().readField(new FieldReference(book, "bookId"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "bookTitle"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "publisher"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "isbn"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "onHandQty"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "cost"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "pubDate"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "authorId"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "soldQty"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "price"));
UnnamedStreams.safeInput().resetCurrentLine();

Details:

Reading the data for the entire book record from the file using the unnamed input stream results in explicit readField calls for all the buffer's fields. As with the IMPORT statement, the field's value is not retrieved at the readField call, instead the field evaluation is delayed - for this, a FieldReference instance is used to access all fields' values.

Example 6:

import book except cost publisher.

Converted code:

UnnamedStreams.safeInput().readField(new FieldReference(book, "bookId"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "bookTitle"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "isbn"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "onHandQty"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "pubDate"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "authorId"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "soldQty"));
UnnamedStreams.safeInput().readField(new FieldReference(book, "price"));
UnnamedStreams.safeInput().resetCurrentLine();

Details:

This example demonstrates the EXCEPT option, which excludes specified field from the importing process. As the cost and publisher fields have been excluded from the list of the read fields, the conversion rules will not emit readField calls for them.

Example 7:

def var chVar1 as char.
import unformatted chVar1.
display chVar1.

Converted code:

UnnamedStreams.safeInput().readLine(chVar1);

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(chVar1, frame0.widgetChVar1())
};

frame0.display(elementList0);

Details:

This example uses the UNFORMATTED option with the IMPORT statement. This tells the runtime to consider the each line inside the file as a single character variable and read it using a single call.

The input file to read will be:

1234 5678 This Is The String

After application starts the screen is:

Note the runtime, when reading the line, will not consider the look for delimiters or string quotations. Instead, the entire line is read. Also, note how the converted of this IMPORT statement is a bit different - a readLine() call is emitted, while the resetCurrentLine() call being skipped, as the line counter incrementing is embedded inside the readLine() call.

Example 8:

def var chVar1 as char.
import ^ chVar1.
display chVar1.

Converted code:

UnnamedStreams.safeInput().skipField();
UnnamedStreams.safeInput().readField(chVar1);
UnnamedStreams.safeInput().resetCurrentLine();

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(chVar1, frame0.widgetChVar1())
};

frame0.display(elementList0);

Details:

This example shows how the ^ option is used to skip over the next field in the line or even the whole line. Note the ^ option has been converted to the Stream.skipField() call. This moves the internal file pointer to the next data field within the current line. If the input file has the following content:

1234 5678 This Is The String

After application starts the screen is:

Note how the variable is assigned the second field from the line. Moreover, we can skip over the whole line if the IMPORT statement has the following syntax:

import ^.

which is converted to:

UnnamedStreams.safeInput().skipField();
UnnamedStreams.safeInput().resetCurrentLine();

This can be used for selective data reading.

Example 9:

def var intVar1 as integer.
input from "redirected_training37.txt".
import intVar1 no-error.
display intVar1.
message error-status:get-message(error-status:num-messages).

Converted code:

UnnamedStreams.assignIn(StreamFactory.openFileStream("redirected_training37.txt", false, false));
silent(() -> 
{
   UnnamedStreams.safeInput().readField(intVar1);
   UnnamedStreams.safeInput().resetCurrentLine();
});

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(intVar1, frame0.widgetIntVar1())
};
frame0.display(elementList0);

message(ErrorManager.getErrorText(ErrorManager.numErrors()));

Details:

The next important option to consider is the NO-ERROR one. This option is used if it is required some special error processing. When the system encounters a problem it displays the error message and interrupts the current procedure or transaction block. This can potentially destroy the normal execution flow especially if we heavily use the interactive user interface. To be able to take control over the error processing, the NO-ERROR option can be added to the IMPORT statement for the cases when it is expected to encounter import issues. If the input file for this test is:

12123231231323423423423423423

After application starts the screen is:

Note for the NO-ERROR option, as with other statements, the conversion rules will bracket the code with silent() call. When a read error is encountered, the execution is not interrupted. The read variable intVar will not receive the new value and the error status can be checked via ERROR-STATUS:GET-MESSAGE and ERROR-STATUS:NUM-MESSAGES system handle to choose what to do next, if an error was encountered. This improves the flexibility of the application development process.

Example 10:

def var whole_line as character.
def var id as character.
def var book_title as character format "x(100)".
def var isbn as character format "x(13)".
def stream si.

input stream si from lr_import.txt.

repeat:
  import stream si delimiter '\t' id book_title isbn.
  message "read book: id=" + id + "; isbn=" + isbn + "; title=" + book_title.
  readkey.
end.

input stream si close.

Converted code:

siStream.assign(StreamFactory.openFileStream("lr_import.txt", false, false));

repeat("loopLabel0", new Block((Body) () -> 
{
   siStream.setDelimiter("\t");
   siStream.readField(id);
   siStream.readField(bookTitle);
   siStream.readField(isbn);
   siStream.resetCurrentLine();
   message(concat("read book: id=", id, "; isbn=", isbn, "; title=", bookTitle));
   KeyReader.readKey();
}));

siStream.closeIn();

Details:

This code reads book entries from a file called import.txt, containing a unique ID, the book's title and the ISBN on each line, separated by tab characters. The program uses IMPORT to read the ID and the book title, ignoring the ISBN field. After each line is read, details about the book are printed in the message area.

Example 11:

def var whole_line as character.
def stream si.
import stream si unformatted whole_line no-error.
message "read line:" + whole_line.
readkey.

Converted code:

silent(() -> siStream.readLine(wholeLine));
message(concat(new character("read line:"), wholeLine));
KeyReader.readKey();

Details:

This examples reads an entire line, by supplying the UNFORMATTED clause to the IMPORT statement, while ignoring errors.

Example 12:

def var m as memptr.
input from a.txt binary.
import m.
input close.

Converted code:

memptr m = UndoableFactory.memptr();
...
UnnamedStreams.assignIn(StreamFactory.openFileStream("a.txt", false, false));
UnnamedStreams.safeInput().setBinary();
UnnamedStreams.safeInput().readBlock(m);
UnnamedStreams.safeInput().resetCurrentLine();
UnnamedStreams.closeIn();

Details:

When reading memory blocks from a binary file, the conversion rules will emit a readBlock() call, with the parameter set to the variable where the block will be read.

PUT Statement

This statement is used to send the output of one or several expressions to a device other than the terminal.

The 4GL syntax of the statement is:

PUT
   [ STREAM stream ]
   [ UNFORMATTED ]
   [     { expression
            [ FORMAT string ]
            [ { AT | TO } expression ]
         }
      | SKIP [ ( expression ) ]
      | SPACE [ ( expression ) ]
   ] ...

or

PUT [ STREAM stream ] CONTROL expression ...

where its options are:

4GL syntax Description Supported
STREAM stream Specifies the name of the stream. In case of not specified, the unnamed stream is used. Yes
UNFORMATTED The same meaning as the UNFORMATTED option of the EXPORT statement. Yes
expression Variable, constant, field name or expression to output. Yes
FORMAT string The format string is used to perform the output like the one used at variable definition. If this option is omitted the default value is used. Yes
{AT | TO} expression Position to output the values for either the output start or end. Yes
SKIP [ (expression) ] Inserts a new line or several new lines. If the amount of the new lines is not specified, or the amount is equal 0, a new line will be started only if the position is not already on a new line. Yes
SPACE [ (expression) ] The number of spaces to be inserted into the output. Yes
CONTROL expression This is a special mode, used when control characters are send to the non-terminal output. The expression can also be a NULL[ ( n ) ] entry, which outputs the NULL character (\0) 1 or n times. Yes

Depending on which options are used, the conversion rules will emit:

  • a Stream.putControl(com.goldencode.p2j.util.FieldEntry[]) call, when the CONTROL option is used.
  • a Stream.putUnformatted(com.goldencode.p2j.util.FieldEntry[]) call, when the UNFORMATTED option is used.
  • a Stream.putField(com.goldencode.p2j.util.FieldEntry[], String) is emitted when the data has an explicit format.
  • a Stream.putField(com.goldencode.p2j.util.FieldEntry[], boolean, NumberType) is a emitted when the TO or AT clauses are used. The second parameter is a logical value which will be set to true when the AT clause is used and to false when the TO clause is used. Column positions starts at 1.
  • a Stream.put(com.goldencode.p2j.util.FieldEntry[]) call, in all other cases.

Each entry of the FieldEntry[] array will be a PutField, SkipField, SpaceField or NullField instance, depending on whether the entry is an expression, the SKIP clause, the SPACE clause or a NULL field with the PUT CONTROL statement. The SkipField, SpaceField and NullField have c'tors which can receive as an optional parameter, the number of new lines, spaces or null characters to output; the PutField class is similar to the ExportField class used by the conversion rules for the EXPORT statement, except that it's c'tors can also receive as parameters the format to use when writing the data using the fmt parameter, a starts flag (set to true when the AT clause is used, and false when the TO is used) and the col parameter which sets column on which the alignment is done.

As noted above, the statement is used to perform output to the destination other than terminal screen. So it is not important whether the unnamed or named stream is used, but it is important to properly redirect the output to a non-terminal resource. If the STREAM option is not used and the unnamed stream is used for this case the unnamed output stream should be explicitly redirected to a non-terminal resource using the OUTPUT TO statement.

Example 1:

output to "redirected_training38.txt".
put "Hello from the put statement".
output close.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("redirected_training38.txt", true, false));
UnnamedStreams.safeOutput().put(new FieldEntry[]                                                 
{                                                                                                
   new PutField("Hello from the put statement")                                                  
});                                                                                              
UnnamedStreams.closeOut();                                                                       

Details:

This example shows how the PUT statement is used with the unnamed stream. See how the unnamed stream reference is obtained via the UnnamedStreams.safeOutput() call, and the written string is wrapped in a PutField instance.

After the program is ran, the file contains the following:

Hello from the put statement

Example 2:

def var chVar1 as char initial "Another".
def var chVar2 as char initial "Put statement".
def var intVar as integer initial 123.
def var decVar as decimal initial 4567890.

put unformatted chVar1 chVar2 intVar decVar "Character_Constant" intVar+decVar.
output close.

Converted code:

character chVar1 = UndoableFactory.character("Another");
character chVar2 = UndoableFactory.character("Put statement");
integer intVar = UndoableFactory.integer((long) 123);
decimal decVar = UndoableFactory.decimal(new decimal(4567890));
...
UnnamedStreams.safeOutput().putUnformatted(new FieldEntry[]
{
   new PutField(chVar1),
   new PutField(chVar2),
   new PutField(intVar),
   new PutField(decVar),
   new PutField("Character_Constant"),
   new PutField(() -> plus(intVar, decVar))
});
UnnamedStreams.closeOut();

Details:

This example outputs the data using the UNFORMATTED option with the PUT statement. For this case, Stream.putUnformatted() API is used here instead of Stream.put(). This outputs the data to a single line, without formatting it first and without any delimiters.

After the program is ran, the output file is:

AnotherPut statement1234567890Character_Constant4568013

Note there are neither delimiters nor quotes for character variables written.

Example 3:

def var chVar1 as char initial "Another".
def var chVar2 as char initial "Put statement".
def var intVar as integer initial 123.
def var decVar as decimal initial 4567.

put chVar1 6.28 chVar2 intVar decVar "Character_Constant" intVar+decVar.
output close.

Converted code:

character chVar1 = UndoableFactory.character("Another");
character chVar2 = UndoableFactory.character("Put statement");
integer intVar = UndoableFactory.integer((long) 123);
decimal decVar = UndoableFactory.decimal(new decimal(4567));

UnnamedStreams.safeOutput().put(new FieldEntry[]
{
   new PutField(chVar1),
   new PutField(decimal.fromLiteral("6.28")),
   new PutField(chVar2),
   new PutField(intVar),
   new PutField(decVar),
   new PutField("Character_Constant"),
   new PutField(() -> plus(intVar, decVar))
});
UnnamedStreams.closeOut();

Details:

Without the UNFORMATTED option, the output of the file is:

Another       6.28Put stat       123  4,567.00Character_Constant  4,690.00

Notice how the the output is formatted and when the value do not fit the default format, the value is truncated (see the output for chVar2, Put stat instead of Put Statement).

Example 4:

def var chVar1 as char initial "Very long string variable. Truncated if default format is used.".
put chVar1 skip chVar1 format "x(65)".

Converted code:

character chVar1 = UndoableFactory.character("Very long string variable. Truncated if default format is used.");
...
UnnamedStreams.safeOutput().put(new FieldEntry[]
{
   new PutField(chVar1),
   new SkipField(),
   new PutField(chVar1, "x(65)")
});

Details:

The default format for each field can be overridden using the FORMAT option for the field. In this case, the output will be:

Very lon
Very long string variable. Truncated if default format is used.

As the first field uses the chVar1 variable with the default format “x(8)”, the output is truncated. The second line uses the explicit format, and is able to output the entire content of the variable.

Example 5:

def var chEvar as character initial "2.718282".
...
put "1234567890123456789012345678901234567890" skip chEvar at 5.

Converted code:

character chEvar = new character("2.718282");
...
UnnamedStreams.safeOutput().put(new FieldEntry[]
{
   new PutField("1234567890123456789012345678901234567890"),
   new SkipField(),
   new PutField(chEvar, true, 5)
});

Details:

This example demonstrates how to output the data at a certain column, AT options. After running the program, the file content is:

1234567890123456789012345678901234567890
    2.718282

Note how the starting position of the character variable on the second line is 5 steps to the right from the beginning of the line. This is how the AT repositioning works - it alignes the left-edge of the data at the specified column. Also, note how the second parameter for the last PutField instance is set to true - this means the AT option is used; the third parameter is the column on which the alignment will be done.

Example 6:

def var chEvar as character initial "2.718282".
...
put "1234567890123456789012345678901234567890" skip chEvar to 15.

Converted code:

character chEvar = new character("2.718282");
...
   UnnamedStreams.safeOutput().put(new FieldEntry[]
   {
      new PutField("1234567890123456789012345678901234567890"),
      new SkipField(),
      new PutField(chEvar, false, 15)
   });

Details:

When using the TO clause with a field, the effect is similar, but the data is aligned on the right edge of the specified column, instead of left. Now after program completes the file content becomes:

1234567890123456789012345678901234567890
       2.718282

The idea of the TO option can be easily understood from the output above. We have to align the output so that the last symbol ends at the position specified in the TO option. Also, note how the last PutField() constructor has its second parameter set to false, which means that the TO option is used; the third parameter is the column on which the alignment will be performed.

Example 7:

put "This is the first line." skip(5) "And this is after skip 5 option.".

Converted code:

UnnamedStreams.safeOutput().put(new FieldEntry[]
{
   new PutField("This is the first line."),
   new SkipField(5),
   new PutField("And this is after skip 5 option.")
});

Details:

To insert new lines between two fields, the SKIP n clause is used with the PUT statement. The number passed to this option defines the number of the lines to be inserted. If the value 0 is used, it is the same as when the SKIP option without any parameter is used; also, the behavior is the same: a new line is started if the cursor is not already on the new line.

After the program is executed, the file content is:

This is the first line.

And this is after skip 5 option.

Note how we have skipped over the next 5 lines before the second field is written.

Example 8:

put "This is the first line." skip(0) skip "And this is after 2 calls of skip(0).".

Converted code:

UnnamedStreams.safeOutput().put(new FieldEntry[]
{
   new PutField("This is the first line."),
   new SkipField(0),
   new SkipField(),
   new PutField("And this is after 2 calls of skip(0).")
});

Details:

This example shows how SKIP(0) performs. The output in this case is:

This is the first line.
And this is after 2 calls of skip(0).

Note that the first SKIP(0) starts a new line, and the second SKIP does not advance the output to the next line, as as the pointer is on the first column on the current line.

Example 9:

put "1234567890123456789012345678901234567890" 
   skip "One" space "Two" space(0) "Three" space(1)
        "Four" space(2) "Five" space(3) "Six".

Converted code:

UnnamedStreams.safeOutput().put(new FieldEntry[]
{
   new PutField("1234567890123456789012345678901234567890"),
   new SkipField(),
   new PutField("One"),
   new SpaceField(),
   new PutField("Two"),
   new SpaceField(0),
   new PutField("Three"),
   new SpaceField(1),
   new PutField("Four"),
   new SpaceField(2),
   new PutField("Five"),
   new SpaceField(3),
   new PutField("Six")
});

Details:

This example shows how the the SPACE(number) clause is used. Without this option, one will need to manually separate the put fields with spaces. The output of this program is is:

1234567890123456789012345678901234567890
One TwoThree Four  Five   Six

Note how the various SPACE(n) clauses behave. SPACE without any number emits only one space, SPACE(0) emits no spaces, and SPACE(n) where n is greater than 0 inserts the respective number of space characters between data entries.

Example 10:

put control "~033E" NULL(5).

Converted code:

UnnamedStreams.safeOutput().putControl(new FieldEntry[]
{
   new PutField("\033E"),
   new NullField(5)
});

Details:

Another possible way to use the PUT statement is to send a control sequence to the device other than terminal. The CONTROL option should be used for this purpose, and in this case, the putControl API is used to emit the data.

This example sends the ESC E and 5 NULL characters. The output of this example will be the following:

This is the binary content of the file, and not its text content.

Example 11:

def var i as int init 1.
def var j as int init 2.
def var a as char format "x(10)" init "abc".
def var b as char format "x(10)" init "def".

define stream os.
output stream os to out.txt.

put stream os a at 3 format "x(4)" space i format ">>9" space b space j skip .
put stream os unformatted skip a space i space b at 20 space j to 30 skip.
put stream os control null.

Converted code:

integer i = UndoableFactory.integer((long) 1);
integer j = UndoableFactory.integer((long) 2);
character a = UndoableFactory.character("abc");
character b = UndoableFactory.character("def");

Stream osStream = new StreamWrapper("os");

...
osStream.assign(StreamFactory.openFileStream("out.txt", true, false));
osStream.put(new FieldEntry[]
{
   new PutField(a, "x(4)", true, 3),
   new SpaceField(),
   new PutField(i, ">>9"),
   new SpaceField(),
   new PutField(b, FMT_STR_1),
   new SpaceField(),
   new PutField(j),
   new SkipField()
});
osStream.putUnformatted(new FieldEntry[]
{
   new SkipField(),
   new PutField(a),
   new SpaceField(),
   new PutField(i),
   new SpaceField(),
   new PutField(b, true, 20),
   new SpaceField(),
   new PutField(j, false, 30),
   new SkipField()
});
osStream.putControl(new FieldEntry[]
{
   new NullField()
});

Details:

This example combines the formatted and unformatted modes of the PUT statement with a named stream. The content of the output file after running the program above will be:

  abc    1 def                 2
abc 1              def       2

Note the different formatting used for numeric types in the first line as a result of using the FORMAT clause and the effect of AT and TO options in the two lines.

Stream I/O Using Frames

The 4GL mechanism of showing frame data to the terminal or by reading input data from the terminal uses the the unnamed streams. FWD implements this differently - when the unnamed streams are not redirected, all input and output is done using the terminal. If the unnamed streams are redirected, the runtime implementation of the frame-related statements will notice this and will redirect the input or output command to the unnamed streams.

For the named streams cases, 4GL and FWD work the same - an explicit stream reference must be passed to the frame-related statement, and the runtime will send or receive all input/output using that stream.

Registering a frame for header and footer support with paged streams is also available in FWD. See the Paging Support sub-section in this section for details. After the paging support, this section describes the I/O statements which use frames are converted and handled by FWD.

PAGE-TOP / PAGE-BOTTOM Support

In Progress a frame can be registered as a page header/footer by using the PAGE-TOP and PAGE-BOTTOM clauses with a FORM statement. In FWD, this is done by emitting several API calls to mark and register the frame for paging support.

To link together the frame and the stream which will be used to write the page's header or footer, it is enough to execute a VIEW or DISPLAY statement with the header frame, either in a context where the unnamed output stream is redirected or by specifying a named stream reference, via the STREAM stream clause.

The conversion rules will emit code to mark the widgets part of a header/footer frame. When the PAGE-TOP or PAGE-BOTTOM clauses are used with the FORM statement, the following calls will be emitted in the frame's definition class::

  • CommonFrame.setPageTop(true), to mark the frame as a header, when the PAGE-TOP clause is used.
  • CommonFrame.setPageBottom(true), to mark the frame as a footer, when the PAGE-BOTTOM clause is used.

Using a frame configured as above, when the stream starts a new page it forces all headers in the active header list to render (in order of registration) and likewise, at page end all footers are rendered. Any partial page at stream close will be extended to the page end and the footers will be rendered there. PAGE-TOP and PAGE-BOTTOM frames that are not used on a paged output stream have no special effect (they are treated as normal frames).

DISPLAY and VIEW have a special behavior when output is redirected AND the output stream is paged AND the given frame is either PAGE-TOP or PAGE-BOTTOM. In this case, the given frame is registered as a header (PAGE-TOP) or footer (PAGE-BOTTOM) and this will be displayed at the top and bottom of each page respectively. The only difference is that DISPLAY will ALSO cause the frame to output immediately (as well as being "called back" for the header/footer) while VIEW only causes a registration but not an actual output.

Example:

def var hdr as char.
def var ftr as char.

form hdr with page-top frame f1 .
form ftr with page-bottom frame f2 .

def stream s.
output stream s to test.txt.

view stream s frame f1.
view stream s frame f2.

Converted code:

f1Frame.openScope();
f2Frame.openScope();
TransactionManager.registerTopLevelFinalizable(sStream, true);
sStream.assign(StreamFactory.openFileStream("test.txt", true, false));

f1Frame.view(sStream);

f2Frame.view(sStream);
...

and frame definition code:

public interface TestF1
extends CommonFrame
{
...
   public static class TestF1Def
   extends WidgetList
   {
      FillInWidget hdr = new FillInWidget();

      public void setup(CommonFrame frame)
      {
         frame.setDown(1);
         frame.setPageTop(true);
         ftr.setDataType("character");
         ftr.setLabel("hdr");
         ftr.setFormat("x(8)");
      }

      {
         addWidget("hdr", "hdr", hdr);
      }
   }
}

public interface TestF2
extends CommonFrame
{
...
   public static class TestF2Def
   extends WidgetList
   {
      FillInWidget ftr = new FillInWidget();

      public void setup(CommonFrame frame)
      {
         frame.setDown(1);
         frame.setPageBottom(true);
         ftr.setDataType("character");
         ftr.setLabel("ftr");
         ftr.setFormat("x(8)");
      }

      {
         addWidget("ftr", "ftr", ftr);
      }
   }
}

Details:

In the main program, the VIEW is used to register the frame with the stream's paging notifications. Also, in the class definition, the setPageTop and setPageBottom calls are emitted, to mark the frames, accordingly.

DISPLAY

DISPLAY is the most common statement used to write data to the stream. It writes the given list of fields/expressions to the stream using the format and layout of the frame. This is the most common way to generate a report in a file or for a printer. This can be a redirection of the unnamed stream or an explicit reference to a named stream, if DISPLAY STREAM stream is used.

DISPLAY functionality is provided by the display() APIs in the CommonFrame interface (and its implementation, GenericFrame). When using a named stream, the particular Stream instance needs to be passed as an argument:

  • CommonFrame.display(Stream)
  • CommonFrame.display(Stream, FrameElement[])

For the unnamed stream case, the FWD runtime will notice the redirection and will send the frame data to the stream instead of terminal.

The format of the stream output will contain explicit (SKIP) and implicit (based on a list of fields that exceeds the display's width) newline characters exactly as the terminal would display.

Example 1:

def var txt1 as character.
def var txt2 as character.
def var i    as integer.

do i = 1 to 6:
   txt1 = txt1 + "Hello World! ".
end.

txt2 = txt1.

display txt1 format "x(78)" txt2 format "x(78)".

Converted code:

frame0.openScope();

loopLabel0:
for (i.assign(1); _isLessThanOrEqual(i, 6); i.increment())
{
  txt1.assign(concat(txt1, "Hello World! "));
}

txt2.assign(txt1);

FrameElement[] elementList0 = new FrameElement[]
{
  new Element(txt1, frame0.widgetTxt1()),
  new Element(txt2, frame0.widgetTxt2())
};

frame0.display(elementList0);

Details:

The output of this example when using the terminal is:

Example 2:

output to test.txt.

def var txt1 as character.
def var txt2 as character.
def var i    as integer.

do i = 1 to 6:
   txt1 = txt1 + "Hello World! ".
end.

txt2 = txt1.

display txt1 format "x(78)" txt2 format "x(78)".

Converted code:

frame0.openScope();
UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));

loopLabel0:
for (i.assign(1); _isLessThanOrEqual(i, 6); i.increment())
{
  txt1.assign(concat(txt1, "Hello World! "));
}

txt2.assign(txt1);

FrameElement[] elementList0 = new FrameElement[]

  new Element(txt1, frame0.widgetTxt1()),
  new Element(txt2, frame0.widgetTxt2())
};

frame0.display(elementList0);

Details:

After running the program, the test.txt file contains:

txt1
txt2
------------------------------------------------------------------------------
Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!
Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!

It is important to note that the box normally drawn around a frame is not output to the stream, as it can be seen in the output of the two 4GL programs above examples above. Instead, there is an empty line where the top and bottom horizontal lines would be, and leading spaces where the left vertical line would be. No output is generated for the right vertical line. If the frame is defined as a NO-BOX, then none of those spaces or extra lines will be output.

Column headings do get output using hyphen characters (and a space in between each column) as the separators. NO-LABELS and SIDE-LABELS are honored just as they would be in the terminal.

It is also important to note that it still copies data to the screen buffer and it is the resulting screen buffer that is written to the stream.

DISPLAY generates a fully formatted output (very close to the result when viewed on a terminal) and numbers are right aligned, unlike SET/PROMPT-FOR/UPDATE which generate the field output in a "raw" form and aligns numbers to the left.

VIEW

The VIEW language statement does not copy any new data to the screen buffer, however it does write the contents of the referenced screen buffer to the named or unnamed stream. This is often used to output a static (unchanging) report header multiple times. Output is done using "frames" (screen buffers), with the following API's, when a named stream is used:

  • CommonFrame.view(Stream)
  • CommonFrame.view(Stream, FrameElement[])

Example:

def stream os.
output stream os to report.txt.

form header "--Report--" with centered with frame f1.
view stream os frame f1.

Converted code:

Shr1F1 f1Frame = GenericFrame.createFrame(Shr1F1.class, "f1");
Stream osStream = new StreamWrapper("os");
...
FrameElement[] elementList0 = new FrameElement[]
{
   new HeaderElement("--Report--", f1Frame.widgetExpr1())
};
f1Frame.registerHeader(elementList0);

osStream.assign(StreamFactory.openFileStream("report.txt", true, false));
f1Frame.view(osStream);

Details:

Like DISPLAY, VIEW also generates a fully formatted output (very close to the result when viewed on a terminal) and numbers are right aligned.

HIDE

The HIDE statement only has an effect in the case where output is redirected AND the output stream is paged AND the given frame is either PAGE-TOP or PAGE-BOTTOM. In this case, the frame is removed from the list of headers/footers.

Example:

def frame f1 with centered.
def frame f2 with centered.
def var var1 as char format "x(10)" init "v1".
def var var2 as char format "x(10)" init "v2".
def var var3 as char format "x(10)" init "init_val".
define stream ss.

output stream ss to outfile.txt paged page-size 20.

form var1 with no-labels with frame f1 page-bottom title "Bottom1".
view stream ss frame f1.

form var2 with no-labels with frame f2 page-bottom title "Bottom2".
view stream ss frame f2.

display stream ss var3.

message "press a key to page".
readkey.
page stream ss.

var3="new_value".
display stream ss var3.

hide stream ss frame f1.

Converted code:

f1Frame.view(ssStream);
f2Frame.view(ssStream);
...
message("press a key to page");
KeyReader.readKey();
ssStream.advancePage();
...
f0Frame.hide(ssStream, false);

and the frame definition:

public interface HideExampleF1 extends CommonFrame

  public character getVar1();
  public void setVar1(character parm);
  ...
  public static class HideExampleF1Def extends WidgetList
  {
    public void setup(CommonFrame frame)
    {
      ...
      frame.setPageBottom(true);
      frame.setTitle("Bottom1");
    }

Details:

Only frames which have been previously registered as page-top or page-bottom frames can be hidden using this functionality. Hiding happens as a result of calling CommonFrame.hide(Stream s, boolean noPause) for named streams or CommonFrame.hide(boolean noPause) for unnamed streams.

The content of the output file after running the program is:

var3
----------
init_val

                                        Bottom2

                                        Bottom1

var3
----------
new_value

                                        Bottom2

As it can be seen, at the end of the first page of the output two page footer frames are printed (f1 and f2), while in the second page, after the user is prompted to press a key, f1 is deregistered, leaving f2 as the only page-bottom frame (printed on stream close).

DOWN and UP

DOWN/UP can be used in Progress for positioning the cursor on a new line or on a line above. In FWD, DOWN and UP translate to up() and down() API's in CommonFrame.

A call like the following when working with a frame 'fr':

down stream str 2.

translates as:

frFrame.down(strStream, 2);

Similarly, a call to:

up stream str 2.

converts to:

frFrame.up(strStream, 2);

For unnamed streams, the explicit stream reference is omitted. Even if the API itself is simple, behavior of UP/DOWN in the context of streams is a little more complex.

The DOWN and UP statements work differently depending on the stream or terminal as the target. The following rules are applied, if the target device is a stream:

  • UP statement is a no-operation for streams; in a down frame the UP statement has no effect on the screen buffer. The DOWN statement will reset the screen buffer and advance to the next row.
  • for this very reason, no up/down pending counters are maintained for streams and those are reset to 0 with every DOWN.
  • conditional DOWN still sets the down pending flag, which is checked with VIEW, DISPLAY or unconditional DOWN.
  • the effect of the conditional DOWN, when it's applied, differs from a DOWN 1:
  • conditional DOWN flushes the current remote terminal's buffers to the stream and is a no-operation when the buffers are empty.
  • unconditional DOWN n flushes the current remote terminal's buffers to the stream and puts a newline on the stream when the buffers are empty, then adds n - 1 newlines
  • conditional DOWN is OK for 1 down frames
  • both forms of DOWN clear the current iteration of the frame and reset pending flag and counters
  • implicit conditional DOWN is no different
  • frames that were used with a stream at least once, are permanently flagged
  • the above mentioned flag is checked when the frame gets cleared with DOWN processor for the terminal as target:
  • If there is an attempt to go down past the FRAME-DOWN row in a down frame, then a new row will be added to the frame.
  • FRAME-LINE for streamed frames is always 0.
  • When writing, the UP statement has differing behavior. On the terminal, it seems to defeat the normal behavior of a DOWN frame. On the stream, it usually does nothing, however in some cases it can cause a form of overwriting.
  • When writing, DOWN seems to do a similar thing for both terminal and stream. This behavior extends to the fact that some usage of DOWN will overwrite the same data rather than generating multiple lines.
  • Although this is not shown above, when reading from a stream neither DOWN nor UP have any apparent effect. Most important, these language statements do not change the current read position in the stream.

The following examples show the effect of down frames with the DOWN and UP language statements:

Example 1:

define stream str.
output stream str to ./test.txt.

def var i as integer.

do i = 1 to 6 with frame fr:
   display stream str i with 12 down frame fr.
   up stream str 1.
end.

Converted code:

ToClause toClause0 = new ToClause(i, 1, 6);

doTo("loopLabel0", toClause0, new Block()

  public void init()
  {
    frFrame.openScope();
  }

  public void body()
  {
    FrameElement[] elementList0 = new FrameElement[]
    {
      new Element(i, frFrame.widgeti())
    };

    frFrame.display(strStream, elementList0);

    frFrame.up(strStream, 1);
  }
});

Details:

After running this program, the output file test.txt contains:

         i
----------
         1
         2
         3
         4
         5
         6

If the terminal had been used instead, then of course a series of screens would have been displayed on the terminal, with each screen alternating between a number (1-6) and a blank/empty output. 12 screens are displayed in all, each in turn. The first looks like:

Example 2:

define stream str.
output stream str to ./test.txt.

do i = 1 to 10:
  display stream str "Count: " i with 10 down.
  if i < 6
    then down stream str.
    else up stream str.
end.

Converted code:

for (i.assign(1); _isLessThanOrEqual(i, 10); i.increment())
{
  FrameElement[] elementList0 = new FrameElement[]
  {
    new Element("Count: ", frame0.widgetExpr1()),
    new Element(i, frame0.widgeti())
  };

  frame0.display(strStream, elementList0);
  if (_isLessThan(i, 6))
  {
    frame0.down(strStream);
  }
  else
  {
    frame0.up(strStream);
  }
}

Details:

When executed, this creates the following content inside file test.txt:

                 i
        ----------
Count:           1
Count:           2
Count:           3
Count:           4
Count:           5
Count:          10

Had the same logic been executed against the terminal, the output would've been different:

Note the effect of the UP statements on the terminal, which is missing in the streamed case.

Next we show some rules defining the behavior of conditional DOWN statements with a streamed, down frame (with or without box) (NL = NewLine):

  1. If the current line is at the end of a page, then: a DOWN 1 statement will write a NL to the next page; a DOWN 2 statement will write a NL to the current page and a NL to the next page.
  2. If the current line is at position X having Y more rows in the current page, then a DOWN Y statement will write Y NL's to the current page and one more NL to the next page.
  3. If the current line is at position X having Y more rows in the current page, then a DOWN (Y+Z) statement will write Y+1 NL's to the current page and one more NL to the next page.

Example 3:

output stream str to test2.txt page-size 20.

define variable  p as character format "x(8)"  label "P" no-undo.
define variable  i as integer no-undo.
form p with frame f0 down no-box.
do i = 1 to 65:
   p = string(i).
   display stream str p with frame f0.
   down stream str with frame f0.
   if i = 17
   then do:
      display stream str "+++++++" @ p with frame f0.
      down 1 stream str with frame f0.
   end.
   if i = 29
   then do:
      display stream str "@@@@@@@" @ p with frame f0.
      down 6 stream str with frame f0.
   end.
end.

Converted code:

character p = new character("");
integer i = new integer(0);
...
for (i.assign(1); _isLessThanOrEqual(i, 65); i.increment())
{
   p.assign(valueOf(i));

   FrameElement[] elementList0 = new FrameElement[]
   {
      new Element(p, f0Frame.widgetp())
   };

   f0Frame.display(str2Stream, elementList0);

   f0Frame.down(str2Stream);
   if (_isEqual(i, 17))
   {
      FrameElement[] elementList1 = new FrameElement[]
      {
         new Element("+++++++", "x(7)", f0Frame.widgetp())
      };

      f0Frame.display(str2Stream, elementList1);
      f0Frame.down(str2Stream, 1);
   }
   if (_isEqual(i, 31))
   {
      FrameElement[] elementList2 = new FrameElement[]
      {
         new Element("@@@@@@@", "x(7)", f0Frame.widgetp())
      };

      f0Frame.display(str2Stream, elementList2);
      f0Frame.down(str2Stream, 8);
    }
  }
}

Details:

After this is executed, file test2.txt looks like the following:


--------

[...]
16
17
+++++++

--------

18
19
[...]
31
@@@@@@@

--------

32
33
[...]
40

As can be noticed, the DOWN 1 statement, after line 17, causes the page to get filled and a NL to be output to the next page. The DOWN 8 statement after line 31 lands outside the current redirected paged frame. FWD will write 3 NL's in current page in order to complete it, but on the next page only one NL is printed, regardless of the remaining DOWN iterations (5 in this case).

Limitation in computing the LINE-COUNTER and PAGE-NUMBER functions on DOWN:

After a DOWN, if the page is ended, FWD erroneously updates the line counter to 1 and also increments the page number. Progress seems to do this when the subsequent statement (DOWN/DISPLAY/etc) which affects the stream buffer is executed. This is not implemented yet.

UNDERLINE

In 4GL UNDERLINE is used for underlining fields or variables. Since the UI is character based, the actual underlining is done by writing a line right below the field to be underlined. In FWD, underline functionality is provided by the following API's:

  • CommonFrame.underline([ Stream, ] GenericWidget[]): if several widgets are to be underlined, in a certain frame.
  • GenericWidget.underline([ Stream ]): for underlining a specific widget.

For unnamed streams, the CommonFrame.underline API will not receive an explicit stream reference; the FWD runtime will notice the unnamed stream redirection and send the data to the stream, instead of the terminal.

Example 1:

def var i as integer.
def var j as integer.
do i = 432423 to 432433 with frame f2:
  j=i modulo 4.
  display stream str i j with side-labels frame f2.
  if j = 0
    then do:
      underline stream str i.
     end.
end.

Converted code:

integer i = new integer(0);
integer j = new integer(0);

...

ToClause toClause0 = new ToClause(i, 432423, 432433);

doTo("loopLabel0", toClause0, new Block()
{
  ...
  public void body()
  {
    j.assign(modulo(i, 4));
    FrameElement[] elementList0 = new FrameElement[]
    {
      new Element(i, f2Frame.widgeti()),
      new Element(j, f2Frame.widgetj())
    };

    f2Frame.display(strStream, elementList0);
    if (_isEqual(j, 0))
    {
      f2Frame.widgeti().underline(strStream);
    }
  }
});

Details:

As a single widget is used with the UNDERLINE statement, the conversion code will emit an explicit API call to underline the targeted widget.

Example 2:

...
underline stream str i j.
...

Converted code:

...
f2Frame.underline(strStream, new GenericWidget[]
{
   f2Frame.widgeti(),
   f2Frame.widgetj()
});
...

Details:

This example modifies the previous one and underlines two widgets. In this case, as more than one widget is used, the API in CommonFrame - the one that receives an array of widgets - needs to be called instead of the one in GenericWidget.

Reading

If the statements PROMPT-FOR, SET or UPDATE are used to read the data from a file, the FORMAT for the data is ignored. Therefore, if the application is dependent on the FORMAT option to validate the input, the source data will not be validated against the format and the invalid character will be read. If end of the file is reached, the system generates the ENDKEY event. The single period encountered in the input file generates the END-ERROR event to happen. To read the period as character it must be enclosed in quotes(“.”). Also, there are special characters to use in an input file. The first one is tilde(~) or slash(\) on UNIX. This symbol encountered at the end of the line means the next line will be considered as concatenation of the previous one. There must be no space char after tilde before the next line. For example:

Hello~
Outside

will be read as single line in a char variable, resulting in the HelloOutside value. The second special character is hyphen. When using a hyphen(-), it causes the variable setting statement go past the field, but it will not update the appropriate variable. The input-memory conversion is based on the currently installed Java LANG system variable and locale packages installed in JRE under which the converted application is executing.

UPDATE

UPDATE functionality when reading from named or unnamed streams in FWD is provided by CommonFrame.update([ Stream, ]FrameElement[]) APIs. When a named stream is used, this API receives the explicit stream reference as the first parameter.

Example:

def var i as integer init 1.
define stream si.
input stream si from infile.txt.
update stream si i.
input stream si close.

Converted code:

siStream.assign(StreamFactory.openFileStream("infile.txt", false, false));
FrameElement[] elementList0 = new FrameElement[]

  new Element(i, frame0.widgeti())
};
frame0.update(siStream, elementList0);
siStream.closeIn();

Details:

When a stream is used instead of the terminal, the output of UPDATE is not fully formatted and numbers are left aligned.

SET

Similarly, SET corresponds to CommonFrame.set([ Stream, ]FrameElement[]) API. Substituting UPDATE with SET in the previous example :

set stream si i.

then in the converted code we have:

frame0.set(siStream, elementList0);

When a stream is used instead of the terminal, the output of SET is not fully formatted and numbers are left aligned.

PROMPT-FOR

Again, this converts very much like UPDATE and SET. Using PROMPT-FOR in the previous examples instead of UPDATE or SET:

prompt-for stream si i.

results in this converted code:

frame0.promptFor(siStream, elementList0);

When a stream is used instead of the terminal, the output of PROMPT-FOR is not fully formatted and numbers are left aligned.

Paging Support

4GL output streams (both named or unnamed) support paging. This is done by specifying the PAGED and PAGE-SIZE options with the OUTPUT TO, OUTPUT THROUGH or INPUT-OUTPUT THROUGH statement. For paged streams, it is possible to access details as page number, page size and others details, as presented in the next sections.

FWD has a limitation in computing the LINE-COUNTER and PAGE-NUMBER functions: after a DOWN, if the page is ended, FWD erroneously updates the line counter to 1 and also increments the page number. Progress seems to do this when the subsequent statement (down/display/etc) which affects the stream buffer is executed. This is not implemented yet.

PAGED Option

4GL syntax Description Applies To
PAGED Breaks the output with the page separator inserting the CTRL-L symbol at the end of the each page. By default, the page size is 56 lines. If output is sending to the printer the paging is performed automatically. OUTPUT TO, OUTPUT THROUGH

Example 1:

output to "test.txt" paged.
do i = 1 to 100:
   display chvar no-label.
   down(1).
end.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false, true));

loopLabel0:
for (i.assign(1); _isLessThanOrEqual(i, 100); i.increment())
{
   FrameElement[] elementList0 = new FrameElement[]
   {
      new Element(chvar, frame0.widgetChvar())
   };

   frame0.display(elementList0);

   frame0.down(1);
}

Details:

Here, the system will insert the special CTRL-L symbol into output file every 65 line of the text. If we do not specify the PAGED option the output will be as single piece of the data without any page separators.

PAGE-SIZE Option

4GL syntax Description The statements applied to
PAGE-SIZE Redefines the number of the lines per page. The default value is 56 for file or printer outputs. If the output device is the TERMINAL the default value is number of lines that can be fit into the TEXT widget in the window. If the value of the PAGE-SIZE is equal to 0 the output will not be paged. OUTPUT TO, OUTPUT THROUGH

Example 2:

output to "test.txt" paged page-size 20.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false, 20, true));

Details:

Adding a page size of 20 will emit the page separator every 20 lines. If we set the PAGE-SIZE to 0 the paging will not be performed, and is the same as if we do not specify the PAGED option.

PAGE-NUMBER Function

This function returns the current page number of a paged output stream (named/unnamed). If the stream is not paged, the page number returned is zero. In FWD it is implemented by the Stream.getPageNum() API. Its syntax is:

PAGE-NUMBER [ ( name ) ]

where name is a named stream. If missing, it targets the unnamed output stream.

There is a limitation in FWD which results in returning the next page number just after the page has ended, and before any output is emitted to the next page. After a DOWN, if the page is ended, FWD erroneously increments the page number. Progress seems to do this when the subsequent statement (down/display/etc) which affects the stream buffer is executed. This is not implemented yet.

Example 3:

def var n1 as decimal init ?.
def var n2 as decimal init ?.
define stream ss.

output to out1.txt paged page-size 10.
output stream ss to out2.txt paged page-size 20.

n1 = page-number.
n2 = page-number(ss).

Converted code:

[...]
n1.assign(UnnamedStreams.safeOutput().getPageNum());
n2.assign(ssStream.getPageNum());

Details:

Note how for unnamed output stream the UnnamedStreams.safeOutput() is used to obtain a reference to the currently opened unnamed stream. For named streams, a direct reference is implicitly available.

PAGE-SIZE Function

Gives the number of lines per page in an output stream (named/unnamed). If the stream is not paged, a page size of zero is returned. In FWD it is implemented by the Stream.getPageSize() API. Its syntax is:

PAGE-SIZE [ ( name ) ]

where name is a named stream. If missing, it targets the unnamed output stream.

Example 4:

n1 = page-size.
n2 = page-size(ss).

Converted code:

n1.assign(UnnamedStreams.safeOutput().getPageSize());
n2.assign(ssStream.getPageSize());

Details:

Continuing from example №3, getting the page-size for the named and unnamed streams results in emitting a getPageSize() call for the named and unnamed stream reference.

PAGE Statement

The PAGE statement advances to the next page in a paged output destination, in our case page output streams. This is done by emitting a CTRL+L character to the output. In FWD it is implemented by the Stream.advancePage() API. Its syntax is:

PAGE [ STREAM stream ]

If the stream is not specified, the statement will work with the unnamed stream; else, it will work with the specified named stream.

Example 5:

page.
page stream ss.

Converted code:

UnnamedStreams.safeOutput().advancePage();
Stream.advancePage();

Details:

Similar to the PAGE-SIZE function, this statement needs a stream reference to work with. For the unnamed streams, the conversion rules will automatically emit the UnnamedStreams.safeOutput() call, which returns a reference to the unnamed output stream.

LINE-COUNTER Function

This function returns the line number within a page in a paged output destination/stream, starting from 1. It continues to increase as lines are being printed until the page is filled; when the first row has been printed on the new page, the line counter is reset to 1. If the output is not paged LINE-COUNTER returns 0. Its syntax is:

LINE-COUNTER [ ( name ) ]

where name is a named stream. If missing, it targets the unnamed output stream.

There is a limitation in FWD which results in returning line number 1 just after the page has ended, and before any output is emitted to the next page. After a DOWN, if the page is ended, FWD erroneously updates the line counter to 1. Progress seems to do this when the subsequent statement (down/display/etc) which affects the stream buffer is executed. This is not implemented yet.

In FWD LINE-COUNTER is implemented as the Stream.getNextLineNum() API.

Example 6:

def var i as decimal init 0.
def var j as decimal init 0.
def stream so.

output stream so to outfile.txt paged.
output to outfile_default.txt paged.

i=line-counter(so).
j=line-counter.

Converted code:

i.assign(soStream.getNextLineNum());
j.assign(UnnamedStreams.safeOutput().getNextLineNum());

Details:

Checking the current line number in a stream requires FWD access to the targeted stream reference. For the unnamed streams, the UnnamedStreams.safeOutput() is called to obtain a reference to the currently opened unnamed output stream.

SEEK Function

The SEEK function returns the offset in an output stream, either named or unnamed. The stream must be a file stream not a process or terminal stream. In FWD this is implemented as the getPosition() API in Stream class. Its syntax is:

SEEK( { INPUT | OUTPUT | name } )

where INPUT represents the unnamed input stream, OUTPUT the unnamed output stream and name represents a named stream.

Example 7:

def stream si.
def stream so.

def var i as decimal init 0.
def var j as decimal init 0.
def var k as decimal init 0.
def var ll as decimal init 0.

output stream so to outfile.txt paged.
input stream si from infile.txt.
output to outfile_default.txt paged.
input from infile_default.txt.

i=seek(INPUT).
j=seek(OUTPUT).
k=seek(si).
ll=seek(so).

Converted code:

i.assign(UnnamedStreams.safeInput().getPosition());
j.assign(UnnamedStreams.safeOutput().getPosition());
k.assign(siStream.getPosition());
ll.assign(soStream.getPosition());

Details:

Same as with the previous functions and statements which work with unnamed and names streams, the conversion rules will explicitly attach the converted getPosition() code to the required stream reference, be it the named stream field name or the UnnamedStreams.safeInput() call.

SEEK Statement

SEEK can be used to control positioning in a text file opened by an input or output stream, either named or unnamed. The position can be a particular integer value or END for the end of file. Its syntax is:

SEEK { INPUT | OUTPUT | STREAM name } TO { position | END }

where:

  • INPUT represents the unnamed input stream.
  • OUTPUT represents the unnamed output stream.
  • STREAM name represents a named stream.
  • position is an integer value or an expression which evaluates to an integer and represents the targeted position where the position in the file needs to be set to. When used, the conversion rules will emit a setPosition(<position>) call for the targeted stream.
  • END means to set the position at the end of file. When used, the conversion rules will emit a setAppend() call for the targeted stream.

Example 8:

seek input to i.
seek input to end.
seek stream si to i.
seek stream si to end.

seek output to i.
seek output to end.
seek stream so to i.
seek stream so to

Converted code:

UnnamedStreams.safeInput().setPosition(i);
UnnamedStreams.safeInput().setAppend();
siStream.setPosition(i);
siStream.setAppend();

UnnamedStreams.safeOutput().setPosition(i);
UnnamedStreams.safeOutput().setAppend();
soStream.setPosition(i);
soStream.setAppend();

Details:

The FWD runtime, to execute this method, requires access to the targeted stream reference. For unnamed streams, the UnnamedStreams.safeInput() and UnnamedStreams.safeOutput() APIs are used to get this reference. For named streams, the name of the field referencing the stream is enough.

Other I/O Statements

READKEY

In FWD, READKEY functionality is provided by the KeyReader.readKey() API calls. These read a character from a stream or from the terminal and store it as the lastkey, which can be accessed via KeyReader.lastKey(). The following related methods are available:

  • readKey(): reads a character from the unnamed input stream - if redirected - or from the terminal, blocking until input is received.
  • readKey(NumberType): reads a character from the unnamed input stream - if redirected - or from the terminal, blocking until input is received or the specified number of seconds have passed.
  • readKey(Stream): reads a character from the specified input stream, blocking until input is received.
  • readKey(Stream, NumberType): reads a character from the specified input stream, blocking until input is received or the specified number of seconds have passed.

The syntax of this statement is:

READKEY [ STREAM stream ] [ PAUSE n ]

where:

  • STREAM stream is reflected in the Stream parameter passed to the readKey() functions above. Specifies the name of the stream to read from. If the name of the stream is not specified the unnamed stream is used.
  • PAUSE n is reflected in the NumberType parameter passed to the readKey() functions above. Specifies the number of seconds to wait for incoming key value. If there is no keystroke within the specified time, the READKEY returns with setting the

LASTKEY variable to -1.

The Progress READKEY is double-byte enabled. In Java, when READKEY is reading from the terminal, double byte character sets (DBCS) are not supported. READKEY will translate any input from the external character encoding to the Unicode representation according to the current system locale settings, but at this time the implementation does not support DBCS.

READKEY from a file or child process is likely to work for DBCS, so long as the default JVM locale is properly set to that DBCS encoding scheme. With the terminal things are different, because of the NCURSES library that is used, which at the moment cannot handle DBCS.

Example 1:

define stream si.
input stream si from infile.txt.
readkey stream si pause 2.
input stream si close.

Converted code:

Stream siStream = new StreamWrapper("si");

[...]

siStream.assign(StreamFactory.openFileStream("infile.txt", false, false));
KeyReader.readKey(siStream, 2);
siStream.closeIn();

Details:

When reading from a stream, the stream reference needs to be provided to readkey() as shown in the converted code.

Example 2:

define stream instr.
input stream instr from "test.txt".
readkey stream instr.
message "The lastkey character value is:" chr(lastkey).

Converted code:

Stream instrStream = new StreamWrapper("instr");
...
TransactionManager.registerTopLevelFinalizable(instrStream, true);
instrStream.assign(StreamFactory.openFileStream("test.txt", false, false));
KeyReader.readKey(instrStream);
message(new Object[]
{
  "The lastkey character value is:", chr(lastKey())
});

Details:

This code reads the key from an external file stream and displays the character value. If the content of the test.txt file is:

ab c

After application starts the screen is:

Note that only one character has been read in this case.

Example 3:

define stream instr.
input stream instr from "test.txt".
readkey stream instr pause 10.
message "The lastkey value is:" lastkey.
message "The character value is:" chr(lastkey).

Converted code:

...
KeyReader.readKey(instrStream, 10);
message(new Object[]
{
   "The lastkey value is:", lastKey()
});
message(new Object[]
{
   "The character value is:", chr(lastKey())
});
...

Details:

In this example the option PAUSE is used and the input file is empty. After application starts, wait for a 10 seconds, and the screen becomes:

See how the LASTKEY value becomes -1 in this case.

MESSAGE

In Progress, the MESSAGE statement can be used to display information in the special message area at the bottom of the window or in an alert box.

The MESSAGE statement does not have a STREAM option, it always sends messages to the current unnamed output destination, which by default is the terminal. MESSAGE statements output to the unnamed output stream, but in the case where input is NOT redirected (=terminal) and output IS redirected, they display on both the terminal and the redirected output stream. If input IS redirected (and output IS redirected), then there will be no interactive terminal output, only output to the redirected output stream. If messages must not be sent to the current output destination, the output can be made redirected to a named stream, since Progress doesn't write messages to a named stream.

Note that a MESSAGE SET/UPDATE using a redirected input stream operates just like a SET/UPDATE from a redirected input stream, in other words it is a stream reading statement. Full details about the MESSAGE statement and how it works can be found in the Message Area section of the Terminal Management chapter of this book.

Example 1:

output to message.txt.
message "press any key to continue".
readkey.

Converted code:

UnnamedStreams.assignOut(StreamFactory.openFileStream("message.txt", true, false));
message("press any key to continue");
KeyReader.readKey();

Details:

After the application runs it creates the file with the following content:

press any key to continue

As the unnamed output was redirected, the message was written to the file.

Example 2:

input thru "echo 5".
output to test.txt.
PROMPT-FOR x.
MESSAGE INPUT x.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo 5" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);
UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(x, frame0.widgetx())
};

frame0.promptFor(elementList0);

message(frame0.getX());

Details:

When using the INPUT option and the unnamed stream is redirected, there is no difference - the output is still written to the file.

Example 3:

def var chvar as character initial "Hello".
...
output to "test.txt".
message "message set" set chvar.

Converted code:

character chvar = new character("Hello");
...
UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));
message("message set", true, new AccessorWrapper(chvar));

Details:

This example waits for the variable to be set and send output to the file. Note only the message portion of the statement is in the resulting file. As the unnamed input stream is not redirected, after application starts the screen is:

Type any word in the field and press enter. The file test.txt will have the following content:

message set

Note the value of the field is not written to the file.

Example 4:

input thru "echo 5".
output to "test.txt".
message "Update i:" update i.
message "Set i:" set i.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo 5" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);
UnnamedStreams.assignOut(StreamFactory.openFileStream("test.txt", true, false));
message("Update i:", false, new AccessorWrapper(i));
message("Set i:", true, new AccessorWrapper(i));

Details:

When using the UPDATE and SET options with the MESSAGE statement and unnamed output is redirected to a file, only the character part of the message statement will be in the output file. The read value, even if it is read from the redirected unnamed stream, is not sent to the file.

Example 5:

def var i as int.

input thru "echo 5".
output to ./test.txt.

prompt-for i.

output close.
message input i.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo 5" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);
UnnamedStreams.assignOut(StreamFactory.openFileStream("./test.txt", true, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(i, frame0.widgeti())
};

frame0.promptFor(elementList0);

UnnamedStreams.closeOut();
message(frame0.getI());

Details:

test.txt contains:

         i
----------

The message line on the terminal contains:

5

Here, the PROMPT-FOR will output the frame to the unnamed output stream while reading the data from the redirected unnamed input stream. Thus, the value returned by the process (5) will appear both in the targeted file and the terminal.

Example 6:

def var i as int.

input thru "echo 5".
output to ./test.txt.

prompt-for i.

message input i.
output close.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo 5" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);
UnnamedStreams.assignOut(StreamFactory.openFileStream("./test.txt", true, false));

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(i, frame0.widgeti())
};

frame0.promptFor(elementList0);

message(frame0.getI());
UnnamedStreams.closeOut();

Details:

test.txt contains:

         i
----------

The message line on the terminal contains nothing, as the unnamed output stream is still redirected to the file, when the MESSAGE statement is executed. So, the file will contain output from both the frame and the message.

Example 7:

def var i as int.

input thru "echo 5".

prompt-for i.

message input i.

Converted code:

UnnamedStreams.assignIn(StreamFactory.openProcessStream());
ProcessOps.launch(new String[]
{
   "echo 5" 
}, (RemoteStream) UnnamedStreams.safeInput(), (RemoteStream) null);

FrameElement[] elementList0 = new FrameElement[]
{
   new Element(i, frame0.widgeti())
};

frame0.promptFor(elementList0);

message(frame0.getI());

Details:

The terminal displays:

The message line on the terminal contains:

As the unnamed output stream is not redirected, the program reads the input from the process stdout (as the unnamed input stream is redirected). Both statements that generate output (PROMPT-FOR and MESSAGE in the examples above) will send their data to the terminal, as the unnamed output stream is not redirected.


© 2004-2017 Golden Code Development Corporation. ALL RIGHTS RESERVED.