Project

General

Profile

Integrating Hand-Written Java

Server-Side Code

There are 2 kinds of hand-written server-side code: hosted services and integrated logic.

A hosted service is hand-written code that runs in the FWD server, but which is primarily invoked by an external application via some exposed API (application programming interface) in the FWD server. Such a hosted service is not called by the converted application code. Instead, methods (defined by a Java interface) are “exported” as APIs using the FWD server's remote object protocol. These APIs are called by code residing in another process on the same or a different physical system. Please see the chapter on Integrating External Applications for details on how to export and access such hosted services.

In some circumstances that hosted service may call converted code or it may shift into a mode where hand-written code operates using the same facilities and conventions as converted code. See the section below on Progress-Compatible Top-Level Session Wrapper which is the tool that can be used to handle the integration from a hosted service to converted code.

The other kind of hand-written code, integrated logic is code that is used to extend or replace portions of a converted application. Such code is not invoked (directly) by external applications, but rather it is integrated into the FWD compatibility environment and the code is part of the converted application. Since it is part of the converted application, it is always run within the context of the replacement transaction environment provided for Progress 4GL compatibility. This means that the hand-coded logic must either fully implement and abide by the conventions and rules of the converted code (known as implementing conversion-conforming code) OR it must limit its processing to facilities that do not depend upon the transaction environment and which also do not break that environment.

Data Types

Converted code uses replacement data types that are designed for Progress 4GL compatibility. Details on the types that are available and the conventions for their use can be found in Part 4 of the FWD Conversion Reference.

When writing Java code that must integrate with converted code, one approach is to use the same FWD data types as the converted code. For example, the hand-written code might use com.goldencode.p2j.util.integer instead of java.lang.Integer or instead of the primitive int.

Another approach is to minimize any FWD data type usage. This can be done by transforming any FWD types into Java types at the earliest possible moment after such types have been obtained OR by transforming Java types back into FWD types at the last possible moment before returning or submitting such data to conversion-conforming code.

Both approaches can be used by integrated logic. Hosted services may be able to avoid using the FWD types completely, so long as no converted code or converted-conforming code is called.

As soon as the FWD data types are used, it is possible that the features in use may require that the calling code be conversion-conforming. This may include running inside of the proper transaction environment. See Progress-Compatible Top-Level Session Wrapper for more details.

Avoiding Client Features

In FWD each user or batch process that is executing converted code is running a FWD client process. That process connects to the FWD server and invokes the converted application's main entry point. That code is the application's business logic and it runs inside the FWD server. That code may access the database and otherwise execute on the server. Some features of the Progress 4GL are client-specific. In other words, their implementation is dependent upon features that only exist in the FWD client. All visual or interactive portions of the user interface are implemented in the FWD client. Access to the file system (reading/writing files, searching paths , inspecting file meta-data and so on) is a client feature. Process launching (including shell commands and any stream input/output to/from child processes) is also a client feature. In such cases, the FWD server will “call back” to the FWD client to invoke the processing of the client feature being requested. Without the FWD client, such processing will fail.

Hosted services have no FWD client attached since the code is being invoked by custom logic in an external application. For example, hosted services may be invoked via a J2EE application server's remote connection. There is no FWD client in that J2EE server that is “calling into” the FWD server for some service. This means that the wrapped code must avoid all method calls or resource usage that invokes any FWD client functionality.

The following are the most common classes must be avoided (directly or indirectly):

  • com.goldencode.p2j.ui.LogicalTerminal
  • com.goldencode.p2j.ui.GenericFrame
  • com.goldencode.p2j.util.ErrorManager (see below for an exception)
  • com.goldencode.p2j.util.FileSystemOps
  • com.goldencode.p2j.util.KeyReader
  • com.goldencode.p2j.util.ProcessOps
  • com.goldencode.p2j.util.RemoteStream
  • com.goldencode.p2j.util.StreamFactory
  • com.goldencode.p2j.util.UnnamedStream

Any call to a method that attempts to access the FWD client will cause an abnormal end (an exception) since no FWD client exists to service the request.

The ErrorManager may be used only if “headless” mode is enabled in the TransactionManager. See Headless Operation below for more details.

Note that 4GL-compatible code can be executed using the FWD runtime only when it is ensured that the session which executes this code will be used to execute only one remote API, at a given time. This is because the FWD runtime is built to allow each session to have its own context-local data, but this context-local data is not thread-safe; so, a FWD session can not be used safely to concurrently execute remote APIs which rely on the FWD runtime. See the Accessing Services section in the Integrating External Applications chapter of this book, for details why this limitation exists.

Headless Operation

Virtually all server-side code that is a hosted service must be run in headless mode.

In headless mode, certain FWD runtime code will silently bypass calls to functionality that is normally hosted on the FWD client (see the section above on Avoiding Client Features). Hosted code can force headless mode to be enabled using com.goldencode.p2j.util.TransactionManager.setHeadless(true). Once done, this setting is active for the rest of the session (until the client disconnects) or until the setHeadless() method is called with a different state. The state can be changed at any time.

Not all of the client features can be avoided using headless mode. The primary user of client features that does honor headless mode is the com.goldencode.p2j.util.ErrorManager. The ErrorManager will be safe to use directly or indirectly once headless mode is enabled. Since virtually all portions of the FWD compatibility environment (including the basic data types) can generate errors, the ErrorManager can be indirectly invoked in a wide variety of situations. For this reason, it is very important to run all hosted services in headless mode.

Exception Handling

Conversion-conforming code runs inside of a highly managed transaction environment provided by the FWD runtime. The FWD runtime has been carefully designed to duplicate the Progress 4GL control flow, transaction behavior and features such as UNDO, block/loop retry, infinite loop protection and automatic database commits. Much of those (and other) features of the Progress 4GL are dependent upon the block structure of the code and the properties and configuration that is associated with those blocks.

The implementation of these features is hidden as much as possible inside the FWD runtime. This minimizes the complexity of the converted code, centralizes the logic into well designed, well tested locations and makes the result much more consistent and clean. Much of the code is found in com.goldencode.p2j.util.TransactionManager and in com.goldencode.p2j.util.BlockManager. Each 4GL top-level block (external procedure, internal procedure, user-defined function or trigger) and 4GL inner block (DO, REPEAT, FOR or EDITING) is implemented as an anonymous inner class of type com.goldencode.p2j.util.Block. This class contains the code that is associated with that block and it is passed to the BlockManager to execute on a delegated basis.

The methods inside the BlockManager have been carefully crafted to duplicate each block type's behavior. To implement this in Java it has been necessary to use exceptions for more than simply exceptional circumstances. While this is not a best practice, it was necessary to achieve the flow of control results that duplicate Progress 4GL semantics.

For this reason, conversion-conforming code must be especially careful with its exception handling. It may be possible to catch and handle particular exceptions, but doing so may also cause the Progress-compatible behavior to break. Please carefully review the flow of control and transaction processing sections of the FWD Conversion Reference for details. Uncaught exceptions in conversion-conforming code may cause specific Progress-compatible flow control behavior. This is true for the various ConditionException sub-classes as well as for LeaveUnwindException, NextUnwindException, RetryUnwindException, ReturnUnwindException, and StackUnwindException. Any other exception is not expected and will usually cause the entire call stack to unwind and exit from the session.

In hosted services, exception handling can be done as would be normal for Java except in cases where the hosted service calls conversion-conforming code. In such cases, the conversion-conforming code may generate specific exceptions of which the hosted code must be aware. Please see the section entitled Progress-Compatible Top-Level Session Wrapper for details and examples.

Uncaught exceptions in hosted services will cause the API call to abort and the exception will be delivered to the caller on the remote side of the connection. If the caller of the remote object does not handle the exception, the caller's stack will unwind, possibly to the point at which the client process will exit. This will cause the session with the FWD server to be disconnected.

Security Context

The FWD application server is designed to be a multi-user system. Though the FWD server exists as a single process, each thread in the server is associated with a specific security context. A security context is the identity of the account upon whose behalf the code is executing. The association of a specific account with a specific thread is made during the authentication process which occurs when a new FWD session is established. A FWD session is the term that describes all processing, threads and state that belongs to a the account that was authorized during the session setup process. For interactive users that process is initiated by the FWD client, which connects to the FWD server and establishes a session using some combination of login (userid and password) and/or X.509 certificates. The opposite of interactive users are remote processes. There are 2 kinds of remote process, batch processes that run converted 4GL code and external applications that access the FWD server's exported APIs. Both kinds of remote processes use X.509 certificates to authenticate with the FWD server. Batch processes also use the FWD client to connect and invoke the converted 4GL code on the FWD server. External applications use custom Java code that programs the classes in the com.goldencode.p2j.net package to connect and use the FWD sevrer. For more details on how to establish a session with the FWD server from custom code, please see the chapter on Integrating External Applications. No matter how the connection occurs, once the user or process is authenticated to the FWD server, the FWD server associates the account (of that user or process) with all code running on that account's behalf on the server. The threads associated with that FWD session are known and even when a thread temporarily runs code on behalf of that session, that association (security context) is established.

Whenever a thread executes protected code or attempts to access some protected resource, the FWD server will compare the rights (sometimes known as permissions) of the session's account with the rights needed for the request that was made. If the account's rights are sufficient, then access is allowed, otherwise access is denied. This security decision process is handled by the com.goldencode.p2j.security.SecurityManager in combination with specific resource plugins that provide resource-specific decision-making. See the Resource Plugins section of the Runtime Hooks and Plug-Ins chapter for details on how to add a security plugin. If an account requires more rights to access resources, then additional Access Control List (ACL) entries must be added to the FWD server's configuration (the “FWD directory”).

If server-side code launches a thread, that thread will not have a security context. As a result, it will not be able to access any sensitive or protected code or resources on the server. For example, any use of the remote object protocol will fail unless there is a security context associated with the thread and that context's account has sufficient rights to allow the remote object protocol to be used in the manner requested. Since new threads have no security context unless the security manager assigns one via one of the authentication processes, such server-side threads will have greatly limited capabilities. It is recommended to avoid creating context-free server-side threads.

The FWD session ends when the remote client or process intentionally or unintentionally disconnects from the FWD server. That disconnection can be driven by something from the server-side or it can be caused by something on the client end of the connection. Or some network problem in between may cause the session to disconnect. No matter what the cause of the disconnection, at the time the disconnection occurs, the session ends. When the session ends, the security context is dropped and all threads running code associated with that context are ended.

There is a useful technique where server-side code can store and retrieve session-specific state. This means that there can be a single variable in the server-side code, that has a different value for each security context. This is a simple mechanism by which a great deal of complex and useful behavior can be implemented. For details, see the Context-Local Data section of the chapter entitled Runtime Hooks and Plug-Ins.

Threading

For each FWD client there is a remote object session inside the FWD server that has a dedicated thread (called the “conversation” thread) to process business logic and 2 threads to service the network protocol (a reader and writer).

For each external application connection to a hosted service, there is likewise a remote object session inside the FWD server. Each direct session will have 2 (reader and writer) threads to service the network protocol; when creating virtual sessions, there will be only 2 (reader and writer) threads to serve all the virtual sessions. For each independent call to a hosted service's exported API, there will be a thread dispatched to execute that method call. More than one call to a given hosted service may be made simultaneously from the same session (an external process' connection to the FWD server); but, concurrent execution of the same hosted service using the same session has some limitations: the hosted service can't use any of the FWD's runtime; see the Accessing Services section in the Integrating External Applications chapter of this book, for details why this limitation exists. There is no pre-established limit to the number of simultaneous calls on a specific session. Multiple sessions can co-exist to the same FWD server, from the same or different external processes. On the server side, the number of simultaneously processed requests (each API call is a single request) is limited to the number of threads in the dispatcher thread pool. That is a configurable value that is set at the startup of the FWD server (in the bootstrap configuration file). See the FWD Installation, Configuration and Administration Guide for more details. Any requests in excess of the number of threads in the dispatcher thread pool are queued and handled in a FIFO (first-in-first-out) basis.

Server-side code will run on a dedicated conversation thread (used by the users or batch processes which need to connect to the converted application) or on one of the dispatcher threads (for hosted services). Note that the conversation thread will execute all requests in a LIFO order; so, if there are multiple simultaneous API calls using the same session, the conversation thread can not be used. In either case, the thread on which the request is executed (being a dispatcher or the conversation thread) will have a security context associated which cannot be changed (see the section above on Security Context). All processing will be done on that thread unless another thread is launched explicitly. Converted code does not launch any additional threads, but a hosted service can be designed to do so.

Another mechanism to start a thread for server-side code is to explicitly start that thread using code that runs when the server initializes. Please see the section on Server Initialization and Termination Hooks in the Runtime Hooks and Plug-Ins chapter.

Any explicitly launched thread from a hosted service will be running inside the same process as the FWD server, but it will not have any valid security context. This means that any secured FWD functionality (code that requires a security context to operate) will fail if called from such a thread. As long as any such thread limits its processing to non-secured areas of processing, it will run without problem.

All explicitly launched threads must be marked as daemon threads (using Thread.setDaemon(true)). Any thread that is not marked as a daemon thread will cause the FWD server to hang at shut down until that thread naturally exits.

It is recommended to avoid explicitly launched threads to eliminate the above noted issues.

Memory Usage

All server-side code runs inside a single JVM process which is launched and managed by the FWD runtime. This means that all converted code and all code implemented as hosted servers run inside the same FWD server process. The converted code is accessed by interactive users and processes (including batch processes), each of which run a FWD client and connect to the FWD server to run the application code.

All of this means that server-side code that allocates memory must be very careful to limit the total amount of memory used, since there is only a single JVM heap servicing all needs for the FWD server. In addition, any kind of memory leak or long running processing that consumes large amounts of memory will potentially contribute to or substantially cause OutOfMemoryError to be raised if the heap runs out of space. Such errors don't necessarily occur on the same thread that allocated the memory since the next thread to attempt to allocate memory on a system that has just run out of heap space cannot be deterministically predicted.

Limit all memory usage. Be very careful to cleanup resources. Be very careful when referencing memory from static members since such resources do not go away. Avoid using non-static inner classes where possible so that implicit this references are minimized (each non-static inner class has an implicit this reference to the enclosing class, which will cause that containing instance to remain in memory).

Progress-Compatible Top-Level Session Wrapper

Some hand-written code will have a requirement to utilize functionality of the FWD runtime that provides Progress 4GL compatible features. Each thread running hand written code in the FWD server will be running within the context of a specific user. That user's session (security context) has to be enabled to run Progress 4GL compatible code. The code uses a special adapter or “wrapper” to create the proper top-level transaction environment in which all processing will run. FWD provides a top-level session “wrapper” that allows arbitrary hand-written code to run as if it was a 4GL procedure converted into Java.

This hand-written code may be written with the style and conventions of converted code, but without calling any converted code. It is also possible to cleanly call public methods of converted code using this same facility. Both of these techniques can be intermixed as necessary.

Either way, this facility provides the mechanism to shift into an environment in which to run conversion-conforming code.

This wrapper facility is provided by the invoke() method of the com.goldencode.p2j.main.StandardServer class, which has the following signature:

public static Object invoke(int stopDisp, Isolatable entry)
throws NullPointerException,
       IllegalAccessException,
       InvocationTargetException,
       InstantiationException

The second parameter is an instance of the Isolatable interface. This interface is exclusively designed to expose the following method:

public Object execute()
throws IllegalAccessException,
       InvocationTargetException,
       InstantiationException;

The idea is for the caller to implement this interface and then to delegate the execution of that logic to the transaction wrapper (StandardServer.invoke()). The Isolatable provides the delegation mechanism and the transaction wrapper establishes the proper environment by handling the initialization, termination and other transaction processing that is necessary to safely execute the given logic.

The first parameter to invoke() is the stop disposition. This defines how the transaction environment wrapper responds to a STOP condition. A value of 0 is used to have the wrapper implement a retry. This means that if the Isolatable.execute() method invocation throws a StopConditionException, Isolatable.execute() would be run again without ever returning from invoke(). A value of 1 is used to force a logoff (terminate the user's security context or session) and then to force a re-login. A value of 2 is used to force a logoff. In practice, a value of 1 should never be used, since the re-login facility is unavailable for hand written Java code. Not only is it unavailable, but it will call code in LogicalTerminal which is something that should not be done (see below). If an automatic retry is not wanted, then use a value of 2 which will cause invoke() to re-throw the StopConditionException. It is expected that the caller of invoke() would handle the STOP condition in that case.

The Object returned by Isolatable.execute() will be the Object return value from StandardServer.invoke(). This allows the delegated code to return arbitrary data back to the original caller.

Since Isolatable.execute() takes no parameters, it is important to pass any data to the implementation class before StandardServer.invoke() is called. If an anonymous class is used, then it is generally necessary to define that data as final in the calling code so that it can be directly referenced. If an explicitly defined class is written, then the data can be passed through a constructor or any arbitrary set of methods.

It is important to place the wrapped code inside the proper try/catch blocks so that exception handling can be cleanly managed. If a stop disposition of 2 is used, it may be especially important to evaluate the cause of the STOP condition and take special action. It may also be necessary to catch the other ConditionException subclasses (ErrorConditionException, EndConditionException and QuitConditionException), depending on the wrapped code.

An example:

// these are the necessary imports
import com.goldencode.p2j.main.Isolatable;
import com.goldencode.p2j.main.StandardServer;
import com.goldencode.p2j.util.*;

// this import is used to make the wrapped code cleaner
import static com.goldencode.p2j.util.BlockManager.*;

...

// data that will be visible to the wrapped code below
final Object[] myData = new Object[2];

// even though the array reference is final, the values of
// the elements can still be modified; this is a “trick” to
// allow a set of parameters to be referenced from an
// anonymous inner class without necessarily having the data
// be really “final”
myData[0] = parm1;
myData[1] = parm2;

Integer rc = null;

try
{
   // the invocation code must be inside a try/catch block to
   // allow for the proper exception handling

   rc = (Integer) StandardServer.invoke(2, new Isolatable()
   {
      public Object execute()
      {
         // place your “wrapped” code here
         return someLocalMethod(myData);
      }
   });
}

catch (StopConditionException sce)
{
   Throwable chained = sce;

   // look through all causes to determine what the underlying error was
   while (chained != null)
   {
      // change “SomeAppSpecificException” into an exception type
      // that you care about
      if (chained instanceof SomeAppSpecificException)
      {
         throw (SomeAppSpecificException) chained;
      }

      chained = chained.getCause();
   }

   throw sce;
}

catch (Exception ex)
{
   // could be IllegalAccessException,  InvocationTargetException,
   // InstantiationException, NullPointerException or some kind of
   // RuntimeException

   // these exceptions may not even be possible depending on the
   // code that is executed inside the wrapper
}

// do something with the rc here; it is also possible that the myData array
// elements could have been modified by the wrapped code (if you so chose)

It is not a requirement that the wrapped code be separated into it's own method (the call to someLocalMethod() in the example above). But implementing a separate method can make for more readable and manageable code.

The wrapped code must adhere to certain conventions. See the section entitled Server-Side Code in the chapter Integrating Hand-Written Java for the most important limitations.

For the wrapper, it is important to enclose all logic in a try/finally construct. Inside the finally block, make a call to TransactionManager.setProcessingQuit(true). That ensures that during the session-level exit processing, the TransactionManager will not attempt to call any FWD client functionality. If this is not done, an abnormal end will result.

The above described conventions are just the minimum needed to work. Many of the other normal converted code conventions may also need to be honored, depending on what the wrapped code is doing. For full details on how to write conversion-conforming code, please see the FWD Conversion Reference. That book documents each 4GL feature and how the resulting converted code is generated to replace that 4GL.

The following is an example of wrapped code:

private Integer someLocalMethod(final Object[] myData)
throws SomeAppSpecificException
{
   TransactionManager.setHeadless(true);

   final logical error = new logical(false);

   try
   {
      // this is an example REPEAT TRANSACTION block
      repeat(BlockManager.TransactionType.FULL, "my-block-label", new Block()
      {
         public void init()
         {
            // some setup must be done here (especially for database buffers
            // and queries
         }

         public void body()
         {
            // do real work here

            // this is the equivalent of using NO-ERROR in Progress
            ErrorManager.silentErrorEnable();
            // do a single language statement or expression that may error
            ErrorManager.silentErrorDisable();

            // example error handling
            if (ErrorManager.isErrorFlag())
            {
               error.assign(true);
               BlockManager.undoLeave("my-block-label");
            }
         });
      }
   }

   finally
   {
      TransactionManager.setProcessingQuit(true);
   }

   if (error.booleanValue())
   {
      throw new SomeAppSpecificException(ErrorManager.getErrorText(1).toStringMessage());
   }

   return someIntegerReturnValue;
}

Managing Data Model Object (DMO) Changes

The DMOs can be managed both while the 4GL code is still maintained (i.e., conversion is still being run actively) and after the 4GL code maintenance is finished (i.e., conversion will never be run again).

During the active conversion part of the project, only new DMOs can be directly added in Java; maintenance of existing DMOs can not be done in Java, this must be done in the original .df schema file. In this phase, new DMOs must be added to the cfg/dmo_index_merge.xml, as the generated dmo_index.xml file must not be changed (as it might be re-generated during the conversion process, depending on the options used).

After the conversion is no longer active, both existing and new DMOs can be managed directly in Java, and direct edit of the dmo_index.xml file is allowed. At this phase, the cfg/dmo_index_merge.xml is no longer necessary and should not be maintained.

How the DMO Index Merge File Works

The dmo_index.xml file is a special file, generated during conversion, and is located in the dmo package. It contains the list of all DMOs (grouped by parent schema) used by the application and, for each DMO, some runtime information which cannot be stored in its Hibernate configuration file. This file will be read on server startup and all DMOs which are listed here will be registered with the FWD's persistence layer and with Hibernate. This file being generated during conversion, its maintenance is subject to the same concerns as the DMO sources - namely, will there be subsequent conversion runs or not.

Full details about the content of the dmo_index.xml can be found in the DMO index section of the Part 3 - Schema Conversion chapter of the FWD Conversion Reference book.

While the 4GL code is still maintained, changes must not be applied directly to this file. Instead, FWD provides an alternative mechanism to bring the necessary updates to this file - it is a special “merge” file, which will be read during conversion and its content merged with the generated dmo_index.xml file. This special “merge” file is specified in the p2j.cfg.xml file, as a global parameter:

<parameter name="dmo_index_merge” value="${P2J_HOME}/cfg/dmo_index_merge.xml" />

In case the file does not exist, it will be ignored and no merge will be done; else, the conversion rules will read this file and merge it in the generated dmo_index.xml file. The structure of the merge file is the same as the target's - it contains entire DMO specifications, grouped by parent schema. After the main DMO index file is generated, conversion rules will read this file and append the contents for each schema to the existing one; if the schema is not found in the generated DMO index, the entire schema is appended at the end of the file.

For each schema, the merged DMO specifications will be bracketed using these comments:

<!-- Started merged classes section-->
...
<!-- Finished merged classes section-->

In cases when an entire schema is merged, its definition will be bracketed using these comments:

<!-- Started merged schema section-->
...
<!-- Finished merged schema section-->

So, the merge file will be used until its decided the legacy 4GL code will no longer be maintained; after this, as no conversion will be run in the future, the DMO maintenance will be switched from the merged file to the generated dmo_index.xml file. Also, the contents of this file follow the same rules as the contents of the dmo_index.xml file.

When adding a new DMO, depending on the project stage (4GL code under maintenance or not), its definition will be added to the merge file or to the dmo_index.xml file. Regardless, the structure of its definition will be the same. If the file doesn't already have a schema node to which the DMO will be registered, this node will need to be added first. After this, a new class node will be added, which will contain:

  1. an index node, for each non-unique index
  2. an unique node, for each unique index
  3. a case-sensitive node, for each case-sensitive property
  4. an encoded node, for each binary property
  5. a foreign node, for each foreign relation (used in natural joins)

Even if the DMO doesn't need any of the above properties, the class node still must exist, as FWD will read this file and register all the defined DMOs with Hibernate, during database initialization.

For an existing DMO, if a new index is created or there are some property-related changes, the changes will need to be reflected in the dmo_index.xml file. If the merge file is still used and the DMO is defined in it, then all the changes can be safely added to it; else, if the DMO is generated during conversion (and belongs to the dmo_index.xml file), some changes will need to be done in the 4GL schema and others will be automatically picked up during conversion, from the 4GL programs which use this permanent table.

The changes which need to be done in the 4GL schema are related to table fields (i.e. adding, removing, changing type/name, field type, case sensitivity) and table indexes. Other info - like natural joins which translate to foreign relations or runtime defined indexes - will be read from the 4GL file and, during conversion, will be automatically associated with the DMO's definition, in the dmo_index.xml file.

For temporary tables, their definition resides in the 4GL programs; if the 4GL code is still maintained and the temporary table was generated on conversion, all changes related to them must be done in the 4GL program which defines that temporary table. If the temporary table is a new table, which needs to be used in a new Java program, then its definition will need to be written in the merge file or directly in the dmo_index.xml file, following the same rules as for permanent DMOs (whether the 4GL code is still maintained or not).

Following are two examples to show how the dmo_index_merge.xml file works when it has to merge schema and dmo-level nodes, and how the dmo_index.xml file will look after the project is reconverted:

Example 1:

dmo_index_merge.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<dmo-index package="com.app.dmo">
  <schema impl="test.impl" name="test">
    <class interface="Author">
      <index name="idx__author_name">
        <column name="name" ignore-case=”true”/>
        <column name="invoice_number" ignore-case="true" />
    </class>
  </schema>
</dmo-index>

dmo_index.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<dmo-index package="com.app.dmo">
  <schema impl="test.impl" name="test">
    <!-- ... list of other DMO's →
    <!-- Started merged classes section-->
    <class interface="Author">
      <index name="idx__author_name">
        <column name="name" ignore-case=”true”/>
        <column name="invoice_number" ignore-case="true" />
    </class>
    <!-- Finished merged classes section-->
  </schema>
</dmo-index>

Details:

Assuming that the application already contains a schema named test, all the merge DMO info belonging to the test schema will be added at the end of the schema definition. Note how the special comments enclose the merge DMOs; when looking directly at dmo_index.xml, this will help you understand from where the DMO originates: from the 4GL schema or a new DMO written directly in Java.

Example 2:

dmo_index_merge.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<dmo-index package="com.app.dmo">
  <schema impl="model.impl" name="model">
    <class interface="Author">
      <index name="idx__author_name">
        <column name="name" ignore-case=”true”/>
        <column name="invoice_number" ignore-case="true" />
    </class>
  </schema>
</dmo-index>

dmo_index.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<dmo-index package="com.app.dmo">
  <!-- ... other schema nodes →
  <!-- Started merged schema section-->
  <schema impl="model.impl" name="model">
    <class interface="Author">
      <index name="idx__author_name">
        <column name="name" ignore-case=”true”/>
        <column name="invoice_number" ignore-case="true" />
    </class>
  </schema>
  <!-- Finished merged schema section-->
</dmo-index>

Details:

Assuming that the application does not contain the schema named model, the entire schema node for the model schema will be added as a child, at the end of the dmo-index node. Note how the special comments enclose the merge DMOs; when looking directly at dmo_index.xml, this will help you understand from where the DMO originates: from the 4GL schema or a new DMO written directly in Java.

Adding a Field to a Data Model Object

Consider you want to add the field authorName to the DMO Book. Note that you cannot do it if you want to be able to reconvert the project - your files will be overwritten during next conversion. Otherwise, follow these steps in order to do it:

  1. Create and execute on the target database SQL script that will add target column to the target table (to which the given DMO field is mapped to) and, if required, will also create appropriate indexes for faster record lookup. If you're creating indexes for the character field, then you should use for indexing upper-ed and@ rtrim-ed version of the field (because of 4GL character field comparison peculiarities):
    ALTER TABLE book ADD COLUMN author_name CHARACTER VARYING(255);
    CREATE INDEX idx__book_author_name ON book USING btree
       (upper(rtrim(author_name, E' \t\n\r')), id);
    
  2. If added field is unique then you should add unique section to the target DMO into the dmo/dmo_index.xml (note that the database index for this field should be unique too in order to make validation work properly):
    <dmo-index package="com.company.project.dmo">
      <schema impl="mydb.impl" name="mydb">
        <class interface="Book">
          <unique>
            <component name="authorName"/>
          </unique>
          ...
        </class>
        ...
    </dmo-index>
    
  3. Add field definition to the appropriate hibernate mapping file. In our case it will be dmo/mydb/impl/BookImpl.hbm.xml:
    <hibernate-mapping package="com.company.project.dmo.mydb.impl">
    ...
      <class name="BookImpl" table="book">
        ...
        <property column="author_name" name="authorName" type="p2j_character"/>
      </class>
    </hibernate-mapping>
    
  4. Add field getter and setter to the target DMO interface:
    package com.company.project.dmo.mydb;
    ...
    public interface Book
    extends DataModelObject
    {
       ...
       public character getAuthorName();
       public void setAuthorName(character authorName);
    }
    
  5. Implement field getter, setter and some other functionality into the target DMO implementation class:
    package com.company.project.dmo.mydb.impl;
    
    public class BookImpl
    implements Serializable,
               Persistable,
               Book
    {
       ...
       private final character authorName;
    
       public BookImpl()
       {
          ...
          // assign default value
          authorName = new character("");
       }
    
       public void assign(Undoable old)
       {
          BookImpl from = (BookImpl) old;
          ...
          setAuthorName(from.getAuthorName());
       }
    
       ...
    
       public character getAuthorName()
       {
          return new character(authorName);
       }
    
       public void setAuthorName(character authorName)
       {
          this.authorName.assign(authorName);
       }
    
    }
    

Adding a Data Model Object

Consider you want to add a new DMO named Magazine with two fields: title and price. We will give solution for two cases: if want to be able to reconvert the project, and if conversion has been completed. Follow these steps to add the DMO using Java language:

  1. Create and execute on the target database SQL script that will add target table (to which the given DMO is mapped to) and, if required, will also create appropriate indexes for faster lookup. If you're creating indexes for the character field, then you should use for indexing upper -ed and rtrim -ed version of the field (because of 4GL character field comparison peculiarities):
    CREATE TABLE magazine
    (
       id BIGINT NOT NULL,
      title CHARACTER VARYING(255),
      price NUMERIC,
      CONSTRAINT magazine_pkey PRIMARY KEY (id)
    );
    
    CREATE UNIQUE INDEX idx__magazine_title ON magazine USING btree
       (upper(rtrim(title, E' \t\n\r')), id);
    
    CREATE INDEX idx__magazine_price ON magazine USING btree
       (price, id);
    
  2. Add DMO declaration section to cfg/dmo_index_merge.xml if you want to be able to reconvert the project or to src/<project path>/dmo/dmo_index.xml otherwise:
    <dmo-index package="com.company.project.dmo">
      <schema impl="mydb.impl" name="mydb">
        <!-- declare DMO -->
        <class interface="Magazine">
          <!-- declare unique components, if any -->
          <unique>
            <component name="title"/>
          </unique>
        </class>
        ...
    </dmo-index>
    
  3. Create the hibernate mapping file. The best way to do this step and the next two steps is to take an existing file from another DMO and modify it for your own DMO. In our case the file will be named dmo/mydb/impl/MagazineImpl.hbm.xml. You should put it under the srcnew directory if you want to be able to reconvert the project or to the src directory otherwise (see “Including Java classes into converted project” chapter for more information about the srcnew directory).
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 
             "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    <hibernate-mapping package="com.company.project.dmo.mydb.impl">
      <!-- Typedefs for FWD wrapper types -->
      <typedef class="com.goldencode.p2j.persist.type.IntegerUserType" name="p2j_integer"/>
      <typedef class="com.goldencode.p2j.persist.type.DecimalUserType" name="p2j_decimal"/>
      <typedef class="com.goldencode.p2j.persist.type.LogicalUserType" name="p2j_logical"/>
      <typedef class="com.goldencode.p2j.persist.type.CharacterUserType" name="p2j_character"/>
      <typedef class="com.goldencode.p2j.persist.type.DateUserType" name="p2j_date"/>
      <typedef class="com.goldencode.p2j.persist.type.RawUserType" name="p2j_raw"/>
      <typedef class="com.goldencode.p2j.persist.type.RowIDUserType" name="p2j_rowid"/>
    
      <!-- Magazine class -->
      <class name="MagazineImpl" table="magazine">
        <!-- Second level cache -->
        <cache usage="nonstrict-read-write"/>
        <!-- Surrogate primary key -->
        <id column="id" name="id" type="long">
          <generator class="assigned"/>
        </id>
    
        <!-- Title -->
        <property column="title" name="title" type="p2j_character"/>
        <!-- Price -->
        <property column="price" name="price" precision="50" scale="2" type="p2j_decimal"/>
      </class>
    </hibernate-mapping>
    
  4. Create the DMO interface. You should put it under the srcnew directory if you want to be able to reconvert the project or to the src directory otherwise.
    package com.company.project.dmo.mydb;
    
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.util.*;
    
    public interface Magazine
    extends DataModelObject
    {
       // Getter: Title
       public character getTitle();
    
       // Setter: Title
       public void setTitle(character title);
    
       // Getter: Price
       public decimal getPrice();
    
       // Setter: Price
       public void setPrice(NumberType price);
    }
    
  5. Create the DMO implementation class. You should put it under the srcnew directory if you want to be able to reconvert the project or to the src directory otherwise.
    package com.company.project.dmo.mydb.impl;
    
    import java.io.Serializable;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.util.*;
    import com.company.project.dmo.mydb.*;
    
    public class MagazineImpl
    implements Serializable,
               Persistable,
               Magazine
    {
       // Surrogate primary key
       private Long id = null;
    
       // Title
       private final character title;
    
       // Price
       private final decimal price;
    
       // Default constructor.
       public MagazineImpl()
       {
          title = new character("");
          price = new decimal(0, 2);
       }
    
       // Makes a deep copy of the current DMO instance and returns it.
       public Undoable deepCopy()
       {
          Undoable copy = new MagazineImpl();
          copy.assign(this);
          return copy;
       }
    
       // Modifies the contained data of the instance to copy the instance data from
       // the given, backup instance.
       public void assign(Undoable old)
       {
          MagazineImpl from = (MagazineImpl) old;
          setId(from.getId());
          setTitle(from.getTitle());
          setPrice(from.getPrice());
       }
    
       // Getter: Surrogate primary key
       public Long getId()
       {
          return id;
       }
    
       // Setter: Surrogate primary key
       public void setId(Long id)
       {
          this.id = id;
       }
    
       // Getter: Title
       public character getTitle()
       {
          return (new character(title));
       }
    
       // Setter: Title
       public void setTitle(character title)
       {
          this.title.assign(title, true);
       }
    
       // Getter: Price
       public decimal getPrice()
       {
          return (new decimal(price));
       }
    
       // Setter: Price
       public void setPrice(NumberType price)
       {
          this.price.assign(price, true);
       }
    }
    

Temporary DMOs are added in the same way as permanent ones. The name of the temporary database is _temp.

Guidelines for Writing Conversion Conforming Code

When developing new application features or maintaining converted code, as a developer you must not feel constrained to write Java code which is 100% similar in style to the Java code produced by FWD conversion engine. Although FWD runtime features must be used to make the code 4GL-compatible, FWD allows you to combine these runtime APIs with other Java-only features, like Java variables, methods, inheritance, and more. This section will describe how you can best use the FWD runtime and Java when writing 4GL-compatible code. Also, this section acts like a summary of the most common features a developer will use. But, whenever in doubt of how a 4GL feature works in FWD, please refer to the FWD Conversion Reference book for full details.

During early stages of understanding the FWD runtime and conversion process, it is recommended to write 4GL code, look at the converted result and integrate the converted result in your new Java code. This will be helpful in understanding some of the complex part of FWD, like frame definition and query code.

Writing Block-Aware Code

Grouping code in 4GL-style blocks is the only way to provide 4GL-style transaction support and also compatibility with legacy programs. For this, when writing new Java code, one needs to follow the same rules used by the conversion engine to convert the 4GL blocks to Java code. Although to some extent you can customize the way you initialize and execute the block, the result must be an invocation of a block API defined by BlockManager class.

Dedicated pure Java methods can be used to split certain code, so it will be easier to read and maintain; this "pure Java" methods act as the 4GL code is inlined, so any parameters such as buffers, variables, frames, etc, will act as they are scoped to the 4GL-style block which called this method.

Another 4GL feature which can be better emulated with Java methods is 4GL's include files. During the preprocessor part of the conversion, each include file is merged together with the 4GL program which has included it. But, when writing new Java code, this 4GL feature can be better integrate by using static methods. Invoking these static methods from any Java code will act as the code is inlined.

The Block Conversion section of the Blocks chapter of the FWD Conversion Reference book presents details about how the Block class methods need to be defined and how they are handled by the FWD runtime. To resume, each Block instance must have the following structure:

new Block()
{
   public void init ()
   {
      // this is guaranteed to be called AFTER the transaction manager scope opens but BEFORE the
      // block body is ever entered.
      // It will only be called once no matter whether the block iterates or
      // whether there is a retry.
   }

   public void enter()
   {
      // this method is emitted in the converted code only in some special cases of the FOR block.
      // It provides a "callback" to execute user-defined logic at the top of the block body but
      // before the block body is executed.
      // This is guaranteed to be called AFTER the transaction manager scope opens but BEFORE
      // the block body is entered, on each iteration and on retry.
   }

   public void body()
   {
      // implements the delegated code to be executed as part of this instance, on each iteration
   }
}
External Procedures

The first type of block you need to know about is an external procedure, which is a type of top level block. In 4GL it is simply a .p or .w file. In FWD it is usually a separate .java file which has the following structure:

package some.package;

import com.goldencode.p2j.util.*;
import static com.goldencode.p2j.util.BlockManager.*;

public class Test
{
   public void execute()
   {
      externalProcedure(new Block()
      {
         public void body()
         {
            // Procedure code.
         }
      });
   }
}

Full details about how the FWD runtime converts external procedures can be found in the Top Level BlocksExternal Procedures section of the Blocks chapter of the FWD Conversion Reference book.

In order to run an external procedure, you need to build a new instance of the required class and run its execute() method, the equivalent of a 4GL RUN statement:

Test test = new Test();
test.execute();

When the external procedure needs parameters, they will be defined for the execute method, and not for the class c'tor. This is because, to be compatible with dynamic class invocation, the class associated with an external procedure must contain only a default c'tor (with no arguments). Also, the arguments must be declared final, so they will be seen by the anonymous Block instances, where the actual code is written:

public void execute(final <parameter type 1> <parameter name 1>,
                  final <parameter type 3> <parameter name 4>, ...)
{
   externalProcedure(new Block()
   {
      public void init()
      {
         // do this for all parameters *except* NO-UNDO parameters
         TransactionManager.register(<parameter name 1>,
                                     <parameter name 2>, ...);

         // for NO-UNDO parameters you should call
         TransactionManager.deregister(new Undoable[] {<parameter name 3>, <parameter name 4>, … });
      }

      public void body()
      {
         // procedure code
         // access to the parameters is allowed, as they are defined final
      }
   });
}

For details about how to pass the 4GL style parameters, see the Variables section of this chapter.

Internal Procedures

The second type of top level blocks are the internal procedures. To group code in a 4GL-style internal procedure, you need to define a public instance method in the class associated with the external procedure and enclose its code in a Block instance, passed to a BlockManager.internalProcedure call, as in:

public void <procedure name>(final <parameter type 1> <parameter name 1>,
                             final <parameter type 3> <parameter name 4>, ...)
{
   internalProcedure(new Block()
   {
      public void init()
      {
         // do this for all parameters *except* NO-UNDO parameters
         TransactionManager.register(<parameter name 1>, <parameter name 2>, ...);

         // for NO-UNDO parameters you should call
         TransactionManager.deregister(new Undoable[]
         {
            <parameter name 3>,
            <parameter name 4>, ...
         });
      }

      public void body()
      {
       // Procedure code.
      }
   });
}

The details about how FWD handles and converts the internal procedure can be found in the Top Level Blocks → Internal Procedures section of the Blocks chapter of the FWD Conversion Reference book.

User-Defined Functions

User-defined functions have a similar structure as internal procedures, except that they must return a 4GL-compatible value (an instance of a wrapper type provided by FWD) and must use a BlockManager.return* API call to return the desired value to the caller. The Top Level BlocksUser-Defined Functions section of the Blocks chapter of the FWD Conversion Reference book has details about how FWD handles and converts them.

4GL-compatible functions are defined in this way:

public <return type> <function name>(final <parameter type 1> <parameter name 1>,
                                     final <parameter type 2> <parameter name 2>, ...)
{
   // Possible variants are integerFunction, characterFunction etc.
   // See BlockManager for the full set of possible function blocks.
   return <return type>Function(new Block()
   {
      public void init()
      {
         // For some reason 4GL considers all function parameters as NO-UNDO
         // It's up to you to decide whether you want to make them NO-UNDO it too.
         TransactionManager.deregister(new Undoable[]
         {
            <parameter name 1>,
            <parameter name 2>, …
         });
      }

      public void body()
      {
         // Function code which can use returnNormal(<return value>)
      }
   });
}

E.g.:

public integer mult(final integer var)
{
   return integerFunction(new Block()
   {
      public void body()
      {
         returnNormal(multiply(var, 2));
      }
   });
}
Defining Triggers

The code for a 4GL-style trigger is split in two parts: code to register the trigger and code to define the trigger. The trigger registration code is done via LogicalTerminal.registerTrigger APIs, while the trigger definition code must be enclosed in the body() method of a class which implements the Trigger interface. The Top Level BlocksTriggers section of the Blocks chapter of the FWD Conversion Reference book has details about how FWD handles and converts triggers.

When writing new Java code, it is important to note that, in FWD (and 4GL), each time a trigger is invoked, a new instance of the class which encloses the trigger's code is created. So, you need to be careful if the trigger code needs to save any state after invocation - this can be done in instance fields associated with the external procedure class.

The structure of a trigger is:

public class TriggerBlock<index>
extends Trigger
{
   /* field definitions */

   public void init()
   {
      /* state initialization code */
   }

   public void body()
   {
      /* trigger body */
   }
}

where the init() and body() methods have the same roles as the ones defined by the Block interface.

Other Types of Blocks

Other 4GL blocks can be mapped to FWD blocks, by using static BlockManager APIs. The following table describes how each API is mapped to 4GL statements and where you can find details about it:

4GL Statement BlockManager APIs Details
REPEAT repeat
repeatTo
repeatToWhile
repeatWhile
Full details can be found in the Looping BlocksREPEAT Block section of the Blocks chapter in the FWD Conversion Reference book.
FOR EACH forEach
forEachTo
forEachToWhile
forEachWhile
Full details can be found in the Looping BlocksFOR Block section of the Blocks chapter in the FWD Conversion Reference book.
DO doBlock
doTo
doToWhile
doWhile

Java operators:
label: { }
for(...) { }
while(...) { }
Full details can be found in the Looping BlocksDO Block section of the Blocks chapter in the FWD Conversion Reference book.
FOR [ FIRST | LAST ] forBlock
forBlockTo
forBlockToWhile
forBlockWhile
Full details can be found in the Looping BlocksREPEAT Block section of the Blocks chapter in the FWD Conversion Reference book.

Most of these blocks have a similar structure:

<block type>(TransactionType transactionType,
             String blockLabel,
             ToClause toClause,
             LogicalExpression whileClause,
             OnPhrase onPhrase,
             new Block()
{
   public void init()
   {
      // Code that should be executed once the block is entered and before the
      // body() is executed, like opening frame and buffer scopes, setting up
      // accumulators, defining queries, etc. It is optionally to implement this
      // function.
   }

   public void body()
   {
      // Code which corresponds the block's body. It can be executed several times
    // if the block is iterative.
   }
});

where:

  • transactionType - optional, defines the transaction level to be honored. Passing TransactionType.FULL is equivalent to the 4GL TRANSACTION option.
  • blockLabel - defines the block label (e.g. “cycle1”), can be further used by statements like undo, next, leave and retry.
  • toClause - can be applied only to *To and *ToWhile APIs. Represents an incrementing or decrementing loop construct. It is equivalent to 4GL's variable = expression1 TO expression2 [ BY k ] clause. It is handled by a ToClause instance, with its constructor accepting the following parameters:
  • Variable to be looped;
  • Lower limit of the cycle;
  • Upper limit of the cycle;
  • Optionally, cycle step. It may be negative, which means that the cycle will be decrementing. By default it equals 1.

E.g.:

integer var = new integer();
integer limit= new integer(10);
ToClause toClause = new ToClause(var, 1, limit, 2);
...
  • whileExpression - can be applied to *While and *ToWhile blocks. This expression is evaluated every time the loop body is about to be entered and the body will be entered only when the expression evaluates to true. It is equivalent to 4GL WHILE expression clause. In order to define an expression, you should define a new LogicalExpression anonymous class, with its execute() method overridden, as in:
LogicalExpression whileClause = new LogicalExpression()
{
   public logical execute()
   {
      return isLessThan(var, 5);
   }
};

When to and while clauses are present, they both must evaluate to true in order to enter the next iteration of a cycle.

  • onPhrase - optional, equivalent to 4GL ON { ERROR | ENDKEY | STOP | QUIT } clause. It is an array that contains the associations of conditions and actions that should be performed when these conditions will occur. Associations can be established using OnPhrase instances. The Looping BlocksON Phrase section of the Blocks chapter in the FWD Conversion Reference book descries how this statement gets converted and how it works in FWD. Each OnPhrase instance will allow you to specify:
  • The condition to be handled:
    table{margin-left:auto;margin-right:auto}.
    4GL Condition FWD API
    ERROR Condition.ERROR
    ENDKEY Condition.ENDKEY
    STOP Condition.STOP
    QUIT Condition.QUIT
  • The action that should be performed when the given condition occurs:
    table{margin-left:auto;margin-right:auto}.
    4GL Condition FWD API
    RETRY Action.RETRY
    LEAVE Action.LEAVE
    NEXT Action.NEXT
    RETURN Action.RETURN_NORMAL
    RETURN ERROR Action.RETURN_ERROR
    RETURN NO-APPLY Action.RETURN_CONSUME
  • The label of the target block for the given action. If omitted, then the action will be applied to the current block.

For example:

OnPhrase[] onPhrase = new OnPhrase[]
{
   new OnPhrase(Condition.ERROR, Action.RETRY),
   new OnPhrase(Condition.ENDKEY, Action.LEAVE, "outerLoop")
};

The difference between the toClause and whileExpression clauses is that the termination expression for the ToClause instance is evaluated only once (on the first iteration) and the whileExpression is evaluated on each iteration. When writing new code in Java, it is not OK to re-use a ToClause instance, as the instance changes its state on each iteration. This does not apply for the LogicalExpression instance - as this is evaluated on each iteration and the expression relies on external resources (such as variables or records), the same instance can be reused throughout the code, considering that the variables or buffers used in the expression are set to their “initial” value.

The same rule applies to the OnPhrase instances - considering that an OnPhrase instance holds static block properties, you can reduce overhead on building new instances in long loops by using the same OnPhrase instance for multiple blocks. The only concern is that you need to be careful in cases where an explicit block is target (i.e. the label is set).

Controlling Block Iterations

Block iterations of 4GL-style blocks are handled using specialized BlockManager APIs. The Conditions, Honoring Stop Conditions, Infinite Loop Protection and Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY sections of the Blocks chapter in FWD Conversion Reference book are a must-read to understand how the 4GL conditions and block control work.

The following table resumes how the 4GL statements are mapped to these static APIs defined by BlockManager:

4GL Statement BlockManager API Details
NEXT next([label]) Go to the next iteration of a cycle.
LEAVE leave([label]) Leave a block.
RETURN returnNormal([label]) Return from the nearest enclosing top-level block.
RETURN ERROR returnError([error]) Return from the nearest enclosing top-level block and raise error condition in the calling code.
RETURN NO-APPLY returnConsume([message]) Used in order to return from a trigger and skip further processing of the event which caused the trigger execution
UNDO, NEXT undoNext([undoTarget[, nextTarget]]) Undo the current iteration and execute the next one (if none remains, exit the block).
UNDO, LEAVE undoLeave([undoTarget[, leaveTarget]])
undoReturnNormalTopLevel([value])
If UNDO and LEAVE are targeted to a top-level block then use undoReturnNormalTopLevel
UNDO, RETRY undoRetry([target])
undoRetryTopLevel()
If UNDO and RETRY actions are targeted to a top-level block then use undoRetryTopLevel
UNDO, RETURN undoReturnNormal([value]) Return normally from the current top level block, but undo all changes.
UNDO, RETURN ERROR undoReturnError([undoTarget[, leaveTarget]])
undoReturnErrorTopLevel([value])
Return error from the current top level block, and undo all changes.
If UNDO and RETURN are targeted to a top-level block then use undoReturnErrorTopLevel.
UNDO, RETURN NO-APPLY undoReturnConsume([target[, value]])
undoReturnConsumeTopLevel([value])
Return from the current block, but undo all changes and skip further processing of the event which caused the trigger execution, when invoked from trigger.
If UNDO and RETURN actions are targeted to a top-level block then use undoReturnConsumeTopLevel.

next and leave APIs are by default applied to the nearest enclosing block, but you can explicitly specify the label of the target block:

leave(“outerLoop”);

When returning from a 4GL-compatible function you are able to return a value of the type that was specified as the function's return type, using BlockManager.returnNormal(<value>) API call, as in:

returnNormal(1)

When returning from a procedure, you are able to send a character value to the caller:

returnNormal(“val”)

which can be read by the caller using an equivalent of 4GL's RETURN-VALUE function:

ControlFlowOps.getReturnValue()

undoNext, undoLeave and undoRetry APIs allow you to specify both UNDO and NEXT/RETRY/LEAVE targets. undoReturnNormal, undoReturnError and undoReturnConsume APIs allow you to specify only UNDO target.

Leaving and Iterating Java Blocks

Some DO blocks can be represented by Java for and while statements or labeled blocks. You can exit them using the break statement or iterate using the continue statement:

label1:
for (i.assign(1); _isLessThanOrEqual(i, 10); i.increment())
{
   label2:
   for (j.assign(1); _isLessThanOrEqual(j, 10); j.increment())
   {
      ...
      if(...)
      {
         continue label1;
      }

      if(...)
      {
         break label2;
      }
   }
}

However, if we have a block which is defined using a function provided by BlockManager at a level between the target Java-block and the calling level (where you're planning to use leave/next functions), like this:

label1:
for (i.assign(1); _isLessThanOrEqual(i, 10); i.increment())
{
   repeat("label2", new Block()
   {
      public void body()
      {
         // cannot use “continue label1” or “break label1” here
      }
   });
}

then you can not use continue or break to target the outer Java-style loop (label1 block in this case). Instead, you should follow these steps:

  1. Create a string array that contains the target block label and pass it as the “enclosing” parameter to the most outer block which is between the target and the calling level and which is defined by a BlockManager API.
  2. Insert the following code just after the block determined on the previous step (which has received the enclosing parameter):
    // if you are planning to be able to leave the target block
    if (deferredLeave("<target label>"))
       break <target label>;
    

    or
    // if you are planning to be able to iterate the target block
    if (deferredNext("<target label>"))
      continue <target label>;
    
  3. Use in the enclosed block the leave("<target label>") in order to leave the Java-style outer block and next("<target label>") in order to iterate it. An example:
    block1:
    for (i.assign(1); _isLessThanOrEqual(i, 10); i.increment())
    {
       block2:
       {
          String[] enclosing = new String[]
          {
             "block1",
             "block2" 
          };
    
          repeat(enclosing, "block3", new Block()
          {
             public void body()
             {
                repeat("block4", new Block()
                {
                   public void body()
                   {
                      ...
                      if (...)
                      {
                         next("block1");
                      }
    
                      if (...)
                      {
                         leave("block2");
                      }
                   }
                });
             }
          });
    
          if (deferredNext("block1"))
             continue block1;
    
          if (deferredLeave("block2"))
             break block2;
       }
    }
    

    The deferredNext and deferredLeave APIs defined in BlockManager will check if the inner block has invoked a leave or next command for the outer Java-style blocks. If they find such a pending command, they will return true, thus executing the proper Java-style loop control statement.

Working with Variables

All 4GL variables are mutable and undoable. In the FWD runtime, mapping the 4GL data types to its Java primitive (or its wrapper) data type is not enough, as this will not duplicate the 4GL's mutable and undoable properties. With these facts, the FWD runtime had to associate a custom data type for each 4GL data type. In some cases, these custom data types act like a wrapper to a Java primitive type, like int or boolean, while in other cases, even the Java primitive types are not enough to duplicate their 4GL counterpart. Details about how and why FWD chose this approach to implement the 4GL's data types are beyond the scope of this chapter, so it is recommended to read the Data Types chapter of the FWD Conversion Reference book before continuing.

Each 4GL type is defined using a class (with the same name as the 4GL data type, in lowercase), under the com.goldencode.p2j.util package. A 4GL variable will be emitted in the converted code in a different location, depending on its scope and usage. All local and global variables emitted in the converted code will be automatically instantiated at their declaration, following the rules in the next table, while function and procedure parameters are only declared.

Type Default Value INITIAL Option (Explicit) Unknown Value
character “” (empty string) new character(String) new character()
date ? (unknown value) new date(String) OR
new date() (used for the 4GL TODAY function)
date.instantiateUnknownDate()
decimal 0 new decimal(double) OR
new decimal(int)
new decimal()
handle ? (unknown value) n/a new handle()
integer 0 new integer(int) OR
new integer(double)
new integer()
logical false new logical(boolean) new logical()
memptr uninitialized byte array (not the same as unknown value!) n/a memptr.instantiateUnknown()
raw zero length byte array (not the same as unknown value!) n/a raw.instantiateUnknown()
recid ? (unknown value) n/a new recid()
rowid ? (unknown value) n/a new rowid()

The 4GL unknown value can not be mapped by the Java's null reference, as, even if a 4GL variable does not refer any value at this time (it is 'unknown'), the variable can be mutated so that it refers another value. This complicates things if the business logic uses shared variables because, if the object reference is lost, it is not possible to set a shared variable to a new value. The result was that the FWD counterpart data types have a special property, unknown, which sets the variable in the 4GL's 'unknown state'.

All 4GL variables are defined using the DEFINE VARIABLE statement, which is explained in detail in the Variable DefinitionsDEFINE VARIABLE section of the Data Types chapter of the FWD Conversion Reference book.

When writing code directly in Java, working with variables poses a question on whether it is safe or not to use Java primitive types, or is mandatory to use 4GL-style variables. Although this depends on the nature of the code being written and the developer has the final decision on what to use when, Java primitive types are best to be used when their goal is to act as no-undoable variables and their usage is limited to a specific part of the code.

Shared Variables

The goal of a shared variable is to be available in any of the called programs, without passing it as a parameter to each and every called program.

In order to create a shared variable (which in 4GL gets declared via the DEFINE NEW SHARED statement) use the following construction:

<type> <name> = SharedVariableManager.addVariable("<name>", new <type>(<initial value>));

E.g.:

integer var = SharedVariableManager.addVariable(“var”, new integer(1));

In order to create a global shared variable (equivalent to DEFINE NEW GLOBAL SHARED statement) use the following construction:

<VarType> <varName> =
SharedVariableManager.addVariable(ScopeLevel.GLOBAL,
"<varName>", new <Var Type>(<initial value>));

In order to use an already defined shared variable (equivalent to DEFINE SHARED statement) use the following construction:

<VarType> <varName> = SharedVariableManager.lookupVariable("<varName>");

where varName is the name that was passed to the addVariable function. E.g.:

integer var = SharedVariableManager.lookupVariable(“var”);

When working with “shared variables”, it is important to note that each user session executes the business application code in an isolated context, which shares very few information with other sessions. So, if you are thinking to use static fields to share data across executed code, do not proceed, as this will not be safe in a concurrent environment. The only acceptable use is of final static fields (a.k.a constants, or static fields which always act as Java constants), which get initiated on server startup or class initialization and never get changed during server lifetime.

Context-Local Variables

When having classes which act like singletons (only one instance of this class is ever available) and context-local variables are needed, FWD provides a mechanism to define such variables. This mechanism is similar to how ThreadLocal variables work in Java, with the difference that these variables are kept per each authenticated context, and not thread. By using the ContextLocal class defined in the com.goldencode.p2j.security package, you can either define a context-local variable using this snippet:

private static final ContextLocal<type> context =
new ContextLocal<type>()
{
   protected type initialValue() { return (new type()); }
};

where type is the name of the class which is backed by this context-local variable. With this approach, only implementation for the initialValue API defined by the ContextLocal class is needed, which is executed only the first time when a new context accesses it.

To access the context-local instance, use context.get() or, if you need to modify the instance for this context, use context.set(value).

If multiple context-local variables are used, you can bundle them in a single class and define only one context-local instance:

/** Stores context-local state variables. */
private static ContextContainer work = new ContextContainer();
...
/**
 * Stores global data relating to the state of the current context.
 */
private static class WorkArea

   private String var1 = null;

   private int var2 = null;
}

/**
 * Simple container that stores and returns a context-local instance of
 * the global work area.
 */
private static class ContextContainer
extends ContextLocal
{
   /**
    * Obtains the context-local instance of the contained global work
    * area.
    *
    * @return   The work area associated with this context.
    */
   public WorkArea obtain()
   {
      return (WorkArea) this.get();
   }

   /**
    * Initializes the work area, the first time it is requested within a
    * new context.
    *
    * @return   The newly instantiated work area.
    */
   protected synchronized Object initialValue()
   {
      WorkArea wa = new WorkArea();

      // TODO: initialize our data for this session

      return wa;
   }

With this code, using work.obtain() returns the context-local WorkArea instance, which bundles together all context-local variables.

Function and Procedure Parameters

Procedure parameters are defined in 4GL using the DEFINE PARAMETER statement while function parameters are defined in-place, with no special statement. The Variable DefinitionsDEFINE VARIABLE section of the Data Types chapter of the FWD Conversion Reference book describes in full detail how this statement works and gets converted.

In Java, a method parameter defined as a primitive data type will always be passed by value, when the method is invoked. But, as the 4GL variables are implemented in FWD by using custom data types, when passing a 4GL-style variable to a method means that the parameter is passed by reference, and not value; thus, if the parameter is changed by the method, this change will be reflected in the caller code.

To solve this problem for parameters passed by value (i.e. INPUT parameters in 4GL terms), the conversion rules emit a duplicate variable, which is initialized with the value held by the method parameter. Note that, in converted code, all INPUT method parameters are prefixed with an underline, and they are used only to initialize the variable which will be actually used in the procedure/function's body (so that the variable in the caller code will not be affected). Following you can find an example of a method which accepts _txt1 and _txt2 “by value” while the other fields are passed by reference:

public void execute(final character _txt1, final integer num2, final date dat3,
                    final character _txt4, final decimal dec5, final logical log6)
{
   externalProcedure(new Block()
   {
      // these member variables are copies of input parameters that were passed in, any edits
      // will not be “visible” in the calling code

      character txt1 = new character(_txt1);

      character txt4 = new character(_txt4);

      public void init()
      {
         // all parameters are UNDO by default; each undoable variable must be registered here
         // and all NO-UNDO variables must be excluded
         TransactionManager.register(txt1, num2, dat3, txt4, log6);

         // output parameters must be initialized to their default value or to the value given
         // in an initializer option (for date variables the default is the unknown value)
         dat3.assign(date.instantiateUnknownDate());
         log6.assign(new logical(true));

         // options other than initializers
         txt4.setCaseSensitive(true);
         dec5.setPrecision(2);

         // any INPUT-OUTPUT or OUTPUT parameters that are NO-UNDO will be configured here
         TransactionManager.deregister(new Undoable[]
         {
            dec5
         });
      }

      public void body()
      {
         // all parameters can be accessed here by name as needed
      }

   });
}

Parameters are emitted the same way for both internal procedures and external procedures. The above example is of an external procedure.

When writing new Java code, the method's parameters can be mingled, so there is no restrictions in using parameters of primitive (or other non-4GL compatible) types alongside 4GL-style variables. The above constraints (related to “input parameters”) are only for 4GL-compatibiltiy.

Extents

Extents are 4GL's way of defining arrays. They can not be passed as parameters, their size is set at the variable definition and, although they are converted to Java arrays, access to them is not done directly, but via APIs in the ArrayAssigner class.

The 4GL extents should be used only in following cases:

  • undo support
  • compatibility with an extent record field
  • all elements of an extent variable need to be displayed in a frame widget.

At a first glance, you may think to use a Java array to compute and hold the data and use a 4GL extent variable to pass it to the record field or to the frame widget, on display; but, unless you are very careful where you use the 4GL extent variable reference, you may end up with corrupted data in one place, as in some cases (as when displaying data with a frame), the variable reference gets saved and the variable gets evaluated only when the frame sends the data to the screen buffer (i.e. when the data is displayed to the screen). Also, performance issues may appear if many 4GL-style extent variables are built and discarded, in a long loop.

In all other cases, it is recommended to avoid extends and use Java arrays (or one Java collection classes), as performance (and code writing) will be improved.

In the converted code (and we recommend to use the same approach in new Java code, if possible), each extent variable will be defined following these rules:

  • without initial values:
<data type>[] <extent name> = new <data type>[<extent size>];

For example:

integer[] salesPerDay = new integer[7];
  • or, if you have initial values:
<data type>[] <extent name> = new <data type>
{
   new <data type>(<initial value 1),
   new <data type>(<initial value 2), ...
};

For example:

character[] books = new character[]
{
   new character(“FWD Conversion Handbook“),
   new character(“FWD Conversion Reference“)
};

If undo support is needed, you need to register the variable for undo support, as non-undo variables get registered (see next section for details).

When checking the length of a record field of an extent type, you need to manually enter the length of this field. FWD provides no support to identify the length of an extent record. If this is an extent variable (i.e. a Java array), just use the array's length member to get its length. This is the same as how conversion works. During conversion, if the 4GL parameter for the EXTENT function is a variable with an extent > 1, then the referent will be the converted array variable and the result will be a direct reference to the array's length member. If the extent <= 1 (the variable is scalar in 4GL), then the result will be the int literal 0. If the 4GL parameter is a field with an extent > 1, then the Java result will be an int literal of the extent size as read from the schema. If the extent is <= 1 (the field is scalar), the result will be the int literal 1.

In order to change the value at a specific index in an extent variable, you must always use the static function assignSingle
defined by ArrayAssigner:

assignSingle(<extent name>, <1-based index>, <new value>);

For example:

character var = new character(“some value”)
assignSingle(extent2, 2, var);

In order to change multiple values elements in an extent variable, use the family of assignMulti functions defined by ArrayAssigner.

In order to get the 4GL-compatible value at the specified index in an extent variable (error condition will be raised if the index is out of range) use the following function defined by ArrayAssigner:

subscript(<extent name>, <index>);
UNDO Support

When a transaction or sub-transaction is backed out, this will roll back all changes to the records. Also, duplicating the 4GL behavior, FWD will undo all changes to any variable which is not marked as NO-UNDO at its definition.

To register a 4GL variable for undo support, there are two cases:

  • If the variable is not a shared variable, a TransactionManager.register(<variable>) call needs to be added in the in the Block.init method for the root/internal procedure or function block (the block to which the variable is scoped). If more than one variable needs to be registered for undo support, this API supports variable arguments, so all needed variables can be registered in a single call, as in TransactionManager.register(<var1>, <var2>, ..., <varN>).
  • when the variable is shared as global (i.e. duplicates a DEFINE NEW GLOBAL SHARED statement), a TransactionManager.register(true, var) call needs to be added Block.init method for the external/internal procedure or function block, as in:
public void execute()
{
  externalProcedure(new Block()
  {
    public void init()
    {
      TransactionManager.register(true, <variable>);
    }

    public void body()
    {
      ...
    }
  });
}

If multiple variables are needed to be register, you can pass them as a list, as in TransactionManager.register(true, <var1>, <var2>, ..., <varN>).

More details about undo support can be found in the Transaction Support section of this chapter.

Operators and Functions
Operators

Comparison of 4GL-style variables and evaluation of logical expressions must always be done by using the FWD's APIs defined in the CompareOps and logical classes. MathOps class provides APIs which implement 4GLs math operators, while operators which work on date, time or character values are implemented in the date and character class.

All APIs defined in these classes take combinations of 4GL-style variables and Java primitive types, depending on their goal. In FWD, if an API is prefixed with an underscore (_), it means that API will return a Java boolean value. In converted code, APIs which return a Java boolean value are emitted only if they are directly passed to a Java control flow statement, as an if ( ).

To ease work with these APIs, it is recommended to expose the static methods defined these classes by using Java's import static statement.

Each 4GL operator is explained in detail in the Operators section of the Expressions chapter of the FWD Conversion Reference book. This section will summarize briefly how each 4GL operator maps to its FWD API counterpart.

Precedence Level Operator Type FWD API/Operator Purpose
9 () Unary () Parenthesis is the precedence operator. It allows custom control over the order of evaluation by the programmer.
8 : Binary This operator is supported using the Java object referencing operator (the . character). Use to reference handle attributes or methods is fully supported. For details, see the Attributes, Methods and Handles section of this chapter.
Use for COM/ActiveX and 4GL object-oriented resources is not supported at this time.
7 unary - Unary MathOps.negate Unary minus is used to negate the sign of a numeric operand.
7 unary + Unary n/a Unary plus is meaningless.
6 * Binary MathOps.multiply Arithmetic multiply. Can be used on any kind of numeric data.
6 / Binary MathOps.divide Arithmetic divide. Can be used on any kind of numeric data.
6 MODULO Binary MathOps.modulo Arithmetic integer remainder. Can be used on any kind of numeric data.
5 binary - Binary MathOps.minus
date.differenceNum
date.minusDays
Arithmetic subtraction on numeric or date data.
5 binary + Binary MathOps.plus
character.concat
date.plusDays
Arithmetic numeric or date data. The binary plus also can be used for string concatenation.
4 =, EQ Binary CompareOps.isEqual
CompareOps._isEqual
CompareOps.isUnknown
CompareOps._isUnknown
Relational equality comparison. When comparing against an unknown value, the dedicated API must be used.
4 <>, NE Binary CompareOps.isNotEqual
CompareOps._isNotEqual
Relational inequality comparison.
4 <, LT Binary CompareOps.isLessThan
CompareOps._isLessThan
Relational comparison.
4 >, GT Binary CompareOps.isGreaterThan
CompareOps._isGreaterThan
Relational comparison.
4 <=, LE Binary CompareOps.isLessThanOrEqual
CompareOps._isLessThanOrEqual
Relational comparison.
4 >= GE Binary CompareOps.isGreaterThanOrEqual
CompareOps._isGreaterThanOrEqual
Relational comparison.
4 MATCHES Binary character.matches
character._matches
Pattern matching in text data.
4 BEGINS Binary character.begins
character._begins
Prefix check in a text data.
4 CONTAINS Binary n/a Only present in where clauses. For details of WHERE clause conversion, please see Part 5 of the P2j Conversion Reference book.
3 NOT Unary logical.not
logical._not
Logical negation (the result is the opposite of how its operand evaluates).
2 AND Binary logical.and
logical._and
Logical conjoin (the result is only true If both operands evaluate true).
1 OR Binary logical.or
logical._or
Logical disjoin (the result is true If either operand evaluates true).

When assigning a value to a 4GL-style variable in Java, you can not use Java's assignment operator: this will make the variable point to a difference reference and, as 4GL-style variable are mutable, you will end up with more than a variable pointing to the same 4GL-style value. FWD doesn't forbid variables to change their reference, but you need be aware of the implications when making such assignments. For 4GL compatibility, the variable must change its value using an assign call, like in:

<variable>.assing(<value>);

where <variable> is a Java variable of any 4GL-style base data type and <value> is another 4GL-style variable, expression or value which evaluates to the variable's base type.

The IF THEN ELSE ternary operator converts to the Java's ternary ? : operator. The condition will be checked (and unwrapped using booleanValue() if needed). Then the return value will be wrapped into a common BaseDataType for the THEN and ELSE expressions. The wrapper type will always be the same type for both return expressions:

condition ? new wrapper(expr1) : new wrapper(expr2)

The ASSIGN statement has a special purpose in FWD, which is explained in detail in the ASSIGN Statement section of the Data Types chapter of the FWD Conversion Reference book.

Math Functions

Mathematical, string and date related functions are split between the MathOps class and the FWD's numeric wrappers. The following table is a list of all math functions supported by FWD:

4GL Function FWD API Details
ABSOLUTE MathOps.abs(value) Returns an integer or decimal value depending on the type of the input parameter.
EXP MathOps.pow(base, exponent)  
LOG MathOps.log(num[, base])  
MAXIMUM character.maximum()
date.maximum()
logical.maximum() integer.maximum() decimal.maximum()
This is a static method call using Java's variable argument lists (varargs). The result data type will be the same as the class used for the calculation. If all the numeric operands are of one type, then that type will be used for the calculation. For example, if all operands are integer then integer is used to calculate the result and the result will be of type integer. Numeric operands can be intermixed (decimal and integer), but the decimal class will be used to calculate the result. All arguments (other than numerics) can only be compared against others of the same type.
MINIMUM character.minimum()
date.minimum()
logical.minimum() integer.minimum() decimal.minimum()
This is a static method call using Java's variable argument lists (varargs). The result data type will be the same as the class used for the calculation. If all the numeric operands are of one type, then that type will be used for the calculation. For example, if all operands are integer then integer is used to calculate the result and the result will be of type integer. Numeric operands can be intermixed (decimal and integer), but the decimal class will be used to calculate the result. All arguments (other than numerics) can only be compared against others of the same type.
RANDOM MathOps.random(low, high) It is possible to force Progress to always generate the same sequence of random numbers in every session! This sounds like an awful idea but perhaps it is being used. The FWD random implementation does not match that behavior.
ROUND Math.round(value[, precision])  
SQRT MathOps.sqrt(num)  
TRUNCATE MathOps.truncate(value, precision)  
Character and Raw/Memory Access Functions

FWD's character wrapper provides functions which work with character, memptr or raw types:

4GL Function FWD API Details
CAPS character.toUpperCase(text) It can contain DBCS but only the SBCS chars are uppercased.
COMPARE character.compare(text1, operator, text2, strength) There is no collation table support at this time. The result in a DBCS environment may vary from the Progress implementation.
ENTRY character.entry(index, list[, delim]) Delimiter processing is *always* case-sensitive at this time.
FILL character.fill(txt, num)  
GET-BYTE referent.getByte() This instance method will be called on an instance of BinaryData.
GET-SIZE referent.length() This instance method will be called on an instance of memptr.
GET-STRING referent.getString() This instance method will be called on an instance of BinaryData.
INDEX character.indexOf(src, target[,startIdx])  
LC character.toLowerCase(txt) It can contain DBCS but only the SBCS chars are lowercased.
LEFT-TRIM character.leftTrim(txt, list)  
LENGTH character.length()
OR
character.byteLength()
OR
BinaryData.length()
If a character value is passed as the first parameter, then the static method call will be made to character.length() by default (or if the “character” type is passed as the 2nd parameter). Otherwise, a static method call to character.byteLength() will be made for “raw” type. There is no support for "column" based length at this time.

If a raw value is passed for the first parameter, a static method call to BinaryData.length() will be made.

There is no support for the BLOB type at this time.
LOOKUP character.lookup(txt, list[, delim]) Delimiter processing is *always* case-sensitive at this time.
NUM-ENTRIES character.numEntries(list[, delim]) Delimiter processing is *always* case-sensitive at this time.
R-INDEX character.lastIndexOf(src, target[, idx])  
REPLACE character.replaceAll(src, from, to[, caseSens])  
RIGHT-TRIM character.rightTrim(txt, list)  
SUBSTITUTE character.substitute(src, pos, len[, type[, caseSens]]) The variable length argument list is handled by an argument of type Object[].
SUBSTRING character.substring(src, pos, len[, type[, caseSens]])) No support for "raw", "fixed" or "column" based substrings at this time.
TRIM character.trim(txt, list[, caseSens)  
Date/Time Functions

All date and time functions are defined by the FWD's date wrapper:

4GL Function FWD API Details
DATE new date() The constructors are overloaded to support the parameters for all the different forms of input to the function.
DAY date.day()  
MONTH date.month()  
TODAY new date()  
WEEKDAY date.weekday()  
YEAR date.year()  
ETIME date.elapsed()  
TIME date.secondsSinceMidnight()  
Type Conversion
4GL Function FWD API Details
ASC character.asc() There is no source or target codepage support at this time. The result in a DBCS environment may vary from the Progress implementation.
CHR character.chr() There is no source or target codepage support at this time. The result in a DBCS environment may vary from the Progress implementation.
DECIMAL new decimal() The constructors are overloaded to support the parameters for all the different forms of input to the function.
INTEGER new integer() The constructors are overloaded to support the parameters for all the different forms of input to the function.
STRING character.valueOf() Generates the string representation of the given value.

Coding the User Interface

When coding the user interface in Java, until the developer has a chance to master all FWD APIs, it is easier to code the user interface (frame, display statement, etc) in 4GL, convert the file and adjust and use the generated Java code as needed. This provides the developer a better insight on how the 4GL statements are converted, what APIs are emitted and also a faster way to write UI code.

When implementing the UI, the following chapters from the FWD Conversion Reference book are of interest, to understand how the FWD runtime handles various UI statements, how the converted code looks like and how the APIs work:

  • Frames chapter, for full details about how the 4GL frames are implemented in FWD.
  • Displaying Data Using Frames chapter, for details about statements which display data in a frame are handled by FWD conversion and runtime.
  • Editing chapter, for understanding how the frames can be used for input purposes.
  • Events chapter, for details about how the 4GL-style events are handled by FWD during conversion and runtime.
  • Validation chapter, for details about how input data can be validated, when reading using a frame.
  • Terminal Management chapter, for details about how the terminal is accessed by FWD.
Defining Frames and Widgets

The Frame Conversion section of the Frames chapter of the FWD Conversion Reference book provides full details about how the frame widgets are collected from the 4GL code and how the frame is defined in Java code. This section will focus on what the developer should be aware of when implementing a frame definition from scratch.

In FWD, each frame has two parts:

  • an interface which extends the CommonFrame interface, where APIs to set or get the value of a widget are defined. Also, this interface provides the APIs available to the FWD runtime when handling the frame and to the user, when accessing widgets. Access to the methods defined by this interface will be proxied via a GenericFrame handler.
  • a frame definition class, which setups the widget and frame level properties.

When writing a new frame definition, the following table maps the 4GL-style widgets with their FWD counterparts:

4GL Widget FWD Class (com.goldencode.p2j.ui package)
fill-in FillInWidget
editor EditorWidget
button ButtonWidget
combo-box ComboBoxWidget
static values (displayed constants) ControlTextWidget
browse BrowseWidget
BrowseColumnWidget
radio-set RadioSetWidget
selection-list SelectionListWidget
skip SkipEntity (vertical = true)
space SkipEntity (vertical = false)

The general structure of a frame definition can be represented this way:

public interface <frame class name>
extends CommonFrame
{
   public static final Class configClass = <frame class name>Def.class;

   // For each displayed ComboBoxWidget, EditorWidget, FillInWidget, RadioSetWidget
   // and SelectionListWidget we should define value getters, three setters and
   // widget getter.
   // ControlTextWidget, BrowseWidget and BrowseColumnWidget require three
   // setters and widget getter.
   // ButtonWidget requires only widget getter.
   // SkipWidget doesn't require these declarations at all.
   public integer get<widget name>();
   public void set<widget name>(<4GL-compatible data type> parm);
   public void set<widget name>(<Java-compatible data type> parm);
   public void set<widget name>(BaseDataType parm);
   public <widget type> widget<widget name>();

   public static class <frame class name>Def
   extends WidgetList
   {
      <widget type> <widget name> = new <widget type>();

      public void setup(CommonFrame frame)
      {
         // Here frame and widget setup is performed.
      }

      {
         addWidget("<widget name>", <widget name>);
      }
   }
}

Following this structure, if we define a simple 4GL frame with an integer fill-in and a static text element, as in:

def var num as integer format "999".
form num label "some label" 
     "some text" at 1
     with frame simpleFrame side-labels centered size 30 by 10.

when writing its definition from scratch the code will look like:

public interface SimpleFrame
extends CommonFrame
{
   // Always define a constant field which specifies the frame definition class – this field
   // will be read and will use the specified class as frame definition class.
   public static final Class configClass = SimpleFrameDef.class;

   // Definitions for num fill-in:
   // getter which returns the widget's value as a 4GL-style wrapper
   public integer getNum();
   // various setters (all must be defined)
   public void setNum(integer parm);
   public void setNum(int parm);
   public void setNum(BaseDataType parm);
   // getter to access the widget itself
   public FillInWidget widgetNum();

   // Definitions for “some text”.
   public void setSomeText(character parm);
   public void setSomeText(String parm);
   public void setSomeText(BaseDataType parm);
   public ControlTextWidget widgetSomeText();

   public static class SimpleFrameDef
   extends WidgetList
   {
      // an instance field is needed for each widget
      FillInWidget num = new FillInWidget();

      ControlTextWidget someText = new ControlTextWidget();

      public void setup(CommonFrame frame)
      {
         // --- Frame setup ---
         // The frame is not DOWN frame, so its down value is 1.
         frame.setDown(1);

         // Converted SIDE-LABELS annotation.
         frame.setSideLabels(true);

         // Converted CENTERED annotation.
         frame.setCentered(true);

         // Converted SIZE 30 BY 10 annotation.
         frame.setWidth(30);
         frame.setHeight(10);

         // --- Num fill-in setup ---
         // It is obligatory to define a widget data type.
         num.setDataType("integer");

         // The label which corresponds the widget. It can be defined into
         // DEF VAR … LABEL statement or (re)defined into frame definitions.
         num.setLabel("some label");

         // Set this flag if the label was defined into frame definitions
         // rather than into variable definitions.
         num.setForceLabel(true);

         // The format which corresponds the widget. It can be defined into
         // DEF VAR … FORMAT statement or (re)defined into frame definitions.
         num.setFormat("999");

         // --- Num fill-in setup ---
         someText.setDataType("character");
         someText.setFormat("x(9)");

         // Set this flag if the widget represents a constant rather than
         // a modifiable widget.
         someText.setStatic(true);
    {
         addWidget("find", "find", find);

         // Converted AT 1 annotation.
         someText.setColumn(1);

         // Set the static value.
         ((SimpleFrame) frame).setSomeText(new character("some text"));
      }

      {
         // the order in this list will affect how the widgets are displayed by the frame
         addWidget("num", num);
         addWidget("someText", someText);
      }
   }
}

When setting frame and widget properties, for better reading and maintenance you can either:

  • group all frame and widget setup code by frame/widget
    or
  • group all setup code by property being set

Details about each widget property can be found in the Frame Conversion → Widget Properties Conversion section of the Frames chapter from the FWD Conversion Reference book. This section will continue with a resume of the most common widget and frame APIs which map to 4GL-level widget and frame properties.

From the CommonFrame class, the following table shows the most important APIs which map to their 4GL counterparts:

4GL Frame Property CommonFrame API
COLUMN x setColumn(x)
ROW x setRow(x)
CENTERED setCentered(true)
x DOWN setDown(x)
NO-BOX setNoBox(true)
NO-LABELS setNoLabels(true)
SIDE-LABELS setSideLabels(true)
OVERLAY setOverlay(true)
WIDTH x setWidth(x)
SIZE x BY y setWidth(x)
setHeight(y)
TITLE string setTitle(string)
TITLE variable use <frame instance>.setDynamicTitle(variable) just after the scope of this frame has been opened, to set the dynamic title

When setting widget properties, their most common APIs are defined in the GenericWidget and BaseEntity classes:

4GL Widget Property FWD API
AT x setColumn(x)
AT COLUMN x ROW y setColumn(x)
setRow(y)
VIEW-AS TEXT use ControlTextWidget instead of FillInWidget or EditorWidget
FORMAT ftm setFormat(fmt)
LABEL lbl setFormat(lbl)
NO-LABELS setNoLabels(true)
HELP string setHelp(string)
VALIDATE (condition, msg-exception) see validation section of this chapter
SIZE x BY y setWidthChars(x)
setHeightChars(y)

For combo-box widgets, its widget implementation class provides these APIs:

4GL Widget Property ComboBoxWidget API
LIST-ITEM-PAIRS label1, value1, label2, value2, …. setItems(new ControlSetItem[]
{
new ControlSetItem(label1, value1),
new ControlSetItem(label2, value2),
...
});
LIST-ITES value1, value2, …. setItems(new ControlSetItem[]
{
new ControlSetItem(value1, value1),
new ControlSetItem(value2, value2),
...
});
INNER-LINES lines setInnerLines(lines)
SORT setSort(true)

For selection-list widgets, its implementation class provides these APIs:

4GL Widget Property SelectionListWidget API
INNER-CHARS chars setInnerChars(chars)

For radio-set-specific functions:

4GL Widget Property RadioSetWidget API
RADIO-BUTTONS label1, value1, label2, value2, ... setItems(new ControlSetItem[]
{
new ControlSetItem(label1, value1),
new ControlSetItem(label2, value2),
...
});
HORIZONTAL setHorizontal(true)

For skip and space widgets, its implementation class provides these APIs:

4GL Widget Property SkipWidget API
SKIP (n) setVertical(true); setHeight(n)
SPACE (n) setVertical(false); setWidth(n)
Template Frames

When writing code in FWD, it is possible to define a frame mockup, and use Java inheritance to set other specifics. This section will present two examples of how to define mockup frames and how to use them: one defines a frame with a “generic” field, which has its type, label and format set by the implementation and another one which allows the implementation to remove widgets dynamically from a “standard” frame.

Search Frame

Assume we need to provide a search facility in various application screens, which uses different data as input. One could be formatted as a date, one could be a numeric field or a character field following a certain format. In this case, the starting point is to determine what is the common code for all this frames. Looking at the frame definition structure provided earlier, we can say for sure that:

  • each child frame definition class must extend the base frame definition class.
  • each child frame interface must extend the base frame interface.
  • each interface for a child frame must have its own configClass field defined (which points to the child frame definition class).
  • Java getters can't be overwritten or overridden, so they need to be defined in each child interface.
  • the setters can be defined either in the base interface or the child interface. Even if the base interface has more than the needed setters, the FWD runtime will use only the needed ones, depending on the widget's type.

Extrapolating the above notes, we can say that the base interface and frame definition class must have:

  • the frame interface must have defined setters for all fields.
  • the frame definition class should have a c'tor which gets as parameters the widget type, label and format.
  • the frame definition class must have code to define all widgets and setup them.

For our specific case, we need a frame with a single field, which accepts different data. So, we name this field search and assume that we need only character and numeric data to be input. The from the base interface and frame definition class which will look like:

public interface BaseFrame
extends CommonInterface
{
   public void setSearch(BaseDataType parm);

   public FillInWidget widgetSearch(); // common getter

   /* plus setters for each needed data type */
   public void setSearch(character parm);
   public void setSearch(string parm);
   public void setSearch(int parm);
   public void setSearch(integer parm);

   public static class BaseFrameDef
   extends WidgetList
   {
      FillInWidget search = new FillInWidget();

      public BaseFrameDef(String type, String label, String format)
      {
         search.setDataType(type);
         search.setLabel(label);
         search.setFormat(type);
      }

      public void setup(CommonFrame frame)
      {
         // Here frame and widget setup is performed.
      }

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

Having the base interface and frame definition class, each child frame should look like:

// first frame definition, for a string search
public interface SearchByStringFrame
extends BaseFrame
{
   public static final Class configClass = SearchByStringFrameDef.class;

   public character getSearch();

   public static class SearchByStringFrameDef
   extends BaseFrameDef
   {
      public SearchByStringFrameDef()
      {
        super(“character”, “Text”, “x(10)”);
      }
   }
}

// second frame definition, for an int search
public interface SearchByIntFrame
extends BaseFrame
{
   public static final Class configClass = SearchByIntFrameDef.class;

   public integer getSearch();

   public static class SearchByIntFrameDef
   extends BaseFrameDef
   {
      public SearchByIntFrameDef()
      {
        super(“character”, “Int value”, “>>>”);
      }
   }
}

Using the described approach, the user can extend the functionality of one frame definition with simple changes, not having to re-write the entire frame definition code.

Dynamic Frame

This section will describe how a frame definition code can be reused using Java inheritance, so that some widgets can be dynamically removed (or re-arranged). The first issue to be aware is that the widgets, although they be defined in the frame definition class or interface, they are not seen by the runtime until they are added to the widget list, using addWidget calls. So, assume we have this frame, with three widgets (not all code is show here):

public interface SimpleFrame
extends CommonFrame
{
   public static final Class configClass = SimpleFrameDef.class;

   public FillInWidget widgetNum();
   public ControlTextWidget widgetSomeText();
   public FillInWidget widgetSomeDate();
   // the other getters and setters are omitted

   public static class SimpleFrameDef
   extends WidgetList
   {
      FillInWidget num = new FillInWidget();
      ControlTextWidget someText = new ControlTextWidget();
      FillInWidget date = new FillInWidget();

      public void setup(CommonFrame frame)
      {
         // frame and widget setup code is omitted
      }

      {
         // the order in this list will affect how the widgets are displayed by the frame
         addWidget("num", num);
         addWidget("someText", someText);
         addWidget("date", date);
      }
   }
}

From the frame definition above, the last lines of code are of interest:

      {
         // the order in this list will affect how the widgets are displayed by the frame
         addWidget("num", num);
         addWidget("someText", someText);
         addWidget("date", date);
      }

These lines are executed by the default c'tor, when an instance of this class is created. So, this code is responsible for the registering the widgets and providing the basic layout for the frame.

If we decide to create another frame which lets say doesn't have the date widget, but instead adds another widget, then:

  • the child frame interface must:
  • extend the base frame interface.
  • have its own configClass field defined (which must point to the child frame definition class).
  • define getters and setters for the new widget
  • the child frame definition class must:
  • extend the base frame definition class.
  • define an instance field with the new widget.
  • overwrite the setup(CommonFrame) method to setup the widget and frame properties.
  • add only the needed widgets to the list.

Using the SimpleFrame frame, the resulted code (with the date widget removed and another, time widget added) will look like:

public interface SecondFrame
extends CommonFrame
{
   public static final Class configClass = SecondFrameDef.class;

   public FillInWidget widgetTime();
   // the other getters and setters are omitted

   public static class SecondFrameDef
   extends WidgetList
   {
      FillInWidget time = new FillInWidget();

      public void setup(CommonFrame frame)
      {
         super.setup(frame); // calls the setup method in the base frame definition class,
                             // allowing us to only adjust the base frame as needed

         // add code to setup the time widget and/or to setup the frame and other widgets
      }

      {
         // this is the most important part; we add another base default c'tor, so that only
         // our needed widgets are added.
         // clear the widget list, as the base default c'tor has already been called
         getWidgets().clear();
         getNames().clear();

         addWidget("num", num);
         addWidget("someText", someText);
         addWidget("time", time);
      }
   }
}

Although the above example is trivial, in complex situations this helps in not having to duplicate the same frame code in multiple files. When using the SecondFrame in Java code, the FWD runtime will set it up as if the removed widgets existed - it will automatically see only the the info provided for the widgets which were registered via the addWidget calls, in the default c'tor.

Defining Browse Widget

A 4GL-style browse widget is a widget which needs special attention in FWD. Full details about how a browse is converted by FWD can be found in the Frame ConversionDEFINE BROWSE Statement section of the Frames chapter of the FWD Conversion Reference book.

Adding a browse widget to a frame is similar as for normal widgets, considering that you need to define each column as a BrowseColumnWidget, with its definition looking like this:

public interface SomeFrame
extends CommonFrame
{
   public BrowseWidget widget<browse name>();

   public BrowseColumnWidget widget<column name 1>();

   public BrowseColumnWidget widget<column name 2>();

   ...

   public static class SomeFrameDef
   extends WidgetList
   {
      BrowseWidget <browse name> = new BrowseWidget();

      BrowseColumnWidget <column name 1> = new BrowseColumnWidget();

      BrowseColumnWidget <column name 2> = new BrowseColumnWidget();

      ...

      public void setup(CommonFrame frame)
      {
         ....

         // Set up browse widget properties.

         <browse name>.addColumn(<column name 1>);
         // Set up column 1 properties here.

         <browse name>.addColumn(<column name 2>);
         // Set up column 2 properties here.

         ....
      }

      {
         addWidget("<browse name>", <browse name>);
         addWidget("<column name 1>", <column name 1>);
         addWidget("<column name 2>", <column name 2>);
         ...
      }
   }
}

Note how only widget getters are needed for the browse and column widgets. For example, one would define a simple browse like this:

public interface SimpleFrame
extends CommonFrame
{
   ...

   public BrowseWidget widgetBrws();

   public BrowseColumnWidget widgetBrwsBookIdColumn();

   public BrowseColumnWidget widgetBrwsBookTitleColumn();

   public static class SimpleFrameDef
   extends WidgetList
   {
      BrowseWidget brws = new BrowseWidget();

      BrowseColumnWidget brwsBookIdColumn = new BrowseColumnWidget();

      BrowseColumnWidget brwsBookTitleColumn = new BrowseColumnWidget();

      ...

      public void setup(CommonFrame frame)
      {
         ...

         // Set up general browse properties.
         brws.setColumn(1);
         brws.setWidth(50);
         brws.setDown(3);

         // Add “book id” column and set up its properties.
         // It is obligatory to define column data type.
         brws.addColumn(brwsBookIdColumn);
         brwsBookIdColumn.setDataType("integer");
         brwsBookIdColumn.setFormat("999");
         brwsBookIdColumn.setColumnLabel("Book ID");

         // Add “book title” column and set up its properties.
         brws.addColumn(brwsBookTitleColumn);
         brwsBookTitleColumn.setDataType("character");
         brwsBookTitleColumn.setFormat("x(35)");
         brwsBookTitleColumn.setNoLabels(true);
         brwsBookTitleColumn.setEditable(true);
      }

      {
         addWidget("brws", brws);
         addWidget("brwsBookIdColumn", brwsBookIdColumn);
         addWidget("brwsBookTitleColumn", brwsBookTitleColumn);
         ...
      }
   }
}

The browse widget implementation class is BrowseWidget, and the most common APIs are:

4GL-style Browse Property BrowseWidget API
x DOWN setDown(x)
SINGLE setSingle(true)
MULTIPLE setMultiple(true)
NO-ROW-MARKERS setNoRowMarkers(true)
NO-LABELS setNoLabels(true)
NO-BOX setNoBox(true)
TITLE string setTitle(string)
TITLE variable use <frame>.<browse widget getter>().setDynamicTitle(variable) just after the scope of this frame has been opened, to set a dynamic browse widget title.
SEPARATORS setSeparators(true)

For each browse column, the most common APIs are:

4GL-style Browse Column Property BrowseColumnWidget API
ENABLE column-list call setEditable(true) for each needed column
AUTO-RETURN setAutoReturn(true)
Creating and Displaying Frames

When using a frame, access to it is done using an anonymous proxy obtained by a GenericFrame.createFrame call, the user having access only to the APIs defined by the frame's interface or defined by the CommonFrame interface. Each frame definition will look like:

<frame interface> <name> = GenericFrame.createFrame(<frame interface>.class, “<frame name>”);

and it can be defined either as an instance field for the external procedure class or as an instance field for the Block implementation class where it is used.

For example:

SimpleFrame simpleFrame =  GenericFrame.createFrame(SimpleFrame.class, “simpleFrame”);

Before using a frame to display or read data, it should be scoped to a certain block. The Frame Runtime SupportFrame Scope And Life Time section of the Frames chapter of the FWD Conversion Reference book provides full details about how the frame must be scoped in the 4GL-compatible code. To resume, to scope a frame one needs to:

  • add a frame.openScope() call at the beginning of the body() method of the procedure/function block.
  • add a frame.openScope() in the Block.init() method., if the frame is scoped to an internal block.

Details about how to display data using a frame can be found in the Displaying Data Using Frames chapter of the FWD Conversion Reference book. To resume, the construct to display data using a frame is:

FrameElement[] elementList = new FrameElement[]
{
   new Element(<variable / expression / field reference to display> ,
           <frame>.<widget getter>()),
   ....
};

<frame>.display(elementList);

Note that you can reuse the elementList - once created, you can save it in some instance field and re-use it as necessary, as long as the displayed elements suit your need (i.e. attention needs to be payed for variables passed by value, as changes to them will not be reflected in the Element instance).

Passing null elements to the element array will skip that element. Thus, if you want to display/enable/etc widgets depending on some condition, just set the element at the index to null instead of the Element instance, as in:

FrameElement[] elementList = new FrameElement[]
{
   _isGreaterThan(i, 0) ? new Element(i, sampleFrame.widgeti()) : null,
   new Element(j, sampleFrame.widgetj())
};
sampleFrame.display(elementList);

Normally, you will display variables at their corresponding widgets (i.e. something like new Element(someVariable, frame.widgetSomeVariable())), but you can display any expression at any widget, which is equivalent to 4GL's base-field feature. An example of such case is:

FrameElement[] elementList = new FrameElement[]
{
   // displaying variable
   new Element(num1, someFrame.widgetNum1()),
   // displaying expression
   new Element(plus(1, num2), someFrame.widgetNum2()),
   // displaying record fields (FieldReference allows to access the value of
   // the specified field of the given buffer on demand)
   new Element(new FieldReference(book, "bookId"), someFrame.widgetBookId())
};

someFrame.display(elementList);

If a certain frame needs to display its data in many code locations, is better to create a dedicated Java method which will be called in all places where frame display is needed.

Disabling or enabling specific widgets in a frame is done similar to displaying them, with the difference that different APIs are used - GenericFrame.enabled and GenericFrame.disable. This APIs accept as parameter an array of FrameElement instances, similar to GenericFrame.display.

More details about how data is displayed using a frame can be found in the Displaying Statements section of the Displaying Data Using Frames chapter of the FWD Conversion Reference book.

Reading Data Using Frames

Reading data using the GenericFrame's update, set or promptFor APIs is done using a similar construct as when data is displayed - a FrameElement array is received as parameter, which is passed the record field or variable where the data is supposed to be saved and the widget where the user inputs the data.

The PROMPT-FOR, SET and UPDATE Statements section of the Editing chapter of the FWD Conversion Reference book provides full details about how the statements which read data using frames are treated by FWD. The notes related to the FrameElement array related in the previous section, when displaying data, apply to these statements too.

Displaying extents

Extent fields need special attention when used with a frame. For this, it is easier to write the frame definition in 4GL code, convert the code and use the converted code as necessary, but, to resume, one needs to be aware of the following when writing new code by hand:

  1. N widgets need to be defined, of the same type as the extent variable (where N is the extent size). Usually it is fill-ins, but it also may be editors, combo-boxes, radio-sets or selection lists. There is a common practice to name widgets: <extent name>Array<index>, e.g. numArray0.
  2. Define four additional getter for each extent:
    public <data type> get<extent name>Array(NumberType parm);
    public <data type> get<extent name>Array(double parm);
    public <widget type> widget<extent name>Array(NumberType parm);
    public <widget type> widget<extent name>Array(double parm);
    

    This is required for executing statements like DISPLAY num[i] ..., to provide access to the element at the needed position in the extent field.
  3. Define a widget (and associated setters/getters) for each extent element, as is usually done for a normal widget.
  4. Map each element widget to an extent index, considering that numbering starts from 1, using widget.setIndex calls.

As the frame definition is static in 4GL (you can't add or remove widgets, once the frame is built), you need to be careful to synchronize the length of the extent variable which is use to display/read data to the number of widgets defined in the frame. If the count is different, it will result in unexpected behavior.

Here is a sample frame which contains the extent str with two character elements:

public interface TestSampleFrame
extends CommonFrame
{
   public static final Class configClass = TestSampleFrameDef.class;

   /* Common extent access functions */
   public character getStrArray(NumberType parm);
   public character getStrArray(double parm);
   public FillInWidget widgetStrArray(NumberType parm);
   public FillInWidget widgetStrArray(double parm);

   /* First element */
   public character getStrArray0();
   public void setStrArray0(character parm);
   public void setStrArray0(String parm);
   public void setStrArray0(BaseDataType parm);
   public FillInWidget widgetStrArray0();

   /* Second element */
   public character getStrArray1();
   public void setStrArray1(character parm);
   public void setStrArray1(String parm);
   public void setStrArray1(BaseDataType parm);
   public FillInWidget widgetStrArray1();

   public static class TestSampleFrameDef
   extends WidgetList
   {
      FillInWidget strArray0 = new FillInWidget();
      FillInWidget strArray1 = new FillInWidget();

      public void setup(CommonFrame frame)
      {
         frame.setDown(1);
         strArray0.setIndex(1);
         strArray0.setDataType("character");
         strArray1.setIndex(2);
         strArray1.setDataType("character");
         strArray0.setLabel("str[1]");
         strArray1.setLabel("str[2]");
      }

      {
         addWidget("strArray0", "str", strArray0);
         addWidget("strArray1", "str", strArray1);
      }
   }
}
Performing Validation

The Validation chapter of the FWD Conversion Reference book provides full details about how input data can be validated, when reading using a frame. Following summarizes what the developer should be aware of when writing code to validate a widget input:

private class <validator class name> // inner class
extends Validator
{
   public logical validateExpression(BaseDataType _newValue_)
   {
      // perform validation here and return logical true if validation was
      // successfully passed
   }

   public character validateMessage(BaseDataType _newValue_)
   {
      // return an appropriate validation error message
   }
}
...

// call this just after openScope() was called for the target frame
<frame instance name>.<widget getter>().
                                  setValidatable(new <validator class name>());

For example, the following inner class can be defined for the external procedure class:

private class NumValidator
extends Validator
{
   public logical validateExpression(BaseDataType _newValue_)
   {
      final integer _num = (integer) _newValue_;
      return isLessThan(_num, 10);
   }

   public character validateMessage(BaseDataType _newValue_)
   {
      return new character("num should be less than 10!");
   }
}
...

someFrame.widgetNum().setValidatable(new NumValidator());
Working with Key Presses

The key-related APIs are split between the KeyReader and the LogicalTerminal classes. Full details about how they are handled by FWD can be found in the Key Processing section of the Terminal Management chapter of the FWD Conversion Reference book.

The following table shows the APIs provided by these classes, and the handled 4GL statement or function:

4GL Statement/Function FWD API
READKEY [n] KeyReader.readKey([n])
LASTKEY KeyReader.lastKey()
KEYFUNCTION(keycode) LogicalTerminal.keyFunction(keycode)
KEYCODE(keylabel) LogicalTerminal.keyCode(keylabel)
KEYLABEL(keycode) LogicalTerminal.keyLabel(keylabel)
ON key-label key-function remapKey(key-label, key-function)
Using Triggers

4GL-style triggers are fully explained by the trigger-related sections from the Events chapter and in the Top Level BlocksTriggers section of the Blocks chapter of the FWD Conversion Reference book. This section just summarizes the most important facts when coding a trigger.

The definition of a trigger is explained in the Defining Triggers section of this chapter. Trigger registration is done using LogicalTerminal.registerTrigger APIs, as in:

public class <trigger class> // inner class
extends Trigger
{
   public void body()
   {
      // trigger code
   }
}
...
EventList list = new EventList();
list.addEvent(<event 1 name(s)>, <widget(s) associated with the event 1>);
list.addEvent(<event 2 name(s)>, <widget(s) associated with the event 2>);
...
registerTrigger(list, <trigger>.class,
                <class of the current external procedure>.this);

You can specify a single event or a list of events. In FWD event names are represented by strings and you should type them in the same way as in 4GL: “x” (for a key press), “entry”, “choose” etc.

The following table shows how possible cases are mapped to FWD:

4GL's ON Trigger EventList.addEvent API Call
ON …. ANYWHERE addEvent(... , true) – use addEvent with has boolean parameter anywhere
ON … OF CURRENT-WINDOW addEvent(..., LogicalTerminal.currentWindow())
ON … OF FRAME f addEvent(..., f.asWidget())
ON … OF widget [IN FRAME f] addEvent(..., f.<widget getter>())
ON event1, event2 … OF ... addEvent(new String[]
{
"event1",
"event2",
....
}, ...);
ON … OF widget1 [IN FRAME f1],
widget2 [IN FRAME f2], ...
addEvent(..., new GenericWidget[]
{
f1.<widget1 getter>(),
f2.<widget2 getter>(),
...
});
ON … OR … OR ... list.addEvent(...);
list.addEvent(...);
...

An example of binding triggers to events:

public class TriggerBlock0   // there are also trigger blocks 1 and 2 defined
extends Trigger
{
   public void body()
   {
      // some actions
   }
}
...

// ON a ANYWHERE
EventList list0 = new EventList();
list0.addEvent("a", true);
registerTrigger(list0, TriggerBlock0.class, Proc.this);

// ON d, go OF FRAME f1 OR  d OF num1 IN FRAME f1
EventList list1 = new EventList();
list1.addEvent("d", f1.widgetNum1());
list1.addEvent(new String[]
{
   "d",
   "go" 
}, f1.asWidget());
registerTrigger(list1, TriggerBlock1.class, Proc.this);

// ON f, g OF num1 IN FRAME f1, num2 IN FRAME f1
EventList list2 = new EventList();
list2.addEvent(new String[]
{
   "f",
   "g" 
}, new GenericWidget[]
{
   f1.widgetNum1(),
   f1.widgetNum2()
});
registerTrigger(list2, TriggerBlock2.class, Proc.this);
Wait-For Statement

The 4GL's WAIT-FOR statement is pretty complex in how it handles events, and is fully explained by the related sections part of the Events chapter of the FWD Conversion Reference book. This section just summarizes the important parts of how this statement is used.

The general WAIT-FOR structure is:

LogicalTerminal.waitFor(<target frame>, <event list>[, <focused widget>]
                        [, <pause in seconds>]);

where:

  • target frame - the frame for which this request came or null if the currently focused frame should be used.
  • event list - the list of events to wait for. Built in the same way as event lists for triggers, except that you cannot use ANYWHERE option.
  • focused widget - widget which is to be in focus when wait-for starts.
  • pause - maximum time in seconds to wait for event.

For example:

// WAIT-FOR x OF num1 IN FRAME f1 FOCUS num1 IN FRAME f1 PAUSE 10.
EventList list = new EventList();
list.addEvent("x", f1.widgetNum1());
waitFor(f1, list, f1.widgetNum1(), 10);

Some useful handlers (defined into LogicalTerminal) are self() and focus(), which map the SELF and FOCUS functions in 4GL

Applying events

Applying explicit events is done using apply API calls defined by the LogialTerminal, CommonFrame or GenericWidget. Here are the most some common apply versions:

APPLY Statement FWD API Call
APPLY event LogicalTerminal.apply(event)
APPLY event TO handle <handle function>().apply(event)
where <handle function> is the LogicalTerminal's self() or focus() API
APPLY event TO FRAME f f.apply(event)
APPLY event TO widget [IN FRAME f] f.<widget getter>().apply(event)

And some examples:

// APPLY LASTKEY.
apply(lastKey());

// APPLY "entry" TO num1 IN FRAME f.
f.widgetNum1().apply("entry");

// APPLY "go" TO FRAME f.
f.apply("go");

// APPLY "x" TO SELF.
self().apply("x");
Messaging and Pausing Functions

The Message Area and PAUSE Statement sections of the Terminal Management chapter of the FWD Conversion Reference book handle conversion details about how the 4GL's PAUSE and MESSAGE statements get converted. This section summarizes the most important APIs provided by FWD.

The PAUSE statement is mapped by various APIs defined by the LogicalTerminal class, which should be statically imported:

4GL Statement LogicalTerminal API
PAUSE [seconds] [MESSAGE msg] pause([seconds] [, msg])
PAUSE [seconds] NO-MESSAGE pause([seconds] , null)
PAUSE [seconds] BEFORE-HIDE [MESSAGE msg] pauseBeforeHide([seconds] [, msg])
PAUSE [seconds] BEFORE-HIDE NO-MESSAGE pauseBeforeHide([seconds] , null)

Note that while 4GL you could use only constant messages in the PAUSE MESSAGE statement, in FWD you are free to use any statement that will return a string. E.g.:

pause(character.concat("Current user: ", usr));

The MESSAGE statement is mapped by LogicalTerminal APIs too:

4GL Statement LogicalTerminal API
MESSAGE [COLOR col] expression message(expression [, col])
MESSAGE [COLOR col] expression
{ SET | UPDATE}
field [FORMAT fmt] [AUTO-RETURN]
message(expression, <set>, <field accessor>, <auto> [, fmt] [, col])
where:
     set is true for SET, false for UPDATE clauses
     <field accessor> is an AccessorWrapper or FieldReference instance.
     auto is true for AUTO-RETURN clause

As expression you can specify a string or a list of objects with their string representation being concatenated (with interleaving spaces) before displaying.

Color specifications are instances of ColorSpec class, where its c'tor can take the following color styles:

COLOR clause ColorSpec Instance
COLOR NORMAL new ColorSpec(ColorSpec.COLOR_NORMAL)
COLOR INPUT new ColorSpec(ColorSpec.COLOR_INPUT)
COLOR MESSAGES new ColorSpec(ColorSpec.COLOR_MESSAGES)

Some examples:

message("some message");
message(new Object[]{ new character("some"), new character("message") },
        new ColorSpec(ColorSpec.COLOR_NORMAL));

An example when reading data:

// MESSAGE "setting book id: " UPDATE book.book-id.
message("setting book id: ", false, new FieldReference(book, "bookId"));

// MESSAGE COLOR INPUT "setting num1: " SET num1.
message("setting num1: ", true, new AccessorWrapper(num1),
        new ColorSpec(ColorSpec.COLOR_INPUT));

// MESSAGE "updating num1: " UPDATE num1 FORMAT "999" AUTO-RETURN.
message("updating num1: ", false, new AccessorWrapper(num1), true, "999");

To use MESSAGE VIEW-AS ALERT-BOX statement for displaying dialog alert boxes, different APIs are provided by LogicalTerminal:

4GL Statement LogicalTerminal API
MESSAGE [COLOR col] expression
VIEW-AS ALERT-BOX
[alert-type, BUTTONS button-set, TITLE title]
messageBox(expression [, alert-type, button-set, title] [, col])
MESSAGE [COLOR col] expression
VIEW-AS ALERT-BOX
[alert-type, BUTTONS button-set, TITLE title]
[{ SET | UPDATE} field]
messageBox(expression, <set = true for SET, false for UPDATE>, <field accessor> [, alert-type, button-set, title] [, col])

There are the following alert types defined into LogicalTerminal that you can use:

4GL Alert Type FWD Alert Type (LogicalTerminal constant)
MESSAGE ALERT_MESSAGE
QUESTION ALERT_QUESTION
INFORMATION ALERT_INFO
ERROR ALERT_ERROR
WARNING ALERT_WARNING

There are the following button sets defined into LogicalTerminal that you can use:

4GL Button Modes FWD Button Modes (LogicalTerminal constant)
YES-NO BTN_YES_NO
YES-NO-CANCEL BTN_YES_NO_CANCEL
OK BTN_OK
OK-CANCEL BTN_OK_CANCEL
RETRY-CANCEL BTN_RETRY_CANCEL

The messageBox function allows to update only logical values: they are set to true if “yes” or “ok” was selected, to false if “no” was selected and to an unknown value if “cancel” was selected.

If you used UPDATE option (not SET) then setting an appropriate logical value before executing the function makes a certain button to be selected by default, when the dialog is displayed.

An example of use:

logical ret = new logical(true);
messageBox("some message", false, new AccessorWrapper(ret), ALERT_ERROR,
           BTN_YES_NO, "some title", new ColorSpec(ColorSpec.COLOR_MESSAGES));

In order to hide messages and clear the message area you can use the LogicalTerminal.hideMessage API.

Put and Status Functions

The Direct Terminal Manipulation section of the Terminal Management chapter of the FWD Conversion Reference book explains how the 4GL's PUT and STATUS statements work. This section only summarizes the most used APIs provided by FWD, to handle these statements.

An example:

// PUT SCREEN "some text" ROW 1 COLUMN 2.
putScreen("some text", 1, 2);

In order to change displayed status you can use the following functions (defined into LogicalTerminal):

4GL Statement LogicalTerminal API
STATUS DEFAULT expression statusDefault(expression)
STATUS INPUT expression statusInput(expression)
STATUS INPUT OFF statusInputOff()
PUT CURSOR ROW r COLUMN c putCursor(r, c)
PUT CURSOR OFF putCursor(false)
EDITING block

The 4GL's EDITING block is fully explained by the PROMPT-FOR, SET and UPDATE StatementsEDITING Block section of the Editing chapter of the FWD Conversion Reference book.

When using such a block, it is important to always manually read each key and also trigger processing once the block has finished, by sending the key back to the terminal (or ignoring it). You can exit such a block using a leave() API call.

For example:

FrameElement[] elementList = new FrameElement[]
{
   new Element(num1, f1.widgetNum1())
};

updateEditing(f1, elementList, "editingLoop", new Block()
{
   public void body()
   {
      // usually the first statement in the editing loop is READKEY
    KeyReader.readKey();

      // after reading key value you can get it using LASTKEY function, analyze
      // it and take appropriate actions
      if (_isEqual(keyFunction(lastKey()), "end-error"))
      {
         message("end-error called");
         leave();
      }

      // normally, you will want to continue the normal key processing by
      // applying the read key
      apply(lastKey());
   }
});

Streams

When working with streams, it is important to keep in mind that if you need to access a server-side resource (such as an application configuration file), you will need to use Java's stream APIs to access it. This is because all streams available in 4GL access client-side resources. At this time, FWD does not provide server-side streams.

The FWD APIs which provide 4GL-compatible stream support are split in several classes, within the p2j.com.goldencode.util package. The main classes which provide stream APIs are:

  • Stream class for the named streams.
  • UnnamedStreams class for the unnamed streams.

These classes provide APIs to open a resource, device, the terminal or to start a process on client side. Before continuing, it is recommended to read the Streams chapter of the FWD Conversion Reference book, where you can find a detailed view of the 4GL's approach on implementing the streams and how FWD implements it. This section will continue on explaining what you should look for in the converted code to identify stream-related code and what to be careful when writing Java code which requires stream access.

Defining Streams

In the converted code, all named streams are defined as an instance field of com.goldencode.p2j.util.Stream type, with the class associated with the external procedure. The field is assigned a new StreamWrapper instance each time a new “external procedure” instance is created, with its only parameter set to the legacy stream name, as defined in the original 4GL code:

Stream rpt = new StreamWrapper(“rpt”);

If the stream needs new shared, shared or global stream clauses (which are part of the DEFINE STREAM statement), you can find full details about how to define them the Named StreamsDefining Streams in FWD subsection of the Streams chapter in the FWD Conversion Reference book.

In the converted code for this statement, if you encounter a TransactionManager.registerTopLevelFinalizable call emitted for the stream, it means the named stream is a non-shared stream and its scope is limited to the current external procedure. The new shared streams are registered using the TransactionManager.registerFinalizable and SharedVariableManager.addStream calls, emitted at the beginning of the body() implementation for the external procedure's Block instance.

For local streams, the TransactionManager.registerTopLevelFinalizable call ensures that the stream will be automatically closed, when the current procedure exits. Without this call, it can't be ensured that the stream will be closed, upon the return of the external procedure.

If a shared stream defined in another procedure is used, the instance field associated with the stream will not be assigned a new StreamWrapper instance. Instead, it will be searched in the set of available shared variables, using a SharedVariableManager.lookupStream call.

For writing new code, the developer will may use this pattern when defining a new named stream:

public class Test
{
  Stream rpt = new StreamWrapper(“rpt”); // for new defined streams
  // or
  Stream rpt = SharedVariableManager.lookupStream(“rptStream”); // for already defined streams

  public void execute()
  {
    externalProcedure(new Block()
    {
      public void body()
      {
         ...
         // for non-shared streams:
         TransactionManager.registerTopLevelFinalizable(rpt, true);

         // or

         // for new shared streams:
         SharedVariableManager.addStream(“rptStream”, rpt);
         TransactionManager.registerFinalizable(rpt, <global>);
      }
    });
  }
}

where the <global> parameter is set to false for non-global streams and to true for global streams.

The unnamed streams are automatically defined in 4GL, and FWD uses a similar mechanism (to a certain point). Although in 4GL by default all I/O uses the terminal and is done via the unnamed streams pipes, FWD has a different approach. In FWD, all I/O is performed using the terminal and, if the unnamed streams are redirected to another resource, the FWD runtime notices this and will redirect the I/O pipes accordingly. So, no special “define the unnamed streams” code is emitted by the conversion engine. More details about how the unnamed streams work can be found in the FWD Approach sub-section of the Streams chapter of the FWD Conversion Reference.

Opening Streams

To open a stream, FWD uses different APIs, depending on the resource being opened and the type of stream being used. Once opened, the named stream will be closed when the external procedure ends or is explicitly closed by the developer. The Working with StreamsOpening Streams section of the Streams chapter in the FWD Conversion Reference book describes best how FWD implements the named and unnamed stream opening.

For named streams, the following APIs are provided by FWD:

  • Stream.openTerminal() API is used when the target resource is the terminal.
  • Stream.openProcess() API is used when a process needs to be launched, and the stream needs to be connected to either its input or output pipe (will be discussed later in this chapter).
  • Stream.openFile API call is used when the target is a non-terminal resource. 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.

For the unnamed streams, the UnnamedStreams has different APIs to redirected the unnamed input and output stream to a different resource:

  • openProcessIn() - redirects the unnamed input stream to read data from the output pipe of a process.
  • openProcessOut() - redirects the unnamed output stream to send data to the input pipe of a process.
  • openProcessBoth() - redirects the unnamed input and output stream so that both are connected to the process' output and input pipes.
  • openTerminalIn() - special API call which is closes the current unnamed input stream redirection and sets the unnamed input stream to target the interactive terminal.
  • openTerminalOut() - special API call which is closes the current unnamed output stream redirection and sets the unnamed output stream to target the interactive terminal.
  • openFileIn - redirects the unnamed input stream to read data from the specified resource; has only one parameter:
  • 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.
  • openFileOut - redirects the unnamed output stream to send data to the specified resource; has the following parameters:
  • 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.
  • 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 Stream.openFile API is used to open a non-terminal resource, following table provide an insight on how the parameters can be used. Also, when the UnnamedStreams.openFileOut API call is used, its parameters have the same meaning as when the write parameter for the Stream.openFile API is set to true.

fileName write append paged pageSize Details
A file or device FALSE FALSE - - Opens the resource for reading.
“TERM”, “TERMINAL”, “term” or “TERMINAL” literals FALSE FALSE - - Uses the terminal as the input source.
A file or device TRUE FALSE - - Opens a file for writing; the file is cleared.
A file or device TRUE TRUE - - Opens a resource for writing in append mode (the file content is not changed, the pointer is set at the end of file).
A file or device TRUE TRUE or FALSE TRUE - Opens a resource for writing in paged mode, with default paging.
A file or device TRUE TRUE or FALSE TRUE A non-negative integer value. Opens a resource for writing in paged mode, with the page size set to the specified value.
“TERM”, “TERMINAL”, “term” or “TERMINAL” literals TRUE TRUE or FALSE - - Uses the terminal as the output destination. All output is done directly to the terminal.
“TERM”, “TERMINAL”, “term” or “TERMINAL” literals TRUE TRUE or FALSE TRUE A non-negative integer value. Uses the terminal as the output destination, but all output is proxied via the TerminalStream implementation on the client side. Paging is implemented as this is a non-terminal stream, but processes pause as an interactive terminal.

To support other 4GL stream clauses, FWD provides following API's in the Stream class:

  • Stream.setBinary() - set the BINARY mode for the stream, has no parameters.
  • Stream.setEcho - set the ECHO or NO-ECHO flags for the stream, with this parameter:
  • echo: true if echo mode is set.
  • Stream.setConvert - set the CONVERT or NO-CONVERT flags for an input stream, with this parameter:
  • convert: true if the stream's character conversion mode is enabled.
  • Stream.setUnbuffered - set the UNBUFFERED mode for an output stream, with no parameters.

When working with unnamed streams, the above APIs must be used with the stream reference as returned by the UnnamedStreams.safeInput() and UnnamedStreams.safeOutput() calls. It is important to always use these APIs when access to an unnamed stream reference is needed. More details about each of the above stream properties can be found in the Opening Streams in ... Mode sections of the Streams chapter of the FWD Conversion Reference book.

Closing Streams

An opened named stream can be closed either automatically, if it was registered properly, or explicitly. It is recommended always to explicitly close a stream as soon as access to it is no longer needed.

The Working with StreamsClosing Streams section of the Streams chapter in the FWD Conversion Reference book describes how FWD implements the stream closing for both named and unnamed streams. There you can find important details about how the unnamed streams can be nested in procedure calls and what impact closing or opening an unnamed stream in a nested call has on the caller block. This section will describe only what APIs FWD provides for closing a stream, all important details about how stream closing in 4GL works (and how FWD implements it) can be found in the section previously mentioned.

Named stream closing is done using the closeIn(), closeOut() and close() APIs defined in the Stream class. You need to be careful to use the appropriate API call, depending on how the stream was opened, following these rules:

  • if the stream was opened for reading, use the closeIn() API.
  • if the stream was opened for writing, use the closeOut() API.
  • if the stream is a process stream opened for both reading and writing, use the close() API.

For unnamed streams, the UnnamedStreams class provides the closeIn(), closeOut() and closeBoth() APIs, which must be used following the same rules as their named stream API counterparts. If the close API call is not used in sync with the stream's open mode, you can end up leaving the resource opened.

Process Streams

Named and unnamed streams can be linked to the input or output channel of a process. Full details about how FWD implements this can be found in the Process Streams section of the Streams chapter of the FWD Conversion Reference book.

When writing code which uses a process to read or write data, the stream must be first opened using the Stream.openProcess() API for named streams or UnnamedStreams.openProcess[In|Out|Both]() for unnamed streams. Once the stream is opened, you need to launch the process, while linking the stream with the required I/O channels.

The process is launched using the ProcessOps.launch API, which accepts these parameters:

  • cmdList: a String array representing the command to be executed.
  • sout: a reference to a stream opened for reading, so that the child process' output stream is connected to it.
  • sin: a reference to a stream opened for writing, so that the child process' input stream is connected to it.

When using this API, the unnamed stream reference sent to the sout parameter must be determined via the UnnamedStreams.safeInput() call and the stream reference sent to the sin parameter must be determined via UnnamedStreams.safeOutput() calls. For named streams, sout and sin are the StreamWrapper instance which represents the named stream.

I/O Using Streams

When using named streams for reading or writing, in some cases you need to use the stream reference to call a specialized API or, in other cases, you need pass the named stream reference to the API call explicitly. This is because FWD implements the stream-only I/O statements as API calls defined by the Stream class, while other 4GL features which can be used without streams (i.e. which work directly with the terminal) are implemented in other classes.

The Stream class provides APIs for the following 4GL statements. For named streams, use the stream reference to invoke this API. For unnamed streams, use the correct UnnamedStreams.safeInput() or UnnamedStreams.safeOutput() call, depending on the type of the statement (for reading or writing). The following table describes which stream-only APIs are implemented in FWD and where you can find more details about them, in the Streams chapter of the FWD Conversion Reference book. The examples defined by its associated section in the Streams chapter provide a good starting point which can be used when writing new code.

4GL Statement FWD API Streams Chapter Section Details
EXPORT Stream.export   Stream Only I/O StatementsEXPORT Statement. Used for writing data in a standard format, compatible with the IMPORT statement.
IMPORT Stream.import   Stream Only I/O StatementsIMPORT Statement. Used to read data previously written by the EXPORT statement
PUT Stream.put   Stream Only I/O StatementsPUT Statement. Used to send data to a device other than the terminal.

All other statements which can read or write data using a named stream will accept a stream reference, usually as the first parameter for the associated API. When unnamed streams are used, the I/O APIs need no special parameters, as the FWD runtime is responsible for determining the unnamed stream redirection and act accordingly.

The Stream I/O Using Frames, Paging Support and Other I/O Statements sections of the Streams chapter (in the FWD Conversion Reference book) provide a comprehensive description of how named and unnamed streams can be used with the FWD's I/O APIs, when writing new code.

Accessing the Database

Database access can be done using either FWD APIs which duplicate the 4GL semantic or directly, via the persistence layer (using HQL or SQL queries). This section will describe how to write Java code which uses 4GL-style buffers to access the database. The Integrating External Applications chapter of this book __ contains extensive information on how to access database records bypassing the 4GL-style queries, by using the FWD persistence runtime.

The Accessing the Database from Hosted Services section of the Integrating External Applications chapter of this book provides full details about how to bypass the 4GL-style record access and access directly the database via the FWD's persistence layer. This section will describe only how you can write 4GL-style code to access the database and what to be careful when using these features.

Buffer Definitions

When writing new programs which need to access data from certain tables and also this access needs to be 4GL-compatible, the developer needs to define a buffer for each used table. In a Java program, each buffer will be defined as a class field, with its type set to the DMO interface and instantiated using the RecordBuffer.define API (for permanent tables), which creates a proxy backed by a RecordBuffer instance. This allows transparent error handling, record locking, undo support etc. while accessing record fields (using field's getters and setters defined by the DMO interface). The FWD Record Buffer Implementation Classes, DEFINE BUFFER and DEFINE TEMP TABLE (WORK-TABLE, WORKFILE) sections of the Record Buffer Definition and Scoping chapter from the FWD Conversion Reference book describe in detail how the record buffers get converted from 4GL code to Java code; this section will briefly summarize how to define a 4GL buffer directly in Java code.

When defining a buffer for a permanent table, it must be done using a construct like this:

<DmoInterface extends DataModelObject> buffer
 = RecordBuffer.define(<DmoInterface extends
DataModelObject>.class, "<database>", "<DMO alias>");

where:

  • <DmoInterface extends DataModelObject> is the interface for the Data Model Object associated with the buffer
  • <database> is the logical database name
  • <DMO alias> is the alias used for this DMO in all WHERE and ORDER BY clauses used by HQL queries, in which this buffer is involved. Also, this is the variable name used by the TransactionManager to register the buffer for transaction processing.

For example:

Book book = RecordBuffer.define(Book.class, "database1", "book");

When defining a buffer to be used for record access and modification, it is assumed that the backing DMO is properly “installed” in the server's runtime (the implementing class and associated .hbm.xml file are created; for info about how to add a new DMO - temporary or permanent - to the application, see the Managing Data Model Object (DMO) Changes section of this chapter.

For temporary tables, the buffers are defined in a similar manner, but they are backed by a TemporaryBuffer proxy instead of a RecordBuffer. As temporary tables can be marked as no-undo (i.e. changes are auto-committed to the backing table), the TemporaryBuffer.define API provides an optional parameter, <undoable>, which, when set to false, marks the buffer as no-undo. Also, the optional parameter <global> is used to mark the buffer to survive until end of context life (when set to true, for shared temporary buffers - see the Defining Shared Buffers section of this chapter) or to expire upon leaving the top level procedure scope (when set to false).

<DMOInterface extends Temporary> buffer =
TemporaryBuffer.define(<DmoInterface extends Temporary>.class,
“<alias>”[, <global>[, <undoable>]]);

For example:

TemporaryRecord1 tt1 = RecordBuffer.define(TemporaryRecord1.class, "temp", "tt1");

When writing new Java code, you need to understand that buffers must always be declared as instance fields for the external procedure class. This is because of how the FWD conversion and runtime is built: each buffer is emitted as an instance field, but when the buffer is instantiated, the block associated with the external procedure is not yet available, so some state is initialized only when the next top level block is executed. So, if you define a buffer as a local variable or in some other part of the code (i.e. not an instance field for the associated external procedure class), you will end up with unexpected behavior, as some buffer state is not fully initialized.

Buffer Scopes

Before starting using a buffer to access a record, you must open a scope for it. Determining the correct location where to open a scope for a 4GL buffer is quite complicated, and the full set of rules used by the conversion engine are fully described in the Record Scopes section of the Record Buffer Definition and Scoping chapter from the FWD Conversion Reference book.

To open a scope for one or more buffers use a RecordBuffer.openScope call, which can take as arguments a variable number of buffers. Each argument must be a proxy for a permanent or temporary buffer, obtained using the RecordBuffer.define or TemporaryBuffer.define API calls. This will ensure that the specified buffers will be aware of block iteration, allow undo support in case of errors, etc. Depending on the block to which the buffer is scoped, the RecordBuffer.openScope call needs to be added to:

  • the beginning of the Block.body() method, if the block is an external procedure, internal procedure or internal function.
  • inside the Block.init() method, in any other cases.

When writing new Java code, although it is recommended to understand and follow the 4GL scope rules and buffer reference types (for full 4GL compatibility), in new Java code you can avoid complicating your code by using these rules:

  • when a buffer is used in a transaction, the buffer can use the same scope as the transaction.
  • In cases when the buffer is used by a 4GL-compatible procedure or function, the buffer must open a scope at the beginning of that procedure or function block.
  • When the buffer is used in only one block, is enough to scope the buffer to that block.
  • When the buffer is used in multiple non-nested blocks, the developer can open a buffer scope for each individual block. The other solution is to scope the buffer to the (external) procedure/function block or to the block which encloses all buffer references.
  • In cases when the buffer is used in multiple nested blocks, the developer can scope the buffer to the block which encloses all buffer references.
  • In any situation, even for the above cases, it is enough for the buffer to open a scope at the beginning of the root procedure block, internal procedure or function block.

When a pure-Java method is called and the method references one or more buffers (which were passed as method parameters or are defined as instance fields), all the buffers which have an open scope will “see” that open scope. This is because, during program execution, non-4GL compatible Java method calls act like the code is inlined in the caller block, so the method will be executed as if the code is part of the caller block.

Defining Multiple Buffers for the Same Table

When more than one record at a time needs to be available for a certain table, Progress allows more than one buffer to be associated with a certain table. In FWD, you will rely again on a RecordBuffer.define call, in the same manner as the main buffer for a table is defined. When writing new Java code, this translates into another instance field defined for the same DMO, while being careful to use distinct DMO aliases for each field definition:

<DmoInterface extends DataModelObject> secondBuffer = RecordBuffer.define(<DmoInterface extends DataModelObject>.class, "<database>", "<second DMO alias>");

for example:

Book book2 = RecordBuffer.define(Book.class, “database1”, “book2”);

The conclusion of this construct is that FWD allows defining as many buffers as the developer needs (this applies to temporary tables too). The only constraints are that each buffer needs to compute its own scope, even if there is another buffer for the same table, and that all buffer aliases must be unique in the context of an external procedure.

Defining Shared Buffers

Similar to shared variables, Progress allows usage of shared buffers; this is done using the DEFINE NEW SHARED BUFFER statement to declare the shared buffer and DEFINE SHARED BUFFER to reuse the shared buffer declared in another parent program. In FWD, the buffers are shared using the SharedVariableManager's addBuffer and lookupBuffer methods.

For declaring a new shared buffer, one needs to add a SharedVariableManager.addBuffer(“<buffer variable>”, buffer) call in the Block.body() method of the external procedure block. After this call, for all nested scopes (which may be external procedure calls or inner blocks), the <buffer variable> will hold a reference to the passed buffer. If, after an addBuffer call, there is one more call with the same buffer variable name, it will mean that, on all nested scopes from that point forward, the currently held buffer reference is replaced with the new one (which may be for a different DMO). When the block with the addBuffer call is exit, that reference is discarded, and the outer reference (if any) is available.

For reusing a shared buffer in another procedure, instead of using the RecordBuffer.define method to declare the buffer, use following construct:

<DmoInterface extends DataModelObject> buffer  = // buffer is an instance field
   SharedVariableManager.lookupBuffer(“<buffer variable>”);

For example:

public class Proc1
{
   Book buffer1 = RecordBuffer.define(Book.class, "database1", "book");

   public void execute()
   {
      externalProcedure(new Block()
      {
         public void body()
         {
            RecordBuffer.openScope(buffer1);
            SharedVariableManager.addBuffer("book", buffer1);

            // load book #1
            new FindQuery(buffer1, "buffer1.bookId = 1", null,
                                   "buffer1.bookId asc").unique();

            // execute an external procedure
            Proc2 proc2 = new Proc2();
            proc2.execute();
         }
      });
   }
}

public class Proc2
{
   Book buffer2 = SharedVariableManager.lookupBuffer("book");

   public void execute()
   {
      externalProcedure(new Block()
      {
         public void body()
         {
            RecordBuffer.openScope(buffer2);

            // at this point buffer “buffer2” is a reference to buffer1 in Proc1
            // and it contains book #1
         }
      });
   }
}

A shared buffer in a nested scope with the same name as one in an enclosing scope temporarily hides the outer scope's buffer's value. Consider we have called Proc3 from Proc2. In Proc3 we will execute SharedVariableManager.addBuffer("book", buffer3), that will cause lookup for the shared buffer “book” return buffer3 until you will leave Proc3. After that buffer1 will be returned.

When using the lookupBuffer API to resolve a shared buffer, is important for the instance field to have the same DMO type as the field used to add the shared buffer. More, this must be the same as the DMO type of the buffer passed to the latest addBuffer call, so that the currently held reference and the instance field are for the same DMO.

When using temp-tables, all the above applies, except the SharedVariableManager.@addTempTable(“<buffer variable>”, tempBuffer) and the T @emporaryBuffer.useShared(“<buffer variable>”) are used:

<DmoInterface extends Temporary> buffer = TemporaryBuffer.useShared(“<buffer variable>”);

For temporary tables, Progress allows defining them as GLOBAL shared; this means that, instead of the temp-table being available until the root procedure which defines the NEW SHARED temp-table is exit, they are available until the user-context ends. To allow support for the DEFINE NEW GLOBAL SHARED TEMP-TABLE statement, FWD uses a parameter for the TemporaryBuffer.define(...) call, this way:

<DMOInterface extends Temporary> tempBuffer =
 TemporaryBuffer.define(<DmoInterface extends Temporary>.class,
“<alias>”, <global>);

When the <global> parameter is set to true, the buffer is defined as GLOBAL. After this, a call to the SharedVariableManager.addTempTAble(“<buffer variable>”, tempBuffer); in the Block.body method completes the NEW GLOBAL SHARED support.

Passing Buffers as Parameters

When passing a buffer (associated with a permanent table) as parameter to a procedure or function, this allows the called code to have access to the current record; in this case, the parameter is always passed in input-output mode, all changes made to the buffer being reflected in the caller code. In case of temporary tables, the concept is a little different, as buffers for temporary tables can be passed in input, output and input-output modes. Full details about passing buffers as parameters in 4GL can be found in the Record Buffers as Parameters section of the Record Buffer Definition and Scoping chapter in the FWD Conversion Reference book.

For permanent table case, a buffer parameter means it duplicates the 4GL's DEFINE PARAMETER BUFFER <buffer> FOR <table> statement. In FWD, the following construction can be used:

public <return type> <method name>(final <DmoInterface extends DataModelObject> buffer)
{
   <block type>(new Block()
   {
    public void body()
      {
         RecordBuffer.openScope(buffer);

         // use buffer here
      }
   });
}

Although conversion is a little different (a reference to the received parameter is saved in another instance field for the external procedure class, which is actually used throughout the procedure/function), this is not needed in the converted code, due to the fact that buffer parameters for permanent tables are always in input-output mode, and all changes made to the buffer in procedure/function will reflect in the caller code. For example:

public void execute(final Book book)
{
   externalProcedure(new Block()
   {
      public void body()
      {
         RecordBuffer.openScope(book);

         // use book buffer here
      }
   });
}

or

public integer func(final Book book)
{
   return integerFunction(new Block()
   {
      public void body()
      {
         RecordBuffer.openScope(book);

         // use book buffer here
      }
   });
}

If you are using temp tables, the emitted code depends on how the buffer is passed - as input, output or input-output parameter, in append mode or not:

  • For input parameters, it means the called code has access to all records in the temporary table passed as parameter and any change made to the temporary table in the called code will not reflect in the caller code.
  • For output parameters, it means the called code does not have access to any record in the temporary table passed as parameter and any change made to the temporary table in the called code will reflect in the caller code, while all records existing in the temp table (in the caller code, prior to the invocation) will be lost.
  • In input-output mode, the called code has access to all records in the temp table (as they were at the time of invocation) and, upon return, all changes reflect in the caller code.
  • For input and input-output modes, the temp buffer parameter can be set in “append mode”, which keeps all records in the destination temporary table (the temporary table which the buffer parameter references); without this mode, the destination temp table is cleared by the caller code.

For these reasons, passing a temporary buffer as parameter requires an additional API call to be added in the called code, following the rules in this table:

Parameter Mode API Call
input TemporaryBuffer.associate([src-buffer], [dst-buffer], true, false);
input-output TemporaryBuffer.associate([src-buffer], [dst-buffer], true, true);
output TemporaryBuffer.associate([src-buffer], [dst-buffer], false, true);
input+append. TemporaryBuffer.associate([src-buffer], [dst-buffer], true, false, true);
input-output+append TemporaryBuffer.associate([src-buffer], [dst-buffer], true, true, true);

where src-buffer is the buffer reference received as parameter and dst-buffer is the local buffer reference which will be used in the caller code. When establishing the set of common fields for the two buffers, 4GL does not care if the field names are different; what it cares about are only the field type and position: if a source field has the same position and type as the destination field, then they are a match. If the source and destination fields at a certain position do not match, then 4GL will throw an error and abend the application.

When writing new Java code, passing parameters to external procedures may look like:

public class Proc
{
   TempRecord target = TemporaryBuffer.define(TempRecord.class, "target", false);

   public void execute(final Temporary source)
   {
      externalProcedure(new Block()
      {
         public void body()
         {
            RecordBuffer.openScope(target);
            TemporaryBuffer.associate(
               source,
               target,
               true,  // if true, records are copied from source table to target
                      // table
               true); // if true, records are copied from target table back to
                      // source table when the current transaction or
                      // subtransaction is committed;

            // work with records using target buffer here
         }
      });
   }
}

For other cases, if compatibility with records in a permanent table nor transaction support is needed, it is recommended to use Java collections to collect and keep data, as this is faster and cleaner than 4GL's temporary tables.

Transaction Support

In FWD, transaction levels can be set by the developer for any block type (internal/external procedure, function, FOR, REPEAT, DO, trigger, etc) by:

  • passing the TransactionType.FULL, TransactionType.SUB or TransactionType.NONE value to the actual block in case of internal/external procedure, function, FOR, REPEAT or DO blocks; for example, when starting a transaction for a REPEAT block, following will be used:
BlockManager.repeat(TransactionType.FULL, ..., new Block() { ...});
  • setting a flag for the LogicalTerminal.registerTrigger call, in case of a trigger definition:
public static void registerTrigger(EventList events,
                                   Class<?>  trigger,
                                   Object    contain,
                                   boolean   trans) /*  when true,
                                                        this parameter indicates that the
                                                        trigger will start its own full
                                                        transaction. */

In Progress, there is also the possibility for a block to start a sub-transaction. To duplicate this, FWD assumes that a sub-transaction needs to be started, if the transaction level parameter for the block is not used and that block is not a DO block.

The Understanding Where Transactions and Subtransactions Start section of the Transactions chapter of the FWD Conversion Reference book explains in detail how the 4GL's implicit or explicit transaction are determined by the conversion rules and how it reflects in the converted code. Also, the Conversion and Runtime Transaction Handling _section of the same chapter describes how the runtime automatically handles implicit transactions. More, other sections of the _Transactions chapter (such as Advanced Transaction Features and Determining When Transactions Are Active) describe other parts of 4GL's transaction mechanism of which the reader should be aware when writing Java code.

In hand written Java code, a transaction must be started explicitly, at least for the block which directly encloses statements which operate create, update or delete operations on a permanent or temporary buffer. Also, when a transaction is started, the FWD runtime will provide undo support for any variable registered for transaction support and for any temporary buffer not marked as NO-UNDO.

Considering that the converted code always has explicit transaction levels in all cases, while 4GL code may have implicit transaction levels, it means that it is up to the developer to set the appropriate transaction level for each block, when writing new Java code. By default, in FWD each block will start:

  • all types of REPEAT - a SUB-TRANSACTION
  • all types of FOR EACH - a SUB-TRANSACTION
  • all types of DO - no transaction. This contradicts the 4GL behavior, which states that a DO block (with a ON ENDKEY or ON ERROR clause and it reads records using an EXCLUSIVE-LOCK or it modifies the database in any way) needs to start a SUB-TRANSACTION if a transaction is active or a TRANSACTION if no transaction is active. This means that its up to the developer to set the appropriate transaction for such kind of DO block.

When a block reads records using an EXCLUSIVE-LOCK or it modifies the database in any way, and a transaction is not active, that block needs to start a transaction; FWD does this automatically only during conversion, so for hand written Java code, the developer will need to be aware of such cases and start a transaction as needed, by passing the TransactionType.FULL value to the transaction level parameter, on block start.

To be able to check if a transaction is active, FWD converts all the TRANSACTION function calls to a TransactionManager.isTransactionActive() calls. As this method returns a 4GL logical value, is convenient in some cases to use the TransactionManager.isTransaction() API, which returns a Java boolean value.

Beside the TRANSACTION function support, FWD provides also a way of checking if the current block is the block which started the transaction (i.e. the outermost block where a full transaction is active). To do this, the TransactionManager.isFullTransaction() method can be used, which returns a Java boolean value.

When a transaction or sub-transaction is backed out, this will roll back all changes to the records and variables registered for transaction support. Any DMO implementation class implements the Undoable interface, so it will be automatically registered for undo support when its scope is opened. But, as temporary tables can be no-undo, you need to mark any no-undo temporary buffer explicitly at its definition, as in:

<DMOInterface extends Temporary> tempBuffer =
 TemporaryBuffer.define(<DMOInterface extends Temporary>.class,
“<buffer alias>”, false, false);

The first boolean <false> parameter marks this table as “not being GLOBAL” (i.e. a DEFINE [NEW] GLOBAL SHARED TEMP-TABLE). The second <false> parameter marks this table as not being undoable (the NO-UNDO clause in the DEFINE TEMP-TABLE statement). An example:

TempRecord rec = TemporaryBuffer.define(TempRecord.class, "rec", false, false);
Record Locking

In FWD, a record is locked by passing a LockType instance to the required data access API. The Database Record Locking chapter of the FWD Conversion Reference book has all the details about how the FWD conversion engine and runtime handles record locks. The following table summarizes the available locks in 4GL and their FWD counterpart:

4GL Lock Type FWD API
SHARE-LOCK LockType.SHARE
EXCLUSIVE-LOCK LockType.EXCLUSIVE
SHARE-LOCK NO-WAIT LockType.SHARE_NO_WAIT
EXCLUSIVE-LOCK NO-WAIT LockType.EXCLUSIVE_NO_WAIT
NO-LOCK LockType.NONE

To explicitly release a record, use the RecordBuffer.release API, which takes as parameter a buffer for a permanent or temporary table. “Releasing” a record means that it is validated (if validation fails, an error is raised), persisted and lock is released or downgraded (see javadoc of this function for more information). Record buffer becomes empty after this operation.

If the user needs to check if previous record retrieval attempt failed because the record is locked by another user, use the RecordBuffer.wasLocked and RecordBuffer._wasLocked APIs (as usual, an underscore before an API means it returns a Java boolean value).

The *NO_WAIT versions of the SHARE and EXCLUSIVE locks mean that, if locking is not possible, the runtime does not wait for the lock to be released; instead, unless in silent error mode, an ErrorConditionException is thrown, with a notification that the record lock could not be performed.

Creating a Record

For creating a record, FWD provides the RecordBuffer.create(<buffer>) API. This will populate the buffer with a new record (with its fields set to default values) and also will lock it using a SHARE lock. To check if the buffer contains a newly-created record, use the RecordBuffer.isNew(<buffer>) or RecordBuffer._isNew(<buffer>) APIs.

Details about how the FWD runtime and conversion engine handles the 4GL's CREATE statement can be found in the Creating Recor_ds section of the _Record Buffer Statements of the FWD Conversion Reference book.

Updating a Record

Before attempting to change a record, the record must be locked using a SHARE or EXCLUSIVE lock. If the record is not locked, any attempt to modify the record will fail. When the record is locked using a SHARE lock, the lock will automatically be updated to the EXCLUSIVE lock; in case another context holds a SHARE or EXCLUSIVE lock, it will block until that context releases the lock. When modifying a record, it is enough to call the DMO's property setter to change its value. Before the property is changed, FWD will automatically validate the change and will report any errors.

When using the ASSIGN statement in Progress, it is possible to batch together more than one change, for more than one record. To duplicate this, FWD brackets all the DMOs' setter calls with RecordBuffer.startBatch() and RecordBuffer.endBatch() calls. If the ASSIGN statement needs to have a NO-ERROR clause, then the developer needs to bracket the start/end batch calls with ErrorManager.silentErrorEnable() and ErrorManager.silentErrorDisable() calls. Later on, if the user needs to check if an error occurred, the ErrorManager.isError() can be used.

Deleting a Record

In Progress, a record can be deleted only by using the DELETE statement. In FWD, the DELETE statement is duplicated using RecordBuffer.delete(<buffer>) API. If the buffer references a record and that record is locked using an EXCLUSIVE lock, it will delete it. Full details about how this statement is handled by the FWD runtime and conversion engine can be found in the Deleting Records section of the Record Buffer Statements of the FWD Conversion Reference book.

Beside the DELETE statement support, FWD also allows batch deletes, but this support currently is only for temporary tables. The reason is that, when batch deleting records in a permanent table you need to ensure that all “to-be-deleted” records are exclusively locked, perform the deletion and then release the locks. As temporary tables are private for each context, no locking is necessary and batch delete is safe.

For the RecordBuffer.delete(<buffer>, <where>, <args>) API, its arguments are:

  • the <buffer> parameter is a reference to a temporary buffer
  • the <where> parameter is a Hibernate-like WHERE clause, which filters the temporary records to be deleted, without DMO alias prefix.
  • the <args> is an Object array which holds the parameter for the WHERE clause, if any.

For example, to delete all records associated with the temporary buffer named tt1, which has an integer field F greater than the value held by an integer variable i , use following construct:

RecordBuffer.delete(tt1, “tt1.f > ?”, new Object[] { i });

For temporary buffers, FWD also allows removing all the records associated with a certain buffer. For this, use a TemporaryBuffer.clear(<buffer>) API call. This will remove all records belonging to the temporary table associated with the buffer passed as a parameter, but will not touch any record created in another scope or context - only records created in the current scope and context will be deleted.

Other Record Functions

For 4GL compatibility, FWD provides the following APIs in RecordBuffer class:

4GL Statement or Function RecordBuffer API Details
VALIDATE validate(<buffer) Used to validate the record currently referenced by a buffer.
In case the NO-ERROR clause is added, bracket the call with ErrorManager.silentErrorEnable() and ErrorManager.silentErrorDisable() calls.
Is handled by the VALIDATE Statement section of the Record Buffer Statements chapter of the FWD Conversion Reference book.
AVAILABLE _isAvailable(<buffer>)
isAvailable(<buffer>)
Used to check if the passed buffer currently references any record.
Is handled by the Database Field References chapter of the FWD Conversion Reference book.
LOCKED wasLocked(<buffer>)
_wasLocked(<buffer>)
Used to check if last record retrieval attempt using the passed buffer failed because the record was locked by another user.
Is handled by the Database Field References chapter of the FWD Conversion Reference book.
AMBIGOUS wasAmbiguous(<buffer>)
_wasAmbiguous(<buffer>)
Used to check if the last record retrieval attempt failed because the WHERE criteria was ambiguous.
Is handled by the Database Field References chapter of the FWD Conversion Reference book.
NEW .isNew(<buffer>)
_isNew()
Used to check if the currently referenced record by the passed buffer is newly created.
Is handled by the Database Field References chapter of the FWD Conversion Reference book.
BUFFER-COPY copy(<src buffer>, <dest buffer>, <validate>), where:
     the <src buffer> is a buffer which references the source DMO
     the <dest buffer> is a buffer which references the destination DMO
     <validate> is a field which tells FWD to validate the destination buffer after the copy
copy(<src buffer>, <src props>, <exclusive>, <dest buffer>, <validate>), where the new parameters stand for:
     <src props> - source properties to be included or excluded from the copy
     <exclusive> - when true, the source properties are excluded from the copy; when false, only the source properties are included in the copy
Copies the fields' values from the source buffer to the destination buffer, possibly excluding (or including only) some fields.
This statement is explained in detail by the Copy and Compare Buffers section of the Record Buffer Statements chapter of the FWD Conversion Reference book.
BUFFER-COMPARE compare(<buffer1>, <buffer2>, <result>)
After comparing the DMO's referenced by the two passed buffers, FWD will place the result in the <result> parameter
compare(<buffer1>, <prop list>, <exclusive>, <buffer2>, <result>)
When comparing the DMO's referenced by the two passed buffers, FWD will use only by the properties in the <prop list> if the <exclusive> parameter is false, or it will skip the properties in the <prop list> if the <exclusive> parameter is true.
Compares two records loaded by two distinct buffers.
This statement is explained in detail by the Copy and Compare Buffers section of the Record Buffer Statements chapter of the FWD Conversion Reference book.
RECID recordID(<buffer>) Used to retrieve the record ID for the record currently referenced by the passed buffer.
Is handled by the Database Field References chapter of the FWD Conversion Reference book.
ROWID rowID(<buffer>) Used to retrieve the row ID for the record currently referenced by the passed buffer.
Is handled by the Database Field References chapter of the FWD Conversion Reference book.
RELEASE release(<buffer>) Used to release the record currently referenced by the passed buffer.
Is handled by the Lock Types and Characteristics section of the Record Locks chapter of the FWD Conversion Reference book.
4GL-Style Queries

For most 4GL-style queries, FWD converts them into two separate entities: the block to which the query is scoped and the actual query which retrieves the record. The exception are OPEN QUERY statements, which do not have an implicit 4GL-style block.

Depending on the query type, the block used to scope the query is:

  • a BlockManager.forBlock*() block, in case of a FOR FIRST or FOR LAST query
  • a BlockManager.forEach*() block, in case of a FOR EACH query
  • a BlockManager.repeat*() block, in case of a REPEAT PRESELECT query
  • a BlockManager.do*() block, in case of a DO PRESELECT query
  • the FIND query and CAN-FIND function is a special query which doesn't need its own block
  • the OPEN QUERY doesn't need its own block too, you can directly interrogate it

When working with queries, each block of the above type behaves in a similar way, i.e:

  • the transaction level can be provided on a case-by-case basis
  • the block must be provided (i.e. Block.body() and Block.init() implementation)
  • any custom ON phrases can be provided
  • the actual Block implementation must be provided
  • an AbstractQuery instance must be provided

To use a query in hand-written Java code, use the following pattern to define the block:

<blockMethod>(<transaction level>, <label>[, <ohPhrase>], new Block()
{
   <QueryType extends AbstractQuery> query = null;

   public void init()
   {
      // open needed buffer scopes, if any
    RecordBuffer.openScope(buffer1, buffer2, ...);
    // instantiate the query; the arguments may differ, depending on the
      // query type used
    query = new <QueryType extends AbstractQuery>(<buffer>,
                                                    <WHERE clause>,
                                                    <client-side where>,
                                                    <ORDER BY clause>,
                                                    <WHERE clause arguments>,
                                                    <lock type>);
   }

   public void body()
   {
      // Query execution is postponed until the first query.next(), query.prev(), query.first() or
      // query.last() call is executed (i.e. on the first record retrieve attempt);
      // This call needs to be the first statement in the body() method, because if there are no
      /// more records to be retrieved, an error is raised and any other code must not be executed
    query.next(<lock type>); // the lock type can be passed as an optional
                               // parameter to the next/previous/first/last methods
   }
});

Before presenting the query types available in FWD, the developer should be aware of the query class hierarchy in FWD. The interfaces which are implemented and group the query types are those:

  • FWDQuery - interface which provides minimal API all query implementations must provide. This includes API's for behavior related to OPEN QUERY queries.
  • Joinable - query types which can be added as components to a CompoundQuery must implement this interface.
  • Scrollable - classes which support the concept of scrollable cursor must implement this interface.

Fig. 1. Hierarchy of query classes defined by FWD

The query types provided by FWD which act on a single table are the following:

  • FindQuery - all CAN-FIND calls will be converted to this query. For a FIND call, if the conversion rules don't decide that the FIND is used to navigate records (i.e. a loop which calls FIND NEXT on each iteration), it will convert to it. This query type uses a distinct HQL query for each of its previous/next/first/last/current/unique calls. It operates on a single table, and it uses the current record referenced by the buffer as a reference point.
    Details about how and when this query is emitted in the converted code can be found in the Data Access Language Statements → FIND section of the Queries chapter of the FWD Conversion Reference book.
  • AdaptiveFind - this query is used to convert FIND statements which are used for record navigation. This operates on a single table and uses the same behavior as the AdaptiveQuery to switch from dynamic to preselect mode and back.

The query types provided by FWD which can act on multiple tables are the following:

  • PreselectQuery - whenever possible, FWD will convert the FOR EACH or DO/REPEAT PRESELECT query to a query of this type. For this query, FWD will use a database cursor to navigate through the records. If the user modifies a record so that its new state would place it somewhere different in the result set or out of it, this type of query will not reposition the record on its new position - the record order remains the same throughout the query navigation. This should be the query of choice for new development.
    Details about how and when this query is emitted in the converted code can be found in the Data Access Language Statements → FOR _and _DO/REPEAT PRESELECT sections of the Queries chapter of the FWD Conversion Reference book.
  • RandomAccessQuery - queries which access data in a random way use this type of query. For this query, any change to a record state may change its position relative to other matching records and/or may eliminate it from the result set. Client code can jump among results randomly. This class was added only to fully support legacy, converted Progress code; it should not be used for new development, as this uses a very inefficient mechanism to retrieve records (i.e. a distinct query for each record).
  • AdaptiveQuery - on query execution, it will start in preselect mode. But, as this query type is aware of any record state changes and will reposition such records in the result set, it will switch to dynamic mode when it is notified of such a case. When switched to dynamic mode, the AdaptiveQuery will wrap its record retrieval to a RandomAccessQuery instance or to a CompoundQuery instance, depending of the number of components added to this query. But, as soon as it notices that it can switch back to preselect mode, it will do so. Like RandomAccessQuery, this query type was designed to support legacy behavior and should not be used for new development. Also, note that the multi-table support for AdaptiveQuery is not fully implemented by FWD at the time of this writing, so this query type currently works in single-table mode (switch CompundQuery is not supported)/.
  • PresortQuery - this is a special type of preselect query which relies on business application code to sort the results. On conversion, this is used in cases when the BREAK BY groups are specified for the query or when the sort order specified by the used index is different then the one specified in the BY clause. It is not recommended to use this query for new development, but may be necessary if break group functionality is needed.
  • CompoundQuery - this query type provides inner and left outer join support. Inner joins must always occur to the left (i.e., outward from) outer joins. If a component query specified as an inner join is added after any previously added component query specified as an outer join will automatically be converted to an inner join.
    This query retrieves result records in two modes: iterating and random access. Iterating mode is active when using the CompoundQuery.iterate method to advance the query to the next, composite row of records in the result set. Random access mode is active when using any of the other retrieval methods (first, last, next, previous, current). The difference between the modes is that iterating mode always terminates with a QueryOffEndException when no further record retrieval is possible; random access mode does not. Random access mode thus allows client code to advance off the end of the result set and continue processing. This is a very inefficient query implementation designed to support legacy behavior. It should be avoided for new development.
  • PresortCompoundQuery - this query combines the CompoundQuery behavior to allow join support with the PresortQuery behavior to allow client-side (relative to database level) sorting, plus BREAK BY support. This query should be avoided for new development.
  • QueryWrapper - a type of query to which the queries defined by DEFINE QUERY statement convert to. This is a delegating container which delegates all query navigation to internal query components of following types:
  1. PreselectQuery
  2. AdaptiveQuery
  3. AdaptiveFind
  4. PresortQuery
  5. RandomAccessQuery
  6. FindQuery
    The QueryWrapper should be avoided in new code.

From all the possible query parameters passed during initialization, following are the most important ones:

  • buffer - a reference to the buffer which will be used to reference the retrieved records
  • WHERE clause - for each type of query, a HQL WHERE clause can be provided. In the clause, each DMO property must be prefixed by the alias (i.e. variable name) used to define the temporary or permanent buffer provided to the query constructor. In cases when an inverse DMO is used by the query, the inverse DMO is joined using the foreign relations defined in the dmo_index.xml file, and the HQL where clause is updated accordingly before query execution.
  • When constant values are not used in the WHERE clause, the expressions must be passed using order-dependent substitution parameters. i.e., if a record field needs to be matched against a certain variable, the WHERE clause would translate in dmo.field = ?@; the actual variable will be passed to the constructor in the argument list, on the appropriate 0-based index.
  • ORDER BY clause - for each type of query, a database-level ORDER BY clause can be provided. On conversion, the ORDER BY clause is retrieved after resolving the unique index or the specified index used by the record phrase. For each such index, FWD adds entries in the dmo_index.xml file during conversion. Similar to the WHERE clause, the fields which appear in the ORDER BY clause must be prefixed by the buffer alias. When multiple query components are joined together via foreign relations, the ORDER BY clause will contain the mode to which the records will be sorted, for all components; so, the ORDER BY clause may contain fields from multiple buffers.
  • Client-side (relative to database-level) WHERE expression
    This is a special WHERE clause, which can't be resolved at query execution. Instead, during each record retrieval, the record will be matched against this query. If it fails, it will be discarded and the next one will be retrieved. The client-side WHERE expression is used in FWD by providing an implementation for following methods in WhereExpression class:
  1. public Object[] getSubstitutions() - before each match, this method will be invoked to retrieve the record values used for matching. The implementation for this method is optional.
  2. The parameters for this method must be a 4GL variable instance, a FieldReference or a Resolvable instance.
  3. public logical evaluate(final BaseDataType[] args) - this method is used to match each of the passed values using a custom set of rules, which can't be passed to the database-level WHERE clause
  4. In a case when the WHERE clause is missing but there is a client-side WHERE expression, it will result in fetching all the records in that table and each record matched against the client-side WHERE expression. This can result in poor execution times, so this should be used only in cases when the expression is not compatible with the HQL WHERE clause or each record must be matched against some value which modifies during query navigation.

Following is an example of a client-side WHERE expression:

WhereExpression whereExpr0 = new WhereExpression()
{
   public Object[] getSubstitutions()
   {
      return new Object[]
      {
         new FieldReference(buffer1, "<field>"),
      };
   }

  public logical evaluate(final BaseDataType[] args)
   {
      return isEqual((integer) args[0], buffer2.getField2();
   }
};

Client-side WHERE expression was implemented as a last resort for 4GL WHERE clauses that could not be expressed entirely in HQL because they rely on runtime state (like local variables). It is very inefficient and should be avoided at all costs for new development.

  • WHERE clause arguments
    For each parameter in the HQL WHERE clause, a value must be provided for it. To do this, an order dependent Object[] array must be provided.
    In all cases except compound queries, the arguments are resolved once, at query initialization. For compound queries, the arguments are resolved once, at query initialization, for only the first query component. For all other query components, the arguments are resolved each time the query component is executed and the arguments must be passed using:
  • FieldReference instances for record field arguments
  • a Resolvable instance, for other cases

Queries support accumulators (and are handled by the FWD runtime), but we do not encourage at all accumulator usage in new Java code; details about why and how to read the accumulators can be found in the Working with Accumulators section of this chapter.

Special functions and operators for WHERE clause expressions

To provide 4GL compatibility, when the WHERE clause needs to use functions to manipulate string or date columns, to cast a column to a different type or usage of arithmetic or logical operators is needed, FWD provides special functions which run inside the PostgreSQL database server process as user-defined functions. These functions are defined in classes inside the p2j.persist.pl package, and are defined in following classes:

The p2j.persist.pl.Functions class provides support for:

1. character functions

  • BEGINS - mapped to the begins(source, target[, start[, case sensitive]]) API's
  • MATCHES - mapped to the matches(source, target[, case sensitive]) API's to match a string against a pattern and the matchesList(list, item) API to match an item in a comma-separated list against a given value.
  • INDEX - mapped to the indexOf(source, target[, start[, case sensitive]]) API's
  • SUBSTRING - mapped to the substringOf(text, pos[, len[, type]]) API's
  • LENGTH - mapped to the lengthOf(text) APIs
  • ENTRY - mapped to the entry(index, list[, delimiter]) APIs
  • LOOKUP - mapped to the entry(entry, list[, delimiter[, case sensitive]]) API's

2. string concatenation operator - to merge two strings together, always use the concat(op1, op2) API.

3. date functions

  • julianDayInt - transforms the given date to its number of days since 12/31/-4714, as an integer.
  • julianDayDec - transforms the given date to its number of days since 12/31/-4714, as a decimal value.
  • getYear - retrieve the year number from a date
  • getMonth - retrieve the month (1-based) from a date
  • getDay - retrieve the day of the month (1-based) from a date

4. decimal functions

  • ROUND - mapped to Functions.roundDec(value[, precision]) API's
  • reportPrecisionScale - helper function to report the precision and scale of a numeric value as a string.

5. casting

  • toInt(value) - cast the given value to an integer
  • toDec(value[, precision]) - cast the given value to a decimal
  • t oString(value[, format]) - cast the given value to a string, using the given format

The p2j.persist.pl.Operators class provides support for:

1. logical operators

  • eq(value1, value2) - the equality operator
  • ne(value1, value2) - the not-equal operator
  • gt(value1, value2) - the greater-than operator
  • gte(value1, value2) - the greater-or-equal operator
  • lt(value1, value2) - the less-than operator
  • lte(value1, value2) - the less-or-equal operator

All the above API's return a Java boolean value. These operators must be used when the left-side expression of a WHERE test is a complex expression which takes as operands DMO properties.

The operands can be of any from the following Java types - java.lang.Boolean, java.lang.Integer, java.lang.String or java.lang.BigDecimal.

2. arithmetic operators

  • plus(value1, value2)
  • minus(value1, value2)
  • multiply(value1, value2)
  • divide(value1, value2)
  • modulo(value1, value2)

All the above API's return a Java numeric or date value. These operators must be used when the left-side expression of a WHERE test is a complex expression which takes as operands DMO properties.

The operands can be of any from the following Java types - java.lang.Integer, java.util.Date or java.lang.BigDecimal.

3. date operators

  • dateSpan(date1, date2) - determines the number of days between two dates

The API's provided by the above two classes will be invoked when the operator/function call will be used in a WHERE clause. These must be used only when an operand of a logical test in the WHERE clause is a complex expression which is computed from DMO properties, i.e.:

“minus(dmo.date1, 30) = ?”

When the above API's are used, it ensures that the complex expression will be computed such the result will be 4GL compatible. However, new code can likely be more flexible and use the database's built-in operators instead. This will allow the database's query planner to make better decisions in terms of index selection (the UDFs generally are opaque to the query planner).

PreselectQuery

This is the query type to be used for new development. When using a PreselectQuery, it is usually scoped to an iterating block. For example:

repeat(“label”, new Block()
{
   PreselectQuery query1 = null;

   public void init()
   {
      // version 1:
    query1 = new PreselectQuery(<buffer>, <WHERE clause>, <where expression>,
              <ORDER BY clause>, <inverse DMO>, <WHERE clause args>, <lock type>);

    //  version 2:
      query1 = new PreselectQuery(<sort order>); // in this case, the query
                                  // components must be added by the user using
                                  // query1.addComponent(...) calls
    // add other query components which join using foreign relations
    query1.addComponent(<buffer>, <where clause>, <inverse DMO>,
                          <where clause arguments>, <lock type>);
    ...
   }

   public void body()
   {
      query1.next();
   }
});
AdaptiveFind

This query type exists as a conversion optimization of certain FIND statements in loops. Like its parent AdaptiveQuery, it is not intended for new code.

The AdaptiveFind query can be scoped to any type of block. Following is an example on how to use this type of query:

repeat(“label”, new Block()
{
   AdaptiveFind query1 = null;
   boolean first = true;

   public void init()
   {
      query1 = new AdaptiveFind(<buffer>, <WHERE clause>, <ORDER BY clause>,
                                { <lock type> | <inverse sorting> });
    first = false;
   }

   public void body()
   {
      if (first)
    {
       first = false;
       query1.first()
    }
    ...
    query1.next();
    ...
   }
});

This type of query allows record retrieval only in one direction (i.e. forward). To simulate the backward record retrieval, the query should pass a true value to the <inverse sorting> parameter. Also, note that currently FWD doesn't provide WHERE clause arguments. So, only constant values can be used in the WHERE clause (or it can be skipped in the first place).

AdaptiveQuery

This type of query is not recommended for new development.

As the AdaptiveQuery is a PreselectQuery which starts in preselect mode and can switch to and back from dynamic mode, its usage is the same as the one described in the PreselectQuery example.

PresortQuery

The PresortQuery is one of the two query types which allows [BREAK] BY clauses. Note that care should be taken with PresortQuery, since a large result set can use an enormous amount of Java heap memory.

To support this, FWD provides following methods:

  • PresortQuery.enableBreakGroups() - a call to this method is needed in the Block.init() method, to enable the BREAK groups.
  • PresortQuery.addSortCriterion(Resolvable, { descending }) - calls to this method add client-side (relative to database level) sorting and are used in@ Block.init(). Note that this overrides any ORDER BY clause passed to the query constructor.

Following methods can be used in Block.body():

  • PresortQuery.isFirst() - Determine whether the query result row currently being visited is the first row in the presorted results list (FIRST function).
  • PresortQuery.isFirstOfGroup(Resolvable) - Determine whether the query result row currently being visited is the first row within a break group whose category is identified by the specified resolvable object (FIRST OF function).
  • PresortQuery.isLast() - Determine whether the query result row currently being visited is the last row in the presorted results list.
  • PresortQuery.isLastOfGroup(Resolvable) - Determine whether the query result row currently being visited is the last row within a break group whose category is identified by the specified resolvable object.

To use the PresortQuery, it can be scoped to any kind of query-type block. Following is an example of how it can be used:

forEach(“label”, new Block()
{
   PresortQuery query1 = null;
   Resolvable expr1 = new FieldReference(dmo, property, {uppercase}, {index});

   public void init()
   {
      // version 1:
    query1 = new PresortQuery(<buffer>, <WHERE clause>, <where expression>,
             <ORDER BY clause>, <inverse DMO>, <WHERE clause args>, <lock type>);

    //  version 2:
    query1 = new PresortQuery(); // in this case, the query components must
                 // be added by the user using query1.addComponent(...) calls

    // enable break groups
    query1.enableBreakGroups();
    // add a client-side sort criteria
    query1.addSortCriterion(expr1);
    ...

    // add other query components which join using foreign relations
    query1.addComponent(<buffer>, <where clause>, <inverse DMO>,
                <where clause arguments>, <lock type>, <iteration type>, <outer>);
   }

   public void body()
   {
      query1.next();
      ...
   }
});
CompoundQuery

This type of query is not recommended for new development, use PreselectQuery (which supports server-side joins of multiple tables) if it is possible.

To duplicate 4GL multi-table join, FWD provides following methods:

  • CompoundQuery(resolveOnce, preselect) constructor
    The parameters for this constructor are used for different cases:
  • resolveOnce is true only in cases when queries available via an OPEN QUERY statement are needed
  • preselect parameter is true only in cases when a query backed by a OPEN QUERY PRESELECT, DO PRESELECT or a REPEAT PRESELECT statement is needed.
  • CompoundQuery.setScrollable() - used only with a OPEN QUERY ... SCROLLABLE statement
  • CompoundQuery.addComponent(query, {iteration}, {outer}) - this method is used to add query components to the compound query.
    • The query parameter can be a query of any Joinable subtype (PreselectQuery, AdaptiveQuery, AdaptiveFind, PresortQuery, RandomAccessQuery, FindQuery)
    • the iteration parameter is the iteration type of this query, which is a QueryConstants.FIRST/LAST/NEXT/PREVIOUS/UNIQUE value
    • the outer flag indicates whether this is a left outer join (when true) or an inner join (when false).
  • CompoundQuery.iterate() - Advance the compound query, triggering one or more of the underlying queries to retrieve a new record. This produces a virtual, composite row of results which is the combination of the records retrieved by each component query.
  • For the first, last, next and previous methods, following parameters can be provided:
    • iterating - true if this method is being invoked within the context of an iterating loop, which can only be exited by raising an end condition; false if end condition should not be raised, even if no further advancement of the composite result is possible.
    • lockType - Lock type which should override that of any underlying query.

Following is an example on how to use this type of query:

forEach(“label”, new Block()
{
   CompoundQuery query1 = null;
   Resolvable expr1 = new FieldReference(dmo, property, {uppercase}, {index});

   public void init()
   {
      query1 = new CompoundQuery(false, false);

    // add query components
    // add first query
    query1.addComponent(new AdaptiveQuery(<buffer1>, <WHERE clause>,
        <where expression>, <ORDER BY clause>, <inverse DMO>,
        <WHERE clause args>, <lock type>));

    // add second query
    // prepare the subsequent buffer, so that the query will process its
      // substitution parameters
    RecordBuffer.prepare(<buffer2>);
    query1.addComponent(new RandomAccessQuery(<buffer2>, <WHERE clause>,
           <where expression>, <ORDER BY clause>, <inverse DMO>,
           <WHERE clause args>, <lock type>); // in this case, the WHERE clause
                                              // arguments can be only Resolvable instances
                                              // (FieldReference's or other implementations)
      ...
   }

   public void body()
   {
      query1.iterate();
    ...
   }
});
PresortCompoundQuery

This type of query is not recommended for new development, as it has the memory footprint of PresortQuery and the inefficient data access of CompoundQuery. However, it may be necessary for certain types of joins not supported by PreselectQuery.

To implement queries of this type, FWD combines the presorted and the compound query behavior. The presorted behavior is implemented similar to the PresortQuery class and the compound behavior is inherited from the CompoundQuery class.

forEach(“label”, new Block()
{
   CompoundQuery query1 = null;
   Resolvable expr1 = new FieldReference(dmo, property, {uppercase}, {index});

   public void init()
   {
      query1 = new CompoundQuery(false, false);

    // add query components
    // add first query
    query1.addComponent(new AdaptiveQuery(<buffer1>, <WHERE clause>,
        <where expression>, <ORDER BY clause>, <inverse DMO>,
        <WHERE clause args>, <lock type>));

    // add second query
    // prepare the subsequent buffer, so that the query will process
      // its substitution parameters
    RecordBuffer.prepare(<buffer2>);
    query1.addComponent(new RandomAccessQuery(<buffer2>, <WHERE clause>,
             <where expression>, <ORDER BY clause>, <inverse DMO>,
             <WHERE clause args>, <lock type>); // in this case, the WHERE clause
                                    // arguments can be only Resolvable instances
                                    // (FieldReference's or other implementations)
    ...
    // enable break groups
    query1.enableBreakGroups();
    // add a client-side sort criteria
    query1.addSortCriterion(expr1);
    ...
   }

   public void body()
   {
      query1.iterate();
    ...
   }
});
QueryWrapper

There is no much need to use this type of query for new development - FindQuery and PreseletQuery provide enough support.

The QueryWrapper is used by FWD to implement the queries used by DEFINE/OPEN QUERY statements. For this, FWD provides following APIs:

  • QueryWrapper(scrolling) constructor - constructs a new query wrapper, in scrolling mode or not
  • assign(query) - assign a query to which the navigation is delegated to. The query can be only one of CompoundQuery, RandomAccessQuery or PreselectQuery (sub)types
  • setScrolling() - set the query as SCROLLABLE
  • addComponent(query, ... ) - add other components to the query, only when the assigned query is a CompoundQuery
  • addComponent(buffer, ...) - add other components to the query, used only when the assigned query is a@ PreselectQuery
  • setIterationType(iteration) - when the assigned query is a PreselectedQuery, this is used to set the iteration type (from QueryConstants) for the last added query component
  • addSortCriterion() - when the assigned query is a PresortQuery, use it to add a sort criterion
  • enableBreakGroups() - when the assigned query is a PresortQuery, enable break group support
  • addAccumulator(accumulator, {deferred}) - register an accumulator with the query. When the deferred parameter is true, calls to QueryWrapper.iterate() will postpone accumulation until an Accumulator.accumulate() call. Else, it will accumulate data immediately.
  • iterate() - advance a@ CompoundQuery delegate
  • repositionByID(recid) - reposition the query to the record with specified recid (i.e. R EPOSITION TO RECID statement).
  • repositionByID(rowid, rowid...) - reposition the query the the rows with the specified ID, for each of the registered query (i.e.@ REPOSITION TO ROWID statement)
  • reposition(row) - reposition to the specified 1-based index (i.e. REPOSITION ROW | FORWARD | BACKWARDS row statement).
  • open() - use it to open the query (i.e. the OPEN QUERY statement)
  • close() - use it to close the query (i.e. the CLOSE QUERY statement)
  • currentRow() - return the 1-based index of the current row (i.e. the@ CURRENT-RESULT-ROW function)
  • size() - return the number of records currently in the results list of this query (i.e. the NUM-RESULTS function)

To navigate through the query, QueryWrapper provides following methods as a place holder for the GET statement. Following definitions are the same for the GET NEXT/PREV/FIRST/LAST statements:

  • next({<lock type>}) - used for any kind of delegate
  • next(iterating, {<lock type>}) - used only by a CompoundQuery delegate. The iterating flag indicates that an end condition should be when the query navigates past the last record.
  • next(<values>, {<lock type>}) - used only by a RandomAccessQuery delegate. The values represent the substitution values to be used when executing the HQL query.

For the GET CURRENT statement, QueryWrapper provides following methods:

  • current({<lock type>}) - used for any kind of delegate

Beside the statements available in Progress, FWD provides implementation for a GET UNIQUE equivalent (used when the query should return only one record). For this, the QueryWrapper.unique() method is available, with the same parameters as the QueryWrapper.next versions.

Following is an example about how the QueryWrapper can be used:

repeat(“label”, new Block()
{
   final QueryWrapper query1 = new QueryWrapper(<scrolling>);
   Resolvable expr1 = new FieldReference(dmo, property, {uppercase}, {index});

   public void body()
   {
      // add query components
    query1.assign(new PreselectQuery(<buffer1>, <WHERE clause>,
                                       <client-side where>, <ORDER BY clause>, <inverse DMO>,
                                       <WHERE clause args>, <lock type>));
    query1.open();
    ...
    // navigate through the query
    query1.first();
    ...
    query1.close();
   }
});

For using a BROWSE widget with a frame, the QueryWrapper must be registered first with that widget, by calling the BrowseWidget.registerQuery@(FWDQuery query, FrameElement[] fields) method. The <fields@> parameter is an array of@ BrowseColumnWidget 's used by the browser. To use a query registered with a BROWSE widget, following code can be used:

repeat(“label”, new Block()
{
   final QueryWrapper query1 = new QueryWrapper(<scrolling>);
   Resolvable expr1 = new FieldReference(dmo, property, {uppercase}, {index});

   public void body()
   {
      ...
    // register query with the BROWSE widget
    FrameElement[] columns = new FrameElement[]
    {
       new Element(new FieldReference(<buffer>, “<property>”), frame.widgetBrowserCol()),
         ...
      };
    frame.widgetBrowser().registerQuery(query1, columns);
    ...
    // add query components
    query1.assign(new PreselectQuery(<buffer1>, <WHERE clause>,
                                       <client-side where>, <ORDER BY clause>, <inverse DMO>,
                                       <WHERE clause args>, <lock type>));
    query1.open();
    ...
    frame.enable(); // enable all widget in the frame (including the browser)
    ...
    waitFor(...); // add a WAIT-FOR loop
    ...
    query1.close();
    }
});
FindQuery

When using a FindQuery, it's important to note that any of following calls will trigger a database query. So, using it in high-usage blocks may reduce performance and other approaches should be considered. To create a FindQuery instance, one should use following code:

FindQuery f = new FindQuery(<buffer>,
<WHERE clause>, <where expression>, <ORDER BY clause>,
 <inverse DMO>, <WHERE clause arguments>, <lock
type>);

Following methods are made available by FWD (note that all methods can receive the lock type as a parameter):

  • next() - retrieve the next record, using the specified ORDER BY and WHERE clauses and the currently referenced record as a reference point.
  • previous() - retrieve the previous record, using the specified ORDER BY and WHERE clauses and the currently referenced record as a reference point.
  • first() - retrieve the first record, using the specified ORDER BY and WHERE clauses
  • last() - retrieve the last record, using the specified ORDER BY and WHERE clauses
  • current() - refresh the current referenced record
  • unique() - retrieve an unique record using the specified WHERE clause

Support for the above methods is added for each of the presented query types and their parameters may vary. Note that when the above statements take a LockType parameter, it will override any LockType specified at query constructor.

Although some other public methods are made available in the FindQuery class (mostly inherited from the RandomAccessQuery class), those are not of a 4GL behavior interest, but only to provide internal persistence support for the FWD server.

For a CAN-FIND statement, following methods are provided by the FindQuery class:

  • hasAny({<values>, {<lock type>}}) - corresponds with the CAN-FIND FIRST/LAST function. The <values> parameter hold the substitution values for this query
  • hasOne({<values>, {<lock type>}}) - corresponds with the CAN-FIND function with no qualifier.
Connecting external databases

FWD allows to connect to external databases which are controlled by other FWD servers (each database should have a single authoritative server, which is responsible for ID generation, record locking etc., and all connection to this database should be done through this server). You connect to another FWD server using the same TCP port that is used for incoming client connections. Facility for database management is provided by the ConnectionManager. It provides the following APIs:

  • connect(<physical database name>[, <logical database name>][, <host name / address>] [, <port / port name>])
    or
    connect(“-db <physical database name> [-ld <logical database name>] [-H <host name / address>] [-S <port / port name>]”)
    Connects to a remote database, where the arguments are:
  • <physical database name> - the name of database on the remote server to connect to (local name on the remote server);
  • <logical database name> - the name which should represent the database on the local server (local alias for the remote database);
  • <host name / address> - remote server host name or IP address;
  • <port / port name> - TCP port number on the remote host or the port name as it is represented in the server/<server name>/port_services section of the configuration directory file.

E.g.

ConnectionManager.connect(“database1”, “remote_database1”, “192.168.0.1”, “remote_database1_port”);
ConnectionManager.connect(“-db database1 -ld remote_database2 -H host2 -S 3340”);

If an error occurred while connecting to a remote database then an ERROR condition is raised.

  • disconnect(<logical database name>) - disconnects a database.
  • createAlias(<alias>, <logical database name>) - creates an alias for a local or remote database. Creating aliases may be very convenient. Consider you want to perform a search over multiple databases. Then you have to do the following (assuming that all databases have the target DMOs):
  • connect all external databases;
  • create an external procedure that performs the main task and which have the record buffers (for the target DMOs) defined for some database alias:
Rec rec = RecordBuffer.define(Rec.class, "alias", "rec");
  • execute the following cycle:
for (String db : databases)
{
   ConnectionManager.createAlias(“alias”, db);
   new Proc().execute();
   ConnectionManager.deleteAlias(“alias”);
}

As the Proc program uses the alias to link the record to a logical database, on each execution will use a different database - the database to which the alias currently points.

  • deleteAlias(<alias>) - deletes previously created alias.
  • connected(<logical database name> or <alias>) - checks whether the database with the given name is connected.
  • numDBs() - number of currently connected databases in the current context.
  • ldbName(<index>) and pdbName(<index>) - get the logical / physical name of the database at the given index among all databases currently connected in this context.

The Database Connection Related Statements chapter of the FWD Conversion Reference book provides full support for all database-related APIs - how FWD runtime handles them and how and when they get converted.

Accumulators

Accumulators are 4GL's mechanism of collecting and aggregating data to compute various statistics, like average, minimum, maximum, etc. This is a 4GL-specific feature which should be used only when writing 4GL code. Although the specific FWD APIs can be used in hand written Java code, we strongly recommend not to use accumulators in hand written code, as the converted code gets quite complicated. Also, their functionality (when used with record fields) can be better implemented using SQL aggregate functions with the result being computed directly at the physical database level with increased performance. In other cases, a simple logic is faster and cleaner to use in Java code. Details about how to access the database directly, bypassing the 4GL statements, can be found in the Accessing the Database section of this chapter.

The conversion rules which emit the Java code for the accumulators (i.e., the converted implementation of the 4GL ACCUMULATE statement and ACCUM function) is fully explained in the Accumulators chapter of the FWD Conversion Reference book. The chapter also contains details about how FWD runtime implements the accumulators.

Following are some important notes, in case you still decide to use accumulators. When used with 4GL-style queries, if accumulators need to be used for aggregating record fields, the accumulator must be registered with the query in the Block.init() method. This is done by adding a query.addAccumulator(accumulator) call in the Block.init() method, after the query was initialized. After the record is retrieved, the accumulator will aggregate the field's value only when the accum.accumulate() is called. To provide query-level accumulator support, FWD provides following APIs:

  • FWDQuery.addAccumulator(Accumulator) - registers a certain accumulator with the query; the accumulator will be notified of any BREAK group changes.
  • Accumulator.reset() - in the Block.init() method, all accumulators registered with the query needs to be reset by adding a call to this method.
  • Accumulator.accumulate() - called in the Block.body() method, after the query retrieved the next record(s).
  • Accumulator.addBreakGroup(Resolvable) - in the Block.init() method, this method is used to register the BY clause for the accumulator. A call to this method is needed for each break expression.
  • Accumulator.setSubOnly() - sets the accumulator in sub-only mode (provide results only for intermediate break groups; no grand total is provided). Also, as 4GL allows usage of a sub-accumulators only in FOR EACH ... BREAK BY blocks, its recommended to use the accumulators with break support only when they are scoped to a BlockManager.forEach* block.
  • <? extends Accumulator>.getResult() - gets the accumulated value using the data source across all iterations of the loop.
  • <? extends Accumulator>.getSubResult(Resolvable) - gets the accumulated value for the specified BREAK BY group.

As 4GL limits break accumulator support only for queries which have a BREAK BY clause, this means that the developer must use the Accumulator.addBreakGroup(Resolvable) only with a presorted query (i.e. only PresortQuery and PresortCompoundQuery support BREAK BY clauses and client-side sorting). Also, as a second limitation, the query (and the accumulator) must be scoped to a FOR EACH block, which provides the BREAK BY clause.

If used to display intermediate accumulator values or grand totals using a frame, the DISPLAY Statement with Aggregate Phrase section of the Accumulators chapter of the FWD Conversion Reference book provides enough details to understand how to implement it directly in Java.

Executing Other Programs

4GL allows execution of other program files (i.e. .p files) or external processes. To invoke a 4GL file, all needed details can be found in the RUN Statement section of the Control Flow chapter of the FWD Conversion Reference book. This section will focus on the most important parts the developer should be aware of when writing new code.

To invoke a converted 4GL program from new Java code, you need to create a new instance (of the class to which the external procedure was converted) and invoke its execute method. Creating a new instance is just as easy as invoking the default c'tor; note that no c'tor will be emitted in the converted code for any existing 4GL program, and the program's parameters will be passed to the execute() method. For example:

LegacyProgram program = new LegacyProgram();
program.execute(param1, param2, ..., paramn);

The conversion rules emit the program's parameters to the execute method because of how FWD implements the RUN statement: as this statement can take as argument the result of a complex expression, the invoked program's name in some cases can't be resolved at conversion time, only at runtime. Consequently, to handle the cases where the program name is input by the user or just determined in some other dynamic way, FWD provides APIs in the ControlFlowOps class. These APIs are discussed in detail in the RUN Statement → Dynamic Procedure Names of the Control Flow chapter.

Although currently only ControlFlowOps.invoke(Object caller, Object[] param) is emitted by the conversion rules, all ControlFlowOps.invoke* APIs are working and can be used when writing Java code by hand.

As a 4GL-compatible program can be dynamically invoked, when writing a Java program by hand it is important to know the intent of this program at start: if it is certain that the program will always be statically invoked, you can add custom c'tors to the Java class. But, as requirements can change over time, we recommend to follow the 4GL-style: always provide a default c'tor and pass all arguments to the execute() method. This ensures future compatibility of the new program with the ControlFlowOps APIs, so that it can be executed either from new code or from 4GL converted code.

Invoking External Processes

All APIs which provide access to an external applications are provided by the ProcessOps class. The Process Launching chapter of the FWD Conversion Reference book handles all cases of how conversion rules handle the process-related statements which launch external processes and also how the command statement to run them is interpreted.

............

Minimize changes to converted code if possible, try to encapsulate new features in new source files

Writing Block-Aware Code

  • dedicated pure Java methods can be used to split certain code, so it will be easier to read and maintain; this "pure Java" methods act as the 4GL code is inlined, so any parameters such as buffer, variables, frames, etc, will act as they are scoped to the 4GL-style block which called this method
  • whenever is possible, add API's for code used by more then one program, via static Java methods. This can be either:
  • "pure-Java" method calls, which inline the code;
  • methods which define 4GL function/procedures, execute a certain task and might be called from legacy 4GL code. Although they can be static methods, they act as 4GL internal functions/procedures and act as being scoped to the 4GL-style program which called them.
  • 4GL-compatible code added to Java static methods can emulate .i files.
  • Code added to plain Java methods acts like a DO ... END block (with no error/transaction support).

Writing block-aware code, doing common block things (common loops and blocks, basic flow of control stuff, transactions, exiting normally and abnormally...)

  • External/Internal procedures, functions
  • Looping blocks - when to use 4GL compatible and when to use Java looping blocks
  • when to use ON phrases
  • How to leave/iterate the 4GL-style blocks

Coding the User Interface

Frame definitions (creating, using)

Working with frames

  • each frame should be defined in a package inside the srcnew/com/company/app/ui/ folder
  • explain how to structure the frame's interface and definition class, i.e.:
  • what setters/getters need to be defined for each 4GL type (get*, set*, widget*), how to best group them
  • how to best group the widget settings in the frame's definition setup() method - keep all “setLabel” and such calls grouped ? Group the calls per widget ?
  • how are the extents defined ?
  • how validators can be added to the frame ?
  • when two or more screens have some common layout and fields, its possible to use Java inheritance to define a frame which has the common layout and after this, extend it and create each separate frame. (see the aero/timco/majic/ui/ap/PendingInvoicesReportFrame.java and FindInvoicesFrame.java hierarchy).
  • to scope a frame to a certain block:
  • if the frame is scoped to a procedure/function block, add a frame.openScope(); call at the beginning of the body() method
  • if the frame is scoped to an internal block, add a frame.openScope(); in the Block.init() method
  • when a certain frame needs to display its data in many code locations, is better to add a dedicated Java method which will be called in all places where frame display is needed.
    • how to best structure a frame definition
    • you can define the frame in a 4GL program, convert and see how the results look
    • which widgets are available, how to define them
    • how to display a frame
    • how to read data with a frame
    • how to work with events

Accessing the Database

  • if complex queries are not needed to be used against a set of data which is collected in-memory, java collections can be used faster when data needs to be passed to other methods and/or held for further processing. Also, Java loops (i.e. FOR, WHILE, etc) can be easier used against such data which needs to be processed, instead of temp-tables.

Working with buffers/tables

  • for the buffers scoped to the procedure/function block, a RecordBuffer.openScope(buffer(s)); call needs to be added at the beginning of the body() method
  • for buffers scoped to other inner blocks, add the RecordBuffer.openScope() call to the Block.init() method.

Persistence issues (DMOs, Hibernate ORMs, dmo_index.xml, temp-tables, using record buffers, schema changes, manual data insertions, using SQL vs. HQL)

Transactions

  • explain that only 4GL-style variables can be used in transactions
  • what to be careful when marking a variable as no-undo or used in a transaction
  • other pitfalls the developer needs to be aware.

Issues when using Persistence to access the physical DB:

- the persistence-related code bypasses 4GL validation entirely

- any validation errors will be triggered during commit and must be handled by user code

- also, RecordBuffer API's can't be used

  • when to use 4GL-style buffers and queries and when to use SQL and/or HQL to access records
  • limitations when bypassing 4GL buffers
  • when to use temp-tables
  • can be replaced by Java collections/maps if complex queries are not needed
    • Transactions
  • how and when to start a 4GL-style transaction
  • how to use subtransactions
  • how to use DB-level transactions (i.e. Persistence APIs)
  • CRUD operations, locks (in 4GL-style code and in low-level code)
    • 4GL-style Queries
  • avoid client-side WHERE expressions at all costs
  • avoid accumulators
  • show how a 4GL-style query block looks like
  • what WHERE clause operators are available and when to use them
    • dmo_index.xml and how to edit it
    • how to define new DMOs and how to change existing ones
    • how to connect to other databases

Executing Other Programs

Integration points between hand-written code and converted code - customer-specific rules, name_map inserts + bogus run stmts...

In Developing 4GL Code During and After Conversion, in sections Calling a Java “External Procedure” and Calling a Java Method, there are lots of details about how to use the name_map_merge.xml file and how to call Java static methods - should those be moved here ?

  • how to run existing 4GL converted programs
  • how to run from 4GL code a new Java program

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